mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
ab52df470a
A leading UTF-8 byte order mark (U+FEFF) before the first `#` stops the Markdown parser from recognizing the heading, so the guide renders its title as a paragraph and drops the standard docs header. The character is invisible, so it cannot be caught in review. Add a check in the guides generation pipeline that throws when a source file starts with a BOM, failing the build with the offending file name. This sits alongside the existing unknown-anchor check and prevents the regression fixed in #69889 from recurring.
125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {readFile, writeFile} from 'fs/promises';
|
|
import path from 'path';
|
|
import {parseMarkdownAsync} from '../shared/marked/parse.mjs';
|
|
import {initHighlighter} from '../shared/shiki.mjs';
|
|
import {hasUnknownAnchors} from './helpers.mjs';
|
|
|
|
type ApiManifest = ApiManifestPackage[];
|
|
interface ApiManifestPackage {
|
|
moduleName: string;
|
|
entries: {name: string; aliases?: string[]}[];
|
|
}
|
|
|
|
async function main() {
|
|
const [paramFilePath] = process.argv.slice(2);
|
|
const rawParamLines = (await readFile(paramFilePath, {encoding: 'utf8'})).split('\n');
|
|
const [srcs, outputFilenameExecRootRelativePath, apiManifestPath, definedRoutesAsStr] =
|
|
rawParamLines;
|
|
|
|
// The highlighter needs to be setup asynchronously
|
|
// so we're doing it at the start of the pipeline
|
|
const highlighter = await initHighlighter();
|
|
|
|
let apiManifest: ApiManifest = [];
|
|
if (!apiManifestPath) {
|
|
throw new Error(
|
|
'No API manifest path provided to the markdown parser. Check the failing generate_guides target.',
|
|
);
|
|
}
|
|
|
|
const apiManifestStr = await readFile(apiManifestPath, {encoding: 'utf8'});
|
|
apiManifest = JSON.parse(apiManifestStr);
|
|
|
|
let definedRoutes: string[] = [];
|
|
if (!definedRoutesAsStr) {
|
|
throw new Error(
|
|
'No defined routes path provided to the markdown parser. Check the failing generate_guides target.',
|
|
);
|
|
}
|
|
|
|
const definedGuideRoutes = await readFile(definedRoutesAsStr, {encoding: 'utf8'});
|
|
definedRoutes = JSON.parse(definedGuideRoutes) as string[];
|
|
|
|
await Promise.all(
|
|
srcs.split(',').map(async (filePath) => {
|
|
if (!filePath.endsWith('.md')) {
|
|
throw new Error(`Input file "${filePath}" does not end in a ".md" file extension.`);
|
|
}
|
|
|
|
const markdownContent = await readFile(filePath, {encoding: 'utf8'});
|
|
|
|
// A leading byte order mark (U+FEFF) stops the first Markdown heading from being
|
|
// recognized, so the page title renders as a paragraph and loses its header. Fail
|
|
// the build so the BOM has to be removed from the source file instead.
|
|
if (markdownContent.charCodeAt(0) === 0xfeff) {
|
|
throw new Error(
|
|
`The file "${filePath}" starts with a byte order mark (BOM). Remove it so the leading heading is parsed correctly.`,
|
|
);
|
|
}
|
|
|
|
const htmlOutputContent = await parseMarkdownAsync(markdownContent, {
|
|
markdownFilePath: filePath,
|
|
apiEntries: mapManifestToEntries(apiManifest),
|
|
highlighter,
|
|
definedRoutes,
|
|
});
|
|
|
|
// The expected file name structure is the [name of the file].md.html.
|
|
const htmlFileName = filePath + '.html';
|
|
const htmlOutputPath = path.join(outputFilenameExecRootRelativePath, htmlFileName);
|
|
|
|
const unknownAnchor = hasUnknownAnchors(htmlOutputContent);
|
|
if (unknownAnchor) {
|
|
throw new Error(
|
|
`The file "${filePath}" contains an anchor link to "${unknownAnchor}" which does not exist in the document.`,
|
|
);
|
|
}
|
|
|
|
await writeFile(htmlOutputPath, htmlOutputContent, {encoding: 'utf8'});
|
|
}),
|
|
);
|
|
}
|
|
|
|
main();
|
|
|
|
function mapManifestToEntries(
|
|
apiManifest: ApiManifest,
|
|
): Record<string, {moduleName: string; targetSymbol?: string}> {
|
|
const duplicateEntries = new Set<string>();
|
|
|
|
const entryToModuleMap: Record<string, {moduleName: string; targetSymbol?: string}> = {};
|
|
for (const pkg of apiManifest) {
|
|
for (const entry of pkg.entries) {
|
|
if (duplicateEntries.has(entry.name)) {
|
|
continue;
|
|
} else if (entryToModuleMap[entry.name]) {
|
|
delete entryToModuleMap[entry.name];
|
|
duplicateEntries.add(entry.name);
|
|
} else {
|
|
const normalizedModuleName = pkg.moduleName.replace(/^@angular\//, '');
|
|
|
|
entryToModuleMap[entry.name] = {moduleName: normalizedModuleName};
|
|
|
|
// If there are aliases, create entries for each alias
|
|
if (entry.aliases) {
|
|
for (const alias of entry.aliases) {
|
|
entryToModuleMap[alias] = {
|
|
moduleName: normalizedModuleName,
|
|
targetSymbol: entry.name,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return entryToModuleMap;
|
|
}
|