@alauda/doom
Version:
Doctor Doom making docs.
103 lines (102 loc) • 4.14 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import { glob } from 'tinyglobby';
import { visit } from 'unist-util-visit';
import { xfetch } from 'x-fetch';
import { parse, stringify } from 'yaml';
import { FALSY_VALUES, TRUTHY_VALUES } from "../shared/index.js";
export const parseBoolean = (value) => value === undefined || !FALSY_VALUES.has(value);
export const parseBooleanOrString = (value) => value === undefined ||
(FALSY_VALUES.has(value) ? false : TRUTHY_VALUES.has(value) || value);
const DOC_PATTERN = /\.mdx?$/;
export const isDoc = (filename) => DOC_PATTERN.test(filename);
export const getMatchedDocFilePaths = (matched) => Promise.all(matched.map(async (it) => {
const stat = await fs.stat(it);
if (stat.isDirectory()) {
return glob('**/*.md{,x}', {
absolute: true,
cwd: it,
});
}
if (stat.isFile() && isDoc(it)) {
return it;
}
return [];
}));
export const stringifyMatter = (frontmatter, content) => '---\n' +
stringify(frontmatter) +
'---\n' +
(content.startsWith('\n') ? content : '\n' + content);
/**
* Support custom id like `#hello world {#custom-id}`
* Avoid https://mdxjs.com/docs/troubleshooting-mdx/#could-not-parse-expression-with-acorn-error
* {@link https://github.com/web-infra-dev/rspress/blob/f3e6544780a371d7c629d8784f31dbcf28fb2b07/packages/core/src/node/utils/escapeHeadingIds.ts}
*/
export function escapeMarkdownHeadingIds(content) {
const markdownHeadingRegexp = /(?:^|\n)#{1,6}(?!#).*/g;
return content.replace(markdownHeadingRegexp, (substring) => substring
.replace('{#', '\\{#')
// prevent duplicate escaping
.replace('\\\\{#', '\\{#'));
}
export const defaultGitHubUrl = (url) => /^https?:\/\//.test(url)
? url
: `https://github.com/${url.replace(/^(?:\/*github.com)?\/+/i, '')}`;
const parseTerms_ = async () => {
const terms = await xfetch(process.env.RAW_TERMS_URL ||
'https://gitlab-ce.alauda.cn/alauda-public/product-doc-guide/-/raw/main/terms.yaml', { type: 'text' });
return parse(terms);
};
let parsedTermsCache;
export const parseTerms = () => (parsedTermsCache ??= parseTerms_());
const RELATIVE_FILE_META_REGEX = /(^|\s)(file)(\s*=\s*)(['"`]?)(\.\.?\/[^\s'"`]+)\4/g;
export const translateCodeFile = (content, { sourceBase, targetBase }) => {
visit(content, 'code', (code) => {
const nextMeta = code.meta?.replace(RELATIVE_FILE_META_REGEX, (_match, prefix, key, equals, quote, value) => `${prefix}${key}${equals}${quote}${path.relative(targetBase, path.resolve(sourceBase, value))}${quote}`);
if (nextMeta !== code.meta) {
code.meta = nextMeta;
}
});
return content;
};
const CODE_BLOCK_PLACEHOLDER_PREFIX = '__DOOM_TRANSLATE_CODE_BLOCK_';
const CODE_BLOCK_PLACEHOLDER_PATTERN = new RegExp(`^${CODE_BLOCK_PLACEHOLDER_PREFIX}(\\d+)__$`);
const getCodeBlockPlaceholderIndex = (value) => {
const match = CODE_BLOCK_PLACEHOLDER_PATTERN.exec(value);
if (!match) {
return;
}
return +match[1];
};
export const replaceCodeBlocksWithPlaceholders = (content) => {
const placeholders = [];
visit(content, 'code', (code) => {
if (code.value.length <= 50) {
return;
}
const placeholder = `${CODE_BLOCK_PLACEHOLDER_PREFIX}${placeholders.length}__`;
placeholders.push({
node: { ...code },
placeholder,
});
code.value = placeholder;
delete code.lang;
delete code.meta;
});
return placeholders;
};
export const restoreCodeBlockPlaceholders = (content, placeholders) => {
visit(content, 'code', (code) => {
const value = code.value.trim();
const index = getCodeBlockPlaceholderIndex(value);
if (index == null) {
return;
}
const placeholder = placeholders.at(index);
if (!placeholder || placeholder.placeholder !== value) {
throw new Error(`Unmatched code block placeholder: ${value}`);
}
Object.assign(code, placeholder.node);
});
return content;
};