import type { PubLeafletBlocksUnorderedList, PubLeafletPagesLinearDocument, PubLeafletRichtextFacet, } from "@atcute/leaflet"; import type { BlockContent, DefinitionContent, Nodes, PhrasingContent, RootContent } from "mdast"; import type { Blob } from "@atcute/lexicons"; import type { Replacement, ReplacementCtx } from "./config"; import { visit } from "unist-util-visit"; export function generateBlocks( children: RootContent[], uploadedImages: Map, codeblockTheme?: string ) { codeblockTheme ??= "catppuccin-mocha"; return children .flatMap((val): PubLeafletPagesLinearDocument.Block | PubLeafletPagesLinearDocument.Block[] | null => { if (val.type == "heading") { const { text, facets } = getTextAndFacets(val.children, "", [], uploadedImages); return { $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.header", plaintext: text, level: Math.min(val.depth, 3), facets: facets, }, }; } else if (val.type == "thematicBreak") { return { $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.horizontalRule" }, }; } else if (val.type == "paragraph") { const { text, facets, blocks } = getTextAndFacets(val.children, "", [], uploadedImages); if (blocks.length > 0) { return blocks; } return { $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.text", plaintext: text, facets: facets, }, }; } else if (val.type == "code") { return { $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.code", plaintext: val.value, language: val.lang == null ? undefined : val.lang, syntaxHighlightingTheme: codeblockTheme, }, }; } else if (val.type == "blockquote") { for (const child of val.children) { if (child.type == "paragraph") { const { text, facets } = getTextAndFacets(child.children, "", [], uploadedImages); return { $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.blockquote", plaintext: text, facets: facets }, }; } } } else if (val.type == "list") { return { $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.unorderedList", children: val.children.map((listItem) => { const listChild: PubLeafletBlocksUnorderedList.ListItem = { $type: "pub.leaflet.blocks.unorderedList#listItem", content: undefined!, }; //only headers, images and text allowed for (const child of listItem.children) { if (child.type == "heading") { const { text, facets } = getTextAndFacets(child.children, "", [], uploadedImages); listChild.content = { $type: "pub.leaflet.blocks.header", plaintext: text, facets: facets, level: Math.min(child.depth, 3), }; break; } else if (child.type == "paragraph") { const check = listItem.checked == undefined ? "" : listItem.checked ? "[ ] " : "[x] "; const { text, facets } = getTextAndFacets(child.children, "", [], uploadedImages); listChild.content = { $type: "pub.leaflet.blocks.text", plaintext: check + text, facets: facets }; break; } } return listChild; }), }, }; } return null; }) .filter((val) => !!val); } export function gatherImages(children: RootContent[], res?: string[]) { res ??= []; const walkPhrasingContent = (children: PhrasingContent[], res: string[]) => { for (const child of children) { if (child.type == "image") { res.push(child.url); } else if ("children" in child) { res = walkPhrasingContent(child.children, res); } } return res; }; const walkBlockDefinitionContent = (children: (BlockContent | DefinitionContent)[], res: string[]) => { for (const child of children) { if (child.type == "paragraph") { res = walkPhrasingContent(child.children, res); } else if (child.type == "blockquote") { res = walkBlockDefinitionContent(child.children, res); } else if (child.type == "list") { for (const listItem of child.children) { res = walkBlockDefinitionContent(listItem.children, res); } } } return res; }; for (const child of children) { if (child.type == "image") { res.push(child.url); } else if (child.type == "paragraph") { res = walkPhrasingContent(child.children, res); } else if (child.type == "list") { for (const listItem of child.children) { res = walkBlockDefinitionContent(listItem.children, res); } } } return res; } export function getTextAndFacets( children: PhrasingContent[], text: string, facets: PubLeafletRichtextFacet.Main[], uploadedImages: Map, offset?: number, parents?: PhrasingContent[] ): { text: string; facets: PubLeafletRichtextFacet.Main[]; offset: number; blocks: PubLeafletPagesLinearDocument.Block[]; } { offset ??= 0; parents ??= []; let blocks: PubLeafletPagesLinearDocument.Block[] = []; const encoder = new TextEncoder("utf-8"); for (const content of children) { if (content.type == "text") { const availableFacets = parents.filter( (val) => val.type == "emphasis" || val.type == "strong" || val.type == "link" || val.type == "delete" ); if (availableFacets.length > 0) facets = [ { $type: "pub.leaflet.richtext.facet", features: parents .filter( (val) => val.type == "emphasis" || val.type == "strong" || val.type == "link" || val.type == "delete" ) .map((val): PubLeafletRichtextFacet.Main["features"][0] => { if (val.type == "emphasis") { return { $type: "pub.leaflet.richtext.facet#italic" }; } if (val.type == "link") { return { $type: "pub.leaflet.richtext.facet#link", uri: val.url as PubLeafletRichtextFacet.Link["uri"], }; } if (val.type == "delete") { return { $type: "pub.leaflet.richtext.facet#strikethrough", }; } return { $type: "pub.leaflet.richtext.facet#bold" }; }), index: { $type: "pub.leaflet.richtext.facet#byteSlice", byteStart: offset, byteEnd: offset + encoder.encode(content.value).length, }, }, ...facets, ]; text += content.value; offset += encoder.encode(content.value).length; } else if (content.type == "break") { blocks.push({ $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.text", plaintext: text, facets: facets, }, }); text = ""; facets = []; offset = 0; } else if ( content.type == "emphasis" || content.type == "strong" || content.type == "link" || content.type == "delete" ) { const res = getTextAndFacets(content.children, text, facets, uploadedImages, offset, [content, ...parents!]); facets = [...res.facets]; offset = res.offset; text = res.text; blocks = [...blocks, ...res.blocks]; } else if (content.type == "inlineCode") { facets = [ { $type: "pub.leaflet.richtext.facet", index: { $type: "pub.leaflet.richtext.facet#byteSlice", byteStart: offset, byteEnd: offset + encoder.encode(content.value).length, }, features: [{ $type: "pub.leaflet.richtext.facet#code" }], }, ...facets, ]; text += content.value; offset += encoder.encode(content.value).length; } else if (content.type == "image") { if (text != "") { blocks.push({ $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.text", plaintext: text, facets: facets, }, }); text = ""; facets = []; offset = 0; } blocks.push({ $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.image", image: uploadedImages.get(content.url)!.blob, alt: !content.alt ? undefined : content.alt, aspectRatio: { $type: "pub.leaflet.blocks.image#aspectRatio", height: uploadedImages.get(content.url)!.height, width: uploadedImages.get(content.url)!.width, }, }, }); } } facets = facets.filter((val, _i, array) => { const samePosFacets = array.filter( (facet) => facet.index.byteStart == val.index.byteStart && facet.index.byteEnd == val.index.byteEnd ); if (samePosFacets.length > 1) { const mostFacetsObj = samePosFacets.reduce((prev, curr) => prev.features.length < curr.features.length ? curr : prev ); if (val == mostFacetsObj) return true; return false; } return true; }); if (blocks.length > 0) { blocks.push({ $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.text", plaintext: text, facets: facets }, }); } blocks = blocks.filter((val) => { if (val.block.$type == "pub.leaflet.blocks.text" && val.block.plaintext.trim() == "") { return false; } return true; }); return { text, facets, offset, blocks }; } export function replaceInAst(tree: Nodes, replacement: Replacement, context: ReplacementCtx) { const regex = /{{([-\w]+?)}}/g; const getValue = (key: string) => { if (Array.isArray(replacement)) { return replacement.find((val) => val[0] == key)?.[1]; } else { return replacement(key, context); } }; visit(tree, "text", function (node, index, parent) { const regexRes = regex.exec(node.value); if (regexRes) { const key = [...regexRes][1]!; const val = getValue(key); if (val) node.value = node.value.substring(0, regexRes.index) + val + node.value.substring(regexRes.index + regexRes[0].length); } }); visit(tree, "link", function (node, index, parent) { const regexRes = regex.exec(node.url); if (regexRes) { const key = [...regexRes][1]!; const val = getValue(key); if (val) node.url = node.url.substring(0, regexRes.index) + val + node.url.substring(regexRes.index + regexRes[0].length); } }); }