|
| 1 | +/* eslint-disable indent */ |
| 2 | +import {unified} from "unified"; |
| 3 | +import rehypeParse from "rehype-parse"; |
| 4 | +import rehypeRemark from "rehype-remark"; |
| 5 | +import remarkStringify from "remark-stringify"; |
| 6 | +import rehypeIgnore from "rehype-ignore"; |
| 7 | +import rehypeFormat from "rehype-format"; |
| 8 | +import remarkGfm from "remark-gfm"; |
| 9 | +import rehypeVideo from "rehype-video"; |
| 10 | +import fs from "node:fs"; |
| 11 | +import path from "node:path"; |
| 12 | +import {toHtml} from "hast-util-to-html"; |
| 13 | +import {JSDOM} from "jsdom"; |
| 14 | + |
| 15 | +const aElementRegex = /<a.*?href="(.*?)".*?>([^<]*)<\/a>/; |
| 16 | + |
| 17 | +function escapeMarkdown(input) { |
| 18 | + const map = { |
| 19 | + "|": "|" |
| 20 | + }; |
| 21 | + |
| 22 | + const escapeLine = (line) => { |
| 23 | + let out = line; |
| 24 | + for (const key of Object.keys(map)) { |
| 25 | + out = out.replaceAll(key, map[key]); |
| 26 | + } |
| 27 | + return out; |
| 28 | + }; |
| 29 | + |
| 30 | + return input |
| 31 | + .split("\n") |
| 32 | + .map((line) => { |
| 33 | + const trimmed = line.trimStart(); |
| 34 | + |
| 35 | + if (trimmed.startsWith("|")) { |
| 36 | + return line; |
| 37 | + } |
| 38 | + |
| 39 | + return escapeLine(line); |
| 40 | + }) |
| 41 | + .join("\n"); |
| 42 | +} |
| 43 | + |
| 44 | +function fixMarkdown(input) { |
| 45 | + return input.replaceAll("\\<optional>", "Optional").replaceAll("<optional>", "Optional"); |
| 46 | +} |
| 47 | + |
| 48 | +async function htmlToMarkdown(options = {}) { |
| 49 | + const file = await unified() |
| 50 | + .use(rehypeParse, {fragment: true}) |
| 51 | + .use(rehypeIgnore) |
| 52 | + .use(remarkGfm) |
| 53 | + .use(rehypeVideo) |
| 54 | + .use(rehypeFormat) |
| 55 | + .use(rehypeRemark, { |
| 56 | + document: false, |
| 57 | + handlers: { |
| 58 | + table(state, node) { |
| 59 | + // This fixes nested tables that happen in the conversion from html to markdown |
| 60 | + let html = toHtml(node); |
| 61 | + const parsedHTML = new JSDOM(html).window.document.getElementsByClassName("params")[1]; |
| 62 | + if (parsedHTML !== undefined) { |
| 63 | + html = parsedHTML.outerHTML; |
| 64 | + } |
| 65 | + const result = {type: "html", value: html}; |
| 66 | + state.patch(node, result); |
| 67 | + return result; |
| 68 | + }, |
| 69 | + a(state, node) { |
| 70 | + // This fixes internal linking e.g. from #~myawesomemethod to #myawesomemethod |
| 71 | + const result = {type: "html", value: toHtml(node).replaceAll("~", "")}; |
| 72 | + let href = result.value.match(aElementRegex); |
| 73 | + // Filters for a tags that need fixing |
| 74 | + // href !== null - Checks if href is null (A element has no href) |
| 75 | + // href[1].split("#")[1] != null - Checks if the a element is an internal link |
| 76 | + // !href[2].includes("line") - Checks for links to GitHub source code |
| 77 | + if (href !== null && href[1].split("#")[1] != null && !href[2].includes("line")) { |
| 78 | + const text = href[2]; |
| 79 | + href = href[1]; |
| 80 | + result.value = |
| 81 | + result.value.replace(href, href.split("#")[0] + "#" + href.split("#")[1].toLowerCase()) |
| 82 | + .replace(text, text.replace(href.split("#")[1], "") + "#" + href.split("#")[1]) |
| 83 | + .replace("##", "#"); |
| 84 | + } |
| 85 | + state.patch(node, result); |
| 86 | + return result; |
| 87 | + } |
| 88 | + } |
| 89 | + }) |
| 90 | + .use(remarkStringify, { |
| 91 | + commonmark: true, |
| 92 | + entities: true |
| 93 | + }) |
| 94 | + .processSync(options.html); |
| 95 | + return String(file); |
| 96 | +} |
| 97 | + |
| 98 | +const deadLinks = []; |
| 99 | +const deadLinksCheckPromises = []; |
| 100 | +const excludedFiles = {"index.html": "", "global.html": "", "custom.css": ""}; |
| 101 | +const inputDirectory = path.join("dist", "api"); |
| 102 | +const outputDirectory = path.join("docs", "api"); |
| 103 | +let packageTagName = "https://github.com/UI5/cli/blob/main/packages/"; |
| 104 | + |
| 105 | +// 1. Find tag name |
| 106 | +if (process.argv[2] == "gh-pages") { |
| 107 | + // Read package.json of packages/builder |
| 108 | + const builderJson = JSON.parse(fs.readFileSync("tmp/packages/@ui5/builder/package.json")); |
| 109 | + packageTagName = `https://github.com/UI5/cli/blob/cli-v${builderJson["version"]}/packages/`; |
| 110 | +} |
| 111 | + |
| 112 | +// 2. Check and create api directory, also remove all existing files if it exists |
| 113 | +if (!fs.existsSync(outputDirectory)) { |
| 114 | + fs.mkdirSync(outputDirectory); |
| 115 | +} else { |
| 116 | + for (const file of fs.readdirSync(outputDirectory)) { |
| 117 | + fs.rmSync(path.join(outputDirectory, file)); |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +// 3. Iterate through every .html generated by jsdoc |
| 122 | +for (const file of fs.readdirSync(path.join("dist", "api"))) { |
| 123 | + // Skip some files |
| 124 | + if (excludedFiles[file] !== undefined) continue; |
| 125 | + |
| 126 | + // Skip js files |
| 127 | + if (file.endsWith(".js.html")) continue; |
| 128 | + |
| 129 | + const filePath = path.join(inputDirectory, file); |
| 130 | + // Skip directories |
| 131 | + if (fs.statSync(filePath).isDirectory()) continue; |
| 132 | + |
| 133 | + const mardownPath = path.join(outputDirectory, file.replace(".html", ".md")); |
| 134 | + console.log("HTML -> Markdown", "|", filePath, "->", mardownPath); |
| 135 | + |
| 136 | + // Read the html file |
| 137 | + const htmlString = fs.readFileSync(filePath); |
| 138 | + |
| 139 | + // Convert html to markdown |
| 140 | + let markdown = await htmlToMarkdown({ |
| 141 | + html: htmlString |
| 142 | + }); |
| 143 | + |
| 144 | + // Escape characters in markdown |
| 145 | + markdown = escapeMarkdown(markdown); |
| 146 | + |
| 147 | + // Fix some markdown syntax e.g. <optional> -> Optional |
| 148 | + markdown = fixMarkdown(markdown); |
| 149 | + |
| 150 | + // Point all js links to GitHub |
| 151 | + const linkMatches = markdown.matchAll(/"[^"]*\.[A-Za-z0-9]+\.[A-Za-z0-9]+[^"]*"/g); |
| 152 | + for (const linkMatch of linkMatches) { |
| 153 | + if (!linkMatch[0].endsWith(".js.html\"")) continue; |
| 154 | + const githubUrl = packageTagName + |
| 155 | + linkMatch[0].replaceAll("\"", "").replace(".js.html", "").replaceAll("_", "/") + |
| 156 | + ".js"; |
| 157 | + markdown = markdown |
| 158 | + .replaceAll(linkMatch[0].replaceAll("\"", "") + "#line", githubUrl + "#L") |
| 159 | + .replaceAll(linkMatch[0].replaceAll("\"", ""), githubUrl); |
| 160 | + } |
| 161 | + |
| 162 | + // Add target="_blank" to the a element (Must link to external site) for a better user experience |
| 163 | + markdown = markdown.replaceAll("<a href=\"http", "<a target=\"_blank\" href=\"http"); |
| 164 | + |
| 165 | + markdown = stripHtmlComments(markdown); |
| 166 | + |
| 167 | + for (const aElement of markdown.matchAll(aElementRegex + "g")) { |
| 168 | + deadLinksCheckPromises.push(checkDeadlinks(aElement[1], filePath)); |
| 169 | + } |
| 170 | + |
| 171 | + // Save markdown file |
| 172 | + fs.writeFileSync( |
| 173 | + mardownPath, |
| 174 | + markdown |
| 175 | + ); |
| 176 | + |
| 177 | + Promise.all(deadLinksCheckPromises); |
| 178 | +} |
| 179 | + |
| 180 | +async function checkDeadlinks(link, sourcePath) { |
| 181 | + if ((await fetch(link)).status != 200) { |
| 182 | + deadLinks.push({ |
| 183 | + link: link, |
| 184 | + sourcePath: sourcePath |
| 185 | + }); |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +function stripHtmlComments(str) { |
| 190 | + return str.replace(/<!--[\s\S]*?-->/g, ""); |
| 191 | +} |
| 192 | + |
| 193 | +console.log("Conversion done"); |
| 194 | +console.log("Found", deadLinks.length, "dead links"); |
| 195 | +if (deadLinks.length != 0) { |
| 196 | + console.log(deadLinks); |
| 197 | +} |
0 commit comments