Skip to content

Commit d946f59

Browse files
committed
chore: add explicit code metadata migration
1 parent 17aca3d commit d946f59

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {promises as fs} from 'node:fs';
2+
import path from 'node:path';
3+
4+
const PAGES_ROOT = path.resolve('src/pages');
5+
const PLUGIN_PATH = path.resolve('src/plugins/rehype-content-figures.ts');
6+
const FENCE_PATTERN = /^(`{3,})([^\s`]*)[ \t]+([^\n]+)$/gm;
7+
const EXPLICIT_META_PATTERN = /(?:^|\s)(?:filename|file|title)=/i;
8+
const FILENAME_PATTERN = /^(?!.*\s)(?:(?:.+\/)?[^/]+\.[^/]+|(?:.+\/)?(?:Dockerfile|Jenkinsfile|Makefile|Procfile))$/iu;
9+
10+
const replaceRequired = (source, search, replacement, description) => {
11+
const next = source.replace(search, replacement);
12+
13+
if (next === source) {
14+
throw new Error(`Could not update ${description}`);
15+
}
16+
17+
return next;
18+
};
19+
20+
const walk = async (directory) => {
21+
const entries = await fs.readdir(directory, {withFileTypes: true});
22+
const files = [];
23+
24+
for (const entry of entries) {
25+
const location = path.join(directory, entry.name);
26+
27+
if (entry.isDirectory()) {
28+
files.push(...(await walk(location)));
29+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
30+
files.push(location);
31+
}
32+
}
33+
34+
return files;
35+
};
36+
37+
const escapeMetaValue = (value) => value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
38+
39+
let migratedFences = 0;
40+
const markdownFiles = await walk(PAGES_ROOT);
41+
42+
for (const file of markdownFiles) {
43+
const source = await fs.readFile(file, 'utf8');
44+
const migrated = source.replace(FENCE_PATTERN, (line, fence, language, rawMeta) => {
45+
const meta = rawMeta.trim();
46+
47+
if (!language || EXPLICIT_META_PATTERN.test(meta)) {
48+
return line;
49+
}
50+
51+
migratedFences += 1;
52+
const key = FILENAME_PATTERN.test(meta) ? 'filename' : 'title';
53+
54+
return `${fence}${language} ${key}="${escapeMetaValue(meta)}"`;
55+
});
56+
57+
if (/<fieldset>\s*<legend>[^<]+<\/legend>[\s\S]*?```/u.test(migrated)) {
58+
throw new Error(`Decorative fieldset remains in ${path.relative(process.cwd(), file)}`);
59+
}
60+
61+
if (/code-example-group/u.test(migrated)) {
62+
throw new Error(`Temporary code example group remains in ${path.relative(process.cwd(), file)}`);
63+
}
64+
65+
if (migrated !== source) {
66+
await fs.writeFile(file, migrated);
67+
}
68+
}
69+
70+
let plugin = await fs.readFile(PLUGIN_PATH, 'utf8');
71+
72+
plugin = replaceRequired(
73+
plugin,
74+
' readonly label?: string;',
75+
' readonly title?: string;',
76+
'CodeMeta title field',
77+
);
78+
79+
plugin = replaceRequired(
80+
plugin,
81+
/const isFilename = \(value: string\): boolean =>\n\s*!\/\\s\/\.test\(value\) && \/\(\?:\^\|\\\/\)\[\\w@\.\-\]\+\(\?:\\\.\[\\w\-\]\+\)\+\$\/u\.test\(value\);/u,
82+
`const isFilename = (value: string): boolean =>\n /^(?!.*\\s)(?:(?:.+\\/)?[^/]+\\.[^/]+|(?:.+\\/)?(?:Dockerfile|Jenkinsfile|Makefile|Procfile))$/iu.test(\n value,\n );`,
83+
'filename detection',
84+
);
85+
86+
plugin = replaceRequired(
87+
plugin,
88+
/const getExplicitCodeMeta = \(pre: Element, code: Element\): CodeMeta => \{[\s\S]*?\n\};\n\nconst getCommentCodeMeta/u,
89+
`const getExplicitCodeMeta = (pre: Element, code: Element): CodeMeta => {\n const propertyFilename = pre.properties.dataFilename ?? code.properties.dataFilename;\n const meta = getCodeMetaString(code);\n const filenameMatch = meta.match(\n /(?:^|\\s)(?:filename|file)=(?:\"([^\"]+)\"|'([^']+)'|([^\\s]+))/i,\n );\n const titleMatch = meta.match(\n /(?:^|\\s)title=(?:\"([^\"]+)\"|'([^']+)'|([^\\s]+))/i,\n );\n const matchedFilename = filenameMatch?.slice(1).find(Boolean)?.trim();\n const matchedTitle = titleMatch?.slice(1).find(Boolean)?.trim();\n const filename =\n typeof propertyFilename === 'string' && propertyFilename.trim()\n ? propertyFilename.trim()\n : matchedFilename;\n const remainingMeta = [filenameMatch?.[0], titleMatch?.[0]]\n .filter((value): value is string => Boolean(value))\n .reduce((value, match) => value.replace(match, ' '), meta)\n .replace(/\\s+/g, ' ')\n .trim();\n const plainMeta = unwrapQuotes(remainingMeta);\n const fallbackFilename = !filename && isFilename(plainMeta) ? plainMeta : undefined;\n const title = matchedTitle ?? (plainMeta && !fallbackFilename ? plainMeta : undefined);\n\n return {\n filename: filename ?? fallbackFilename,\n title,\n };\n};\n\nconst getCommentCodeMeta`,
90+
'explicit filename and title parsing',
91+
);
92+
93+
plugin = replaceRequired(
94+
plugin,
95+
` const label =\n separatorIndex >= 0 ? comment.slice(0, separatorIndex).trim() : undefined;\n\n return {\n filename: candidate,\n label: label || undefined,\n };`,
96+
` const title =\n separatorIndex >= 0 ? comment.slice(0, separatorIndex).trim() : undefined;\n\n return {\n filename: candidate,\n title: title || undefined,\n };`,
97+
'comment metadata title',
98+
);
99+
100+
plugin = replaceRequired(
101+
plugin,
102+
' return explicit.filename || explicit.label ? explicit : getCommentCodeMeta(code);',
103+
' return explicit.filename || explicit.title ? explicit : getCommentCodeMeta(code);',
104+
'explicit metadata fallback',
105+
);
106+
107+
plugin = replaceRequired(
108+
plugin,
109+
' const {filename, label} = code ? getCodeMeta(pre, code) : {};',
110+
' const {filename, title} = code ? getCodeMeta(pre, code) : {};',
111+
'code figure metadata destructuring',
112+
);
113+
114+
plugin = replaceRequired(
115+
plugin,
116+
` filename && label\n ? \`\${label} · \${filename}\`\n : (filename ?? label ?? languageLabel);`,
117+
` filename && title\n ? \`\${title} · \${filename}\`\n : (filename ?? title ?? languageLabel);`,
118+
'code figure caption',
119+
);
120+
121+
await fs.writeFile(PLUGIN_PATH, plugin);
122+
console.log(`Migrated ${migratedFences} code fences to explicit metadata.`);

0 commit comments

Comments
 (0)