diff --git a/netlify/edge-functions/common/definition.ts b/netlify/edge-functions/common/definition.ts index 4bb3df25..7572c4f7 100644 --- a/netlify/edge-functions/common/definition.ts +++ b/netlify/edge-functions/common/definition.ts @@ -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 } + | { tag: "Link"; contents: Array } + | { tag: "Example"; contents: Array } + | { tag: "ExampleBlock"; contents: Array } + | { tag: "Signature"; contents: Array> } + | { tag: "SignatureInline"; contents: Array } + | { tag: "Eval"; contents: [Array, Array] } + | { + tag: "EvalInline"; + contents: [Array, Array]; + } + | { tag: "Embed"; contents: Array } + | { tag: "EmbedInline"; contents: Array }; + +type DocElement = + | { tag: "Word"; contents: string } + | { tag: "NamedLink"; contents: [DocElement, DocElement] } + | { tag: "Paragraph"; contents: Array } + | { tag: "Special"; contents: DocSpecial } + | { tag: "Span"; contents: Array } + | { tag: "UntitledSection"; contents: Array } + | { tag: "Section"; contents: [DocElement, Array] } + | { tag: "BulletedList"; contents: Array } + | { tag: "NumberedList"; contents: [Number, Array] } + | { tag: "Code"; contents: DocElement } + | { tag: "CodeBlock"; contents: [string, DocElement] } + | { tag: "Group"; contents: DocElement } + | { tag: "Join"; contents: Array } + | { tag: "Column"; contents: Array } + | { 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> }; type DefinitionSyntax = { contents: Array; @@ -11,7 +58,7 @@ type APITerm = { defnTermTag: string; signature: Array; termDefinition: DefinitionSyntax; - termDocs: Array; + termDocs: Array<[string, string, DocElement]>; }; type APIType = { @@ -33,5 +80,6 @@ export { APITerm, APIType, APIDefinitions, + DocSpecial, DocElement, }; diff --git a/netlify/edge-functions/social-image-helpers/docs.tsx b/netlify/edge-functions/social-image-helpers/docs.tsx new file mode 100644 index 00000000..fefd0c70 --- /dev/null +++ b/netlify/edge-functions/social-image-helpers/docs.tsx @@ -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

{go(e)}

; + case "Paragraph": + return

{go(e)}

; + default: + return go(e); + } + } + + function go(e: DocElement) { + switch (e.tag) { + case "Special": + const special = e.contents; + switch (special.tag) { + case "Source": + return ( + + + + ); + case "Link": + return ( + + + + ); + case "Example": + return ( + + + + ); + case "ExampleBlock": + return ( + + + + ); + case "EmbedInline": + return ( + + + + ); + case "Embed": + return ( + + + + ); + case "SignatureInline": + return ( + + + + ); + case "Signature": + return ( + + {special.contents.map((s) => ( + + ))} + + ); + case "Eval": + return ( + + + + + ); + case "EvalInline": + return ( + + + + + ); + + default: + return <>; + } + case "NamedLink": + return {go(e.contents[0])}; + case "Word": + return {e.contents}; + case "Paragraph": + return ( + + {intersperse( + e.contents.map(go), + + )} + + ); + case "Span": + return ( + + {intersperse( + e.contents.map(go), + + )} + + ); + case "UntitledSection": + return ( +
+ {e.contents.map(renderSectionContent)} +
+ ); + case "Section": + const [title, sectionContent] = e.contents as [ + DocElement, + Array + ]; + + return ( +
+

{go(title)}

+
+ {sectionContent.map(renderSectionContent)} +
+
+ ); + case "BulletedList": + return ( +
    + {e.contents.map((e) => ( +
  • + {go(e)} +
  • + ))} +
+ ); + case "NumberedList": + const [start, els] = e.contents; + return ( +
    + {els.map((e, i) => ( +
  1. + {start + i}{" "} + {go(e)} +
  2. + ))} +
+ ); + case "Code": + return ( + + {go(e.contents)} + + ); + case "Join": + return {e.contents}; + case "Group": + return {go(e.contents)}; + default: + return <>; + } + } + + const docs = go(props.docRoot); + + return ( +
+ {docs} + +
+ ); +} + +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; diff --git a/netlify/edge-functions/social-image-helpers/project-definition-social-image.tsx b/netlify/edge-functions/social-image-helpers/project-definition-social-image.tsx index 01171c5d..ac31e533 100644 --- a/netlify/edge-functions/social-image-helpers/project-definition-social-image.tsx +++ b/netlify/edge-functions/social-image-helpers/project-definition-social-image.tsx @@ -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 }) { - const segments = props.syntax.map((seg) => { - const annotationStyle = STYLES[seg.annotation?.tag] || {}; - const style = { - ...STYLES.segment, - ...annotationStyle, - }; - return {seg.segment}; - }); - - return ( -
-      {segments}
-      
-    
- ); -} - 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] ? ( + + ) : null; + + const Wrapper = docs ? SocialImageWithLargeSheet : SocialImageWithSheet; + + const bottomRow = docs ? ( + + {syntax} {docs} + + ) : ( + syntax + ); + return ( - + - + ); } @@ -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), }, }; diff --git a/netlify/edge-functions/social-image-helpers/sheet.tsx b/netlify/edge-functions/social-image-helpers/sheet.tsx index 0bbde350..ec236168 100644 --- a/netlify/edge-functions/social-image-helpers/sheet.tsx +++ b/netlify/edge-functions/social-image-helpers/sheet.tsx @@ -16,13 +16,8 @@ type SheetProps = { function renderRow(row: React.ReactNode): React.ReactNode { const rowSep = |; - - 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", diff --git a/netlify/edge-functions/social-image-helpers/syntax.tsx b/netlify/edge-functions/social-image-helpers/syntax.tsx new file mode 100644 index 00000000..229d4ca7 --- /dev/null +++ b/netlify/edge-functions/social-image-helpers/syntax.tsx @@ -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 {props.segment.segment}; +} + +function InlineSyntax(props: { syntax: Array }) { + const segments = props.syntax.map((seg) => ); + + return ( +
+      {segments}
+    
+ ); +} + +function Syntax(props: { syntax: Array }) { + const segments = props.syntax.map((seg) => ); + + return ( +
+      {segments}
+      
+    
+ ); +} + +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 };