This commit is contained in:
Timothy Jaeryang Baek
2026-07-02 15:36:26 -05:00
parent f251c795d0
commit 5b5d81b2b7
2 changed files with 337 additions and 0 deletions
+2
View File
@@ -6,6 +6,8 @@
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"postbuild": "node scripts/generate-raw-docs.mjs",
"raw-docs": "node scripts/generate-raw-docs.mjs",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
+335
View File
@@ -0,0 +1,335 @@
import fs from "node:fs";
import path from "node:path";
import * as cheerio from "cheerio";
const DOCS_DIR = "docs";
const BUILD_DIR = "build";
const SITE_URL = "https://docs.openwebui.com";
function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
return entries.flatMap((entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
return walk(fullPath);
}
return [fullPath];
});
}
function isDocPage(filePath) {
if (!/\.(md|mdx)$/.test(filePath)) {
return false;
}
if (filePath.includes(`${path.sep}tab-`)) {
return false;
}
if (path.basename(filePath).startsWith("_template.")) {
return false;
}
return true;
}
function parseFrontMatter(source) {
if (!source.startsWith("---\n")) {
return { frontMatter: {}, body: source };
}
const end = source.indexOf("\n---", 4);
if (end === -1) {
return { frontMatter: {}, body: source };
}
const raw = source.slice(4, end).trim();
const body = source.slice(end + 4).replace(/^\s*\n/, "");
const frontMatter = {};
for (const line of raw.split("\n")) {
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (match) {
frontMatter[match[1]] = match[2].replace(/^['"]|['"]$/g, "");
}
}
return { frontMatter, body };
}
function routeForDoc(filePath) {
const source = fs.readFileSync(filePath, "utf8");
const { frontMatter } = parseFrontMatter(source);
if (frontMatter.slug) {
return normalizeRoute(frontMatter.slug);
}
let route = filePath
.replace(
new RegExp(
`^${DOCS_DIR.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}${path.sep}`
),
""
)
.replace(/\.(md|mdx)$/, "")
.split(path.sep)
.join("/");
if (route.endsWith("/index")) {
route = route.slice(0, -"/index".length);
}
return normalizeRoute(`/${route}`);
}
function normalizeRoute(route) {
if (!route || route === "/") {
return "/";
}
return `/${route.replace(/^\/+|\/+$/g, "")}`;
}
function routeToHtmlPath(route) {
if (route === "/") {
return path.join(BUILD_DIR, "index.html");
}
return path.join(BUILD_DIR, route.slice(1), "index.html");
}
function routeToRawPath(route, extension) {
if (route === "/") {
return path.join(BUILD_DIR, `index.${extension}`);
}
return path.join(BUILD_DIR, `${route.slice(1)}.${extension}`);
}
function routeToRawHref(route, extension) {
if (route === "/") {
return `/index.${extension}`;
}
return `${route}.${extension}`;
}
function htmlUrl(route) {
return `${SITE_URL}${route === "/" ? "/" : route}`;
}
function textOf($, node) {
return $(node).text().replace(/\s+/g, " ").trim();
}
function tableToMarkdown($, table) {
const rows = [];
$(table)
.find("tr")
.each((_, tr) => {
const cells = $(tr)
.children("th,td")
.map((__, cell) => textOf($, cell).replace(/\|/g, "\\|"))
.get();
if (cells.length) {
rows.push(cells);
}
});
if (!rows.length) {
return "";
}
const width = Math.max(...rows.map((row) => row.length));
const normalized = rows.map((row) => [
...row,
...Array.from({ length: width - row.length }, () => ""),
]);
const header = normalized[0];
const separator = Array.from({ length: width }, () => "---");
const body = normalized.slice(1);
return [header, separator, ...body]
.map((row) => `| ${row.join(" | ")} |`)
.join("\n");
}
function convertChildren($, node) {
return $(node)
.contents()
.map((_, child) => convertNode($, child))
.get()
.join("");
}
function convertNode($, node) {
if (node.type === "text") {
return node.data.replace(/\s+/g, " ");
}
if (node.type !== "tag") {
return "";
}
const tag = node.name.toLowerCase();
const className = $(node).attr("class") ?? "";
const role = $(node).attr("role") ?? "";
const inner = convertChildren($, node).trim();
if (className.includes("hash-link") || role === "tablist") {
return "";
}
if (tag === "ul" && className.split(/\s+/).includes("tabs")) {
return "";
}
if (className.includes("theme-admonition")) {
const title = $(node).find(".admonitionHeading_Gvgb").first().text().trim();
const content = $(node).find(".admonitionContent_BuS1").first();
const body = content.length ? convertChildren($, content).trim() : inner;
return `\n\n> **${title}**\n>\n${body
.split("\n")
.map((line) => `> ${line}`)
.join("\n")}\n\n`;
}
if (/^h[1-6]$/.test(tag)) {
return `\n\n${"#".repeat(Number(tag[1]))} ${inner}\n\n`;
}
if (tag === "p") {
return `\n\n${inner}\n\n`;
}
if (tag === "br") {
return "\n";
}
if (tag === "strong" || tag === "b") {
return `**${inner}**`;
}
if (tag === "em" || tag === "i") {
return `_${inner}_`;
}
if (tag === "code") {
if ($(node).parents("pre").length) {
return $(node).text();
}
return `\`${$(node).text().replace(/`/g, "\\`")}\``;
}
if (tag === "pre") {
const code = $(node).text().replace(/\n+$/, "");
const language =
$(node)
.find("code")
.attr("class")
?.match(/language-([^\s]+)/)?.[1] ?? "";
return `\n\n\`\`\`${language}\n${code}\n\`\`\`\n\n`;
}
if (tag === "a") {
const href = $(node).attr("href");
return href ? `[${inner || href}](${href})` : inner;
}
if (tag === "img") {
const src = $(node).attr("src");
const alt = $(node).attr("alt") ?? "";
return src ? `![${alt}](${src})` : "";
}
if (tag === "ul" || tag === "ol") {
return `\n${$(node)
.children("li")
.map((index, li) => {
const marker = tag === "ol" ? `${index + 1}.` : "-";
return `${marker} ${convertChildren($, li).trim()}`;
})
.get()
.join("\n")}\n\n`;
}
if (tag === "blockquote") {
return `\n\n${inner
.split("\n")
.map((line) => `> ${line}`)
.join("\n")}\n\n`;
}
if (tag === "table") {
return `\n\n${tableToMarkdown($, node)}\n\n`;
}
if (tag === "hr") {
return "\n\n---\n\n";
}
if (tag === "svg" || tag === "button") {
return "";
}
if (tag === "div") {
return inner ? `\n\n${inner}\n\n` : "";
}
return inner;
}
function compactMarkdown(markdown) {
return markdown
.replace(/[ \t]+\n/g, "\n")
.replace(/^[ \t]+(?=```)/gm, "")
.replace(/^[ \t]+(?=#{1,6} )/gm, "")
.replace(/\n{3,}/g, "\n\n")
.replace(/^\s+|\s+$/g, "")
.concat("\n");
}
function markdownFromHtml(htmlPath, route) {
const $ = cheerio.load(fs.readFileSync(htmlPath, "utf8"));
const content = $(".theme-doc-markdown.markdown").first();
if (!content.length) {
return "";
}
const markdown = compactMarkdown(convertChildren($, content));
return markdown.replace(/^(# .+\n)/, `$1\nSource: ${htmlUrl(route)}\n`);
}
function injectAlternateLinks(htmlPath, route) {
const html = fs.readFileSync(htmlPath, "utf8");
const mdHref = routeToRawHref(route, "md");
const txtHref = routeToRawHref(route, "txt");
const links = [
`<link rel="alternate" type="text/markdown" href="${mdHref}">`,
`<link rel="alternate" type="text/plain" href="${txtHref}">`,
].join("");
if (html.includes(`href="${mdHref}"`) && html.includes(`href="${txtHref}"`)) {
return;
}
fs.writeFileSync(htmlPath, html.replace("</head>", `${links}</head>`));
}
function writeRawFiles(route, markdown) {
for (const extension of ["md", "txt"]) {
const outputPath = routeToRawPath(route, extension);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, markdown);
}
}
if (!fs.existsSync(BUILD_DIR)) {
throw new Error("Run `npm run build` before generating raw docs.");
}
let count = 0;
const routes = new Set();
for (const filePath of walk(DOCS_DIR).filter(isDocPage)) {
const route = routeForDoc(filePath);
if (routes.has(route)) {
continue;
}
routes.add(route);
const htmlPath = routeToHtmlPath(route);
if (!fs.existsSync(htmlPath)) {
continue;
}
const markdown = markdownFromHtml(htmlPath, route);
if (!markdown) {
continue;
}
writeRawFiles(route, markdown);
injectAlternateLinks(htmlPath, route);
count += 1;
}
console.log(`Generated raw Markdown/Text for ${count} docs pages.`);