Skip to content

Social Image: render definition docs #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 51 additions & 3 deletions netlify/edge-functions/common/definition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
type SyntaxSegment = { annotation?: { tag: string }; segment: string };
type DocElement = { annotation?: { tag: string }; segment: string };
type SyntaxSegment = {
annotation?: { tag: string };
segment: string;
};

type DocSpecial =
| { tag: "Source"; contents: Array<SyntaxSegment> }
| { tag: "Link"; contents: Array<SyntaxSegment> }
| { tag: "Example"; contents: Array<SyntaxSegment> }
| { tag: "ExampleBlock"; contents: Array<SyntaxSegment> }
| { tag: "Signature"; contents: Array<Array<SyntaxSegment>> }
| { tag: "SignatureInline"; contents: Array<SyntaxSegment> }
| { tag: "Eval"; contents: [Array<SyntaxSegment>, Array<SyntaxSegment>] }
| {
tag: "EvalInline";
contents: [Array<SyntaxSegment>, Array<SyntaxSegment>];
}
| { tag: "Embed"; contents: Array<SyntaxSegment> }
| { tag: "EmbedInline"; contents: Array<SyntaxSegment> };

type DocElement =
| { tag: "Word"; contents: string }
| { tag: "NamedLink"; contents: [DocElement, DocElement] }
| { tag: "Paragraph"; contents: Array<DocElement> }
| { tag: "Special"; contents: DocSpecial }
| { tag: "Span"; contents: Array<DocElement> }
| { tag: "UntitledSection"; contents: Array<DocElement> }
| { tag: "Section"; contents: [DocElement, Array<DocElement>] }
| { tag: "BulletedList"; contents: Array<DocElement> }
| { tag: "NumberedList"; contents: [Number, Array<DocElement>] }
| { tag: "Code"; contents: DocElement }
| { tag: "CodeBlock"; contents: [string, DocElement] }
| { tag: "Group"; contents: DocElement }
| { tag: "Join"; contents: Array<DocElement> }
| { tag: "Column"; contents: Array<DocElement> }
| { tag: "Image"; contents: [DocElement, DocElement, DocElement?] }
| { tag: "Folded"; contents: [DocElement, DocElement] }
| { tag: "Callout"; contents: [DocElement | null, DocElement] }
| { tag: "Aside"; contents: DocElement }
| { tag: "Tooltip"; contents: [DocElement, DocElement] }
| { tag: "SectionBreak"; contents: [] }
| { tag: "Blankline"; contents: [] }
| { tag: "Anchor"; contents: [string, DocElement] }
| { tag: "Style"; contents: [string, DocElement] }
| { tag: "Blockqoute"; contents: DocElement }
| { tag: "Italic"; contents: DocElement }
| { tag: "Bold"; contents: DocElement }
| { tag: "Strikethrough"; contents: DocElement }
| { tag: "Table"; contents: Array<Array<DocElement>> };

type DefinitionSyntax = {
contents: Array<SyntaxSegment>;
@@ -11,7 +58,7 @@ type APITerm = {
defnTermTag: string;
signature: Array<SyntaxSegment>;
termDefinition: DefinitionSyntax;
termDocs: Array<DocElement>;
termDocs: Array<[string, string, DocElement]>;
};

type APIType = {
@@ -33,5 +80,6 @@ export {
APITerm,
APIType,
APIDefinitions,
DocSpecial,
DocElement,
};
249 changes: 249 additions & 0 deletions netlify/edge-functions/social-image-helpers/docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*
Due to this bug: https://github.com/vercel/satori/issues/484
We just render the doc mostly on a single line and, hide the overflow with a fade.
*/
import React from "https://esm.sh/react@18.2.0";
import { DocSpecial, DocElement } from "../common/definition.ts";
import * as Sizing from "../common/sizing.ts";
import Colors from "../common/colors.ts";
import { intersperse } from "../common/utils.ts";
import { InlineSyntax, Syntax } from "./syntax.tsx";
import * as Fonts from "../common/fonts.ts";

function Docs(props: { docRoot: DocElement }) {
function renderSectionContent(e: DocElement) {
switch (e.tag) {
case "Span":
return <p style={STYLES.docBlock}>{go(e)}</p>;
case "Paragraph":
return <p style={STYLES.docBlock}>{go(e)}</p>;
default:
return go(e);
}
}

function go(e: DocElement) {
switch (e.tag) {
case "Special":
const special = e.contents;
switch (special.tag) {
case "Source":
return (
<span style={STYLES.docCode}>
<Syntax syntax={special.contents} />
</span>
);
case "Link":
return (
<span style={{ ...STYLES.docCode, ...STYLES.docCodeInline }}>
<InlineSyntax syntax={special.contents} />
</span>
);
case "Example":
return (
<span style={{ ...STYLES.docCode, ...STYLES.docCodeInline }}>
<InlineSyntax syntax={special.contents} />
</span>
);
case "ExampleBlock":
return (
<span style={STYLES.docCode}>
<Syntax syntax={special.contents} />
</span>
);
case "EmbedInline":
return (
<span style={{ ...STYLES.docCode, ...STYLES.docCodeInline }}>
<InlineSyntax syntax={special.contents} />
</span>
);
case "Embed":
return (
<span style={STYLES.docCode}>
<Syntax syntax={special.contents} />
</span>
);
case "SignatureInline":
return (
<span style={{ ...STYLES.docCode, ...STYLES.docCodeInline }}>
<InlineSyntax syntax={special.contents} />
</span>
);
case "Signature":
return (
<span style={STYLES.docCode}>
{special.contents.map((s) => (
<Syntax syntax={s} />
))}
</span>
);
case "Eval":
return (
<span style={{ ...STYLES.docCode }}>
<Syntax syntax={special.contents[0]} />
<Syntax syntax={special.contents[1]} />
</span>
);
case "EvalInline":
return (
<span style={{ ...STYLES.docCode, ...STYLES.docCodeInline }}>
<InlineSyntax syntax={special.contents[0]} />
<InlineSyntax syntax={special.contents[1]} />
</span>
);

default:
return <></>;
}
case "NamedLink":
return <a style={STYLES.docLink}>{go(e.contents[0])}</a>;
case "Word":
return <span>{e.contents}</span>;
case "Paragraph":
return (
<span style={STYLES.docInline}>
{intersperse(
e.contents.map(go),
<span style={STYLES.docSpace}> </span>
)}
</span>
);
case "Span":
return (
<span style={STYLES.docInline}>
{intersperse(
e.contents.map(go),
<span style={STYLES.docSpace}> </span>
)}
</span>
);
case "UntitledSection":
return (
<section style={STYLES.docBlock}>
{e.contents.map(renderSectionContent)}
</section>
);
case "Section":
const [title, sectionContent] = e.contents as [
DocElement,
Array<DocElement>
];

return (
<section style={STYLES.docBlock}>
<h1 style={STYLES.docBlock}>{go(title)}</h1>
<div style={STYLES.docBlock}>
{sectionContent.map(renderSectionContent)}
</div>
</section>
);
case "BulletedList":
return (
<ul style={STYLES.docList}>
{e.contents.map((e) => (
<li style={STYLES.docListItem}>
<span style={STYLES.docListItemBullet}>•</span> {go(e)}
</li>
))}
</ul>
);
case "NumberedList":
const [start, els] = e.contents;
return (
<ol start={start} style={STYLES.docList}>
{els.map((e, i) => (
<li style={STYLES.docListItem}>
<span style={STYLES.docListItemBullet}>{start + i}</span>{" "}
{go(e)}
</li>
))}
</ol>
);
case "Code":
return (
<code style={{ ...STYLES.docCode, ...STYLES.docCodeInline }}>
{go(e.contents)}
</code>
);
case "Join":
return <span style={STYLES.docInline}>{e.contents}</span>;
case "Group":
return <span style={STYLES.docInline}>{go(e.contents)}</span>;
default:
return <></>;
}
}

const docs = go(props.docRoot);

return (
<section style={STYLES.docs}>
{docs}
<span style={STYLES.docsFade}></span>
</section>
);
}

const STYLES = {
docs: {
display: "flex",
paddingTop: Sizing.toPx(2),
height: Sizing.toPx(5),
maxHeight: Sizing.toPx(5),
overflow: "hidden",
borderTop: `2px solid ${Colors.gray.lighten40}`,
fontSize: Sizing.toPx(2.25),
gap: 0,
position: "relative",
fontWeight: Fonts.Weights.semiBold,
border: "1px solid blue",
},
docsFade: {
position: "absolute",
top: -10,
right: `-${Sizing.toPx(2.5)}px`,
bottom: -10,
width: 400,
height: Sizing.toPx(6),
background: `linear-gradient(to right, rgba(255, 255, 255, 0), ${Colors.gray.lighten100}, ${Colors.gray.lighten100})`,
},
docBlock: {
display: "flex",
flexWrap: "wrap",
gap: 0,
marginTop: 0,
width: "100%",
},
docLink: {
color: Colors.blue1,
},
docList: {
display: "flex",
flexWrap: "wrap",
gap: Sizing.toPx(0.5),
marginTop: 0,
marginLeft: Sizing.toPx(1),
width: "100%",
},
docListItem: {
display: "flex",
flexWrap: "wrap",
gap: 0,
marginTop: 0,
width: "100%",
listStyleType: "disc",
},
docListItemBullet: {
marginRight: Sizing.toPx(1),
},
docInline: {},
docSpace: {
width: "8ch",
},
docCode: {
fontFamily: "monospace",
},
docCodeInline: {},
};

export default Docs;
Original file line number Diff line number Diff line change
@@ -1,32 +1,18 @@
import React from "https://esm.sh/react@18.2.0";
import { ShareAPI, SyntaxSegment } from "../common/share-api.ts";
import { ShareAPI } from "../common/share-api.ts";
import { DocSpecial, SyntaxSegment, DocElement } from "../common/definition.ts";
import Docs from "./docs.tsx";
import { Syntax } from "./syntax.tsx";
import { defaultSocialImage } from "./social-content.tsx";
import * as Sizing from "../common/sizing.ts";
import { Icon, IconType } from "./icon.tsx";
import Colors from "../common/colors.ts";
import SocialImageWithSheet from "./social-image-with-sheet.tsx";
import SocialImageWithLargeSheet from "./social-image-with-large-sheet.tsx";
import Sheet from "./sheet.tsx";
import { titleize, hash } from "../common/utils.ts";
import Tag from "./tag.tsx";

function Syntax(props: { syntax: Array<SyntaxSegment> }) {
const segments = props.syntax.map((seg) => {
const annotationStyle = STYLES[seg.annotation?.tag] || {};
const style = {
...STYLES.segment,
...annotationStyle,
};
return <span style={style}>{seg.segment}</span>;
});

return (
<pre style={STYLES.syntax}>
<code>{segments}</code>
<span style={STYLES.syntaxFade}></span>
</pre>
);
}

async function projectDefinitionSocialImage(
handle: string,
projectSlug: string,
@@ -66,6 +52,7 @@ async function projectDefinitionSocialImage(
category: raw.defnTermeTag,
signature: raw.signature,
definition: raw.termDefinition,
docs: raw.termDocs,
};
}
} else {
@@ -77,6 +64,7 @@ async function projectDefinitionSocialImage(
name: raw.bestTypeName,
category: raw.defnTypeTag,
definition: raw.typeDefinition,
docs: raw.typeDocs,
};
}
}
@@ -149,15 +137,30 @@ async function projectDefinitionSocialImage(
topRowRight = [];
}

const docs =
definition.docs[0] && definition.docs[0][2] ? (
<Docs docRoot={definition.docs[0][2]} />
) : null;

const Wrapper = docs ? SocialImageWithLargeSheet : SocialImageWithSheet;

const bottomRow = docs ? (
<span style={STYLES.syntaxAndDocs}>
{syntax} {docs}
</span>
) : (
syntax
);

return (
<SocialImageWithSheet>
<Wrapper>
<Sheet
title={definition.name}
topRowLeft={[projectRef, branchRef_]}
topRowRight={topRowRight}
bottomRowLeft={syntax}
bottomRowLeft={bottomRow}
/>
</SocialImageWithSheet>
</Wrapper>
);
}

@@ -184,44 +187,11 @@ const STYLES = {
flexDirection: "column",
gap: Sizing.toPx(0.5),
},
syntax: {
position: "relative",
margin: 0,
padding: 0,
fontSize: Sizing.toPx(2),
lineHeight: 1.2,
width: "100%",
},
syntaxFade: {
position: "absolute",
top: -10,
right: `-${Sizing.toPx(2.5)}px`,
bottom: -10,
width: 400,
height: Sizing.toPx(4),
background: `linear-gradient(to right, rgba(255, 255, 255, 0), ${Colors.gray.lighten100}, ${Colors.gray.lighten100})`,
},
segment: {
color: Colors.gray.base,
fontFamily: "FiraCode",
},
DataTypeKeyword: {
color: Colors.gray.lighten30,
},
TypeAscriptionColon: {
color: Colors.gray.lighten30,
},
DataTypeModifier: {
color: Colors.gray.lighten30,
},
TypeOperator: {
color: Colors.gray.lighten30,
},
AbilityBraces: {
color: Colors.gray.lighten30,
},
DelimiterChar: {
color: Colors.gray.lighten30,
syntaxAndDocs: {
display: "flex",
flex: 1,
flexDirection: "column",
gap: Sizing.toPx(0.5),
},
};

21 changes: 12 additions & 9 deletions netlify/edge-functions/social-image-helpers/sheet.tsx
Original file line number Diff line number Diff line change
@@ -16,13 +16,8 @@ type SheetProps = {

function renderRow(row: React.ReactNode): React.ReactNode {
const rowSep = <span style={STYLES.rowSeparator}>|</span>;

const row_ = React.Children.toArray(row);

return intersperse(
row_.filter((e: unknown) => !!e),
rowSep
);
const row_ = React.Children.toArray(row).filter((e: unknown) => !!e);
return intersperse(row_, rowSep);
}

function Sheet(props: SheetProps) {
@@ -78,9 +73,12 @@ const STYLES = {
display: "flex",
flexDirection: "column",
gap: Sizing.toPx(1.25),
flex: 1,
},
rowSeparator: {
color: Colors.gray.lighten40,
paddingLeft: Sizing.toPx(1),
paddingRight: Sizing.toPx(1),
fontSize: Sizing.toPx(2),
fontWeight: Fonts.Weights.regular,
},
@@ -92,19 +90,21 @@ const STYLES = {
color: Colors.gray.lighten20,
fontSize: Sizing.toPx(2),
lineHeight: 1,
flex: 1,
},
topRowInner: {
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: Sizing.toPx(1),
lineHeight: 1,
padding: 0,
},
title: {
color: Colors.gray.darken30,
lineHeight: 1.2,
fontSize: Sizing.toPx(4.5),
margin: 0,
flex: 1,
},
bottomRow: {
display: "flex",
@@ -113,7 +113,10 @@ const STYLES = {
justifyContent: "space-between",
color: Colors.gray.lighten20,
fontSize: Sizing.toPx(1.5),
lineHeight: 1,
background: "yellow",
height: Sizing.toPx(12),
border: "1px solid red",
flex: 1,
},
bottomRowInner: {
display: "flex",
87 changes: 87 additions & 0 deletions netlify/edge-functions/social-image-helpers/syntax.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "https://esm.sh/react@18.2.0";
import { SyntaxSegment } from "../common/definition.ts";
import * as Sizing from "../common/sizing.ts";
import Colors from "../common/colors.ts";

function SyntaxSegment(props: { segment: SyntaxSegment }) {
const annotationStyle = props.segment.annotation?.tag
? STYLES[props.segment.annotation?.tag] || {}
: STYLES.Blank;
const style = {
...STYLES.segment,
...annotationStyle,
};
return <span style={style}>{props.segment.segment}</span>;
}

function InlineSyntax(props: { syntax: Array<SyntaxSegment> }) {
const segments = props.syntax.map((seg) => <SyntaxSegment segment={seg} />);

return (
<pre style={STYLES.inlineSyntax}>
<code>{segments}</code>
</pre>
);
}

function Syntax(props: { syntax: Array<SyntaxSegment> }) {
const segments = props.syntax.map((seg) => <SyntaxSegment segment={seg} />);

return (
<pre style={STYLES.syntax}>
<code>{segments}</code>
<span style={STYLES.syntaxFade}></span>
</pre>
);
}

const STYLES = {
syntax: {
position: "relative",
margin: 0,
padding: 0,
fontSize: Sizing.toPx(2),
lineHeight: 1.2,
border: "1px solid blue",
},
inlineSyntax: {
margin: 0,
padding: 0,
},
syntaxFade: {
position: "absolute",
top: -10,
right: `-${Sizing.toPx(2.5)}px`,
bottom: -10,
width: 400,
height: Sizing.toPx(4),
background: `linear-gradient(to right, rgba(255, 255, 255, 0), ${Colors.gray.lighten100}, ${Colors.gray.lighten100})`,
},
segment: {
color: Colors.gray.base,
fontFamily: "FiraCode",
},
Blank: {
color: Colors.gray.lighten30,
},
DataTypeKeyword: {
color: Colors.gray.lighten30,
},
TypeAscriptionColon: {
color: Colors.gray.lighten30,
},
DataTypeModifier: {
color: Colors.gray.lighten30,
},
TypeOperator: {
color: Colors.gray.lighten30,
},
AbilityBraces: {
color: Colors.gray.lighten30,
},
DelimiterChar: {
color: Colors.gray.lighten30,
},
};

export { Syntax, InlineSyntax, SyntaxSegment };