Something went wrong. Try again.
CLI tool to sync your Markdown to Leaflet
leafletpub atproto cli markdown
Something went wrong. Try again.
12 kB · 357 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358import 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<string, { blob: Blob; width: number; height: number }>, 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<string, { blob: Blob; width: number; height: number }>, 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); } });}