A design system in a box. hip-ui.tngl.io/docs/introduction
at main 113 lines 3.2 kB view raw
1import { glob } from "glob"; 2import { JSDOM } from "jsdom"; 3import { NodeHtmlMarkdown } from "node-html-markdown"; 4import { readFile, rm, writeFile } from "node:fs/promises"; 5import path from "node:path"; 6 7const distClientDir = path.resolve("dist/client"); 8 9function getLanguage(node) { 10 const codeNode = node.querySelector("code[class*='language-']"); 11 const className = codeNode?.getAttribute("class") ?? ""; 12 const match = className.match(/language-([\w-]+)/); 13 14 return match?.[1] ?? ""; 15} 16 17function escapeTableCellPipes(text) { 18 return text.replaceAll("|", String.raw`\|`); 19} 20 21function normalizeInlineWhitespace(text) { 22 return text.replaceAll(/\s+/g, " ").trim(); 23} 24 25function normalizeCodeBlocks(markdownRoot) { 26 const codeBlocks = markdownRoot.querySelectorAll("pre"); 27 28 for (const codeBlock of codeBlocks) { 29 const code = codeBlock.textContent?.replace(/\n$/, "") ?? ""; 30 const language = getLanguage(codeBlock); 31 const normalizedCodeBlock = markdownRoot.ownerDocument.createElement("pre"); 32 const normalizedCode = markdownRoot.ownerDocument.createElement("code"); 33 34 if (language) { 35 normalizedCode.className = `language-${language}`; 36 } 37 38 normalizedCode.textContent = code; 39 normalizedCodeBlock.replaceChildren(normalizedCode); 40 codeBlock.replaceWith(normalizedCodeBlock); 41 } 42} 43 44function normalizeTables(markdownRoot) { 45 const cells = markdownRoot.querySelectorAll("th, td"); 46 47 for (const cell of cells) { 48 const clone = cell.cloneNode(true); 49 50 for (const codeNode of clone.querySelectorAll("pre, code")) { 51 const codeText = normalizeInlineWhitespace(codeNode.textContent ?? ""); 52 codeNode.replaceWith(`\`${escapeTableCellPipes(codeText)}\``); 53 } 54 55 cell.textContent = escapeTableCellPipes( 56 normalizeInlineWhitespace(clone.textContent ?? ""), 57 ); 58 } 59} 60 61function normalizeMarkdown(markdown) { 62 return markdown 63 .replaceAll(/\n{3,}/g, "\n\n") 64 .trim() 65 .concat("\n"); 66} 67 68const markdownConverter = new NodeHtmlMarkdown({ 69 bulletMarker: "-", 70 codeBlockStyle: "fenced", 71}); 72 73async function exportMarkdownFile(htmlFilePath) { 74 const html = await readFile(htmlFilePath, "utf8"); 75 const dom = new JSDOM(html); 76 const markdownRoot = dom.window.document.querySelector( 77 "[data-markdown-export]", 78 ); 79 80 if (!markdownRoot) { 81 throw new Error(`Missing [data-markdown-export] in ${htmlFilePath}`); 82 } 83 84 normalizeCodeBlocks(markdownRoot); 85 normalizeTables(markdownRoot); 86 87 const markdown = normalizeMarkdown( 88 markdownConverter.translate(markdownRoot.innerHTML), 89 ); 90 const markdownDirectoryPath = path.dirname(htmlFilePath); 91 92 await rm(markdownDirectoryPath, { force: true, recursive: true }); 93 await writeFile(markdownDirectoryPath, markdown, "utf8"); 94} 95 96async function main() { 97 const htmlFilePaths = await glob("**/*.md/index.html", { 98 absolute: true, 99 cwd: distClientDir, 100 }); 101 102 if (htmlFilePaths.length === 0) { 103 throw new Error(`No markdown HTML exports found in ${distClientDir}`); 104 } 105 106 await Promise.all( 107 htmlFilePaths.map((htmlFilePath) => exportMarkdownFile(htmlFilePath)), 108 ); 109 110 console.log(`Exported ${htmlFilePaths.length} markdown files.`); 111} 112 113await main();