mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
docs(docs-infra): share markdown rendering and highlighting code between api-gen and guides (#63357)
This reduces code duplication and improves the maintability. PR Close #63357
This commit is contained in:
committed by
Andrew Kushnir
parent
5a111064b8
commit
47dc4ffd1a
@@ -27,14 +27,12 @@ ts_project(
|
||||
),
|
||||
deps = [
|
||||
":entities",
|
||||
"//adev:node_modules/@types/node",
|
||||
"//adev:node_modules/html-entities",
|
||||
"//adev:node_modules/marked",
|
||||
"//adev:node_modules/preact",
|
||||
"//adev:node_modules/preact-render-to-string",
|
||||
"//adev:node_modules/prettier",
|
||||
"//adev:node_modules/shiki",
|
||||
"//adev/shared-docs/pipeline/shared",
|
||||
"//adev/shared-docs/pipeline/shared:shiki",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/regions",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ import path from 'path';
|
||||
import {CliCommand} from './cli-entities.mjs';
|
||||
import {DocEntry} from './entities.mjs';
|
||||
import {isCliEntry, isHiddenEntry} from './entities/categorization.mjs';
|
||||
import {configureMarkedGlobally} from './marked/configuration.mjs';
|
||||
import {getRenderable} from './processing.mjs';
|
||||
import {renderEntry} from './rendering.mjs';
|
||||
import {initHighlighter} from './shiki/shiki.mjs';
|
||||
import {setCurrentSymbol, setSymbols} from './symbol-context.mjs';
|
||||
import {CliCommandRenderable, DocEntryRenderable} from './entities/renderables.mjs';
|
||||
import {initHighlighter} from '../../shared/shiki.mjs';
|
||||
import {setHighlighterInstance} from './shiki/shiki.mjs';
|
||||
|
||||
/** The JSON data file format for extracted API reference info. */
|
||||
interface EntryCollection {
|
||||
@@ -25,7 +25,7 @@ interface EntryCollection {
|
||||
moduleLabel?: string;
|
||||
normalizedModuleName: string;
|
||||
entries: DocEntry[] | CliCommand[];
|
||||
symbols: Map<string, string>;
|
||||
symbols: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Parse all JSON data source files into an array of collections. */
|
||||
@@ -34,10 +34,15 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
const fileContent = readFileSync(jsonDataFilePath, {encoding: 'utf8'});
|
||||
const fileContentJson = JSON.parse(fileContent) as unknown;
|
||||
if ((fileContentJson as EntryCollection).entries) {
|
||||
return {
|
||||
...(fileContentJson as EntryCollection),
|
||||
symbols: new Map((fileContentJson as any).symbols ?? []),
|
||||
};
|
||||
const symbols = Object.fromEntries(
|
||||
// TODO: refactor that, it's dirty and we can probably do better than this.
|
||||
// We're removing the leading `@angular/` from module names.
|
||||
(((fileContentJson as any).symbols ?? []) as [string, string][]).map(
|
||||
([symbol, moduleName]) => [symbol, moduleName.slice(9)],
|
||||
),
|
||||
);
|
||||
|
||||
return {...(fileContentJson as EntryCollection), symbols};
|
||||
}
|
||||
|
||||
// CLI subcommands should generate a separate file for each subcommand.
|
||||
@@ -50,7 +55,7 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
moduleName: 'unknown',
|
||||
normalizedModuleName: 'unknown',
|
||||
entries: [fileContentJson as DocEntry],
|
||||
symbols: new Map(),
|
||||
symbols: {},
|
||||
},
|
||||
...command.subcommands!.map((subCommand) => {
|
||||
return {
|
||||
@@ -58,7 +63,7 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
moduleName: 'unknown',
|
||||
normalizedModuleName: 'unknown',
|
||||
entries: [{...subCommand, parentCommand: command} as any],
|
||||
symbols: new Map(),
|
||||
symbols: {},
|
||||
};
|
||||
}),
|
||||
];
|
||||
@@ -69,7 +74,7 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
moduleName: 'unknown',
|
||||
normalizedModuleName: 'unknown',
|
||||
entries: [fileContentJson as DocEntry], // TODO: fix the typing cli entries aren't DocEntry
|
||||
symbols: new Map(),
|
||||
symbols: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -90,10 +95,8 @@ function getNormalizedFilename(normalizedModuleName: string, entry: DocEntry | C
|
||||
}
|
||||
|
||||
async function main() {
|
||||
configureMarkedGlobally();
|
||||
|
||||
// Shiki highlighter needs to be setup in an async context
|
||||
await initHighlighter();
|
||||
setHighlighterInstance(await initHighlighter());
|
||||
|
||||
const [paramFilePath] = process.argv.slice(2);
|
||||
const rawParamLines = readFileSync(paramFilePath, {encoding: 'utf8'}).split('\n');
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* @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 {marked} from 'marked';
|
||||
import {renderer} from './renderer.mjs';
|
||||
|
||||
/** Globally configures marked for rendering JsDoc content to HTML. */
|
||||
export function configureMarkedGlobally() {
|
||||
marked.use({
|
||||
renderer,
|
||||
});
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/*!
|
||||
* @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 {Renderer, Tokens} from 'marked';
|
||||
import {codeToHtml} from '../shiki/shiki.mjs';
|
||||
import {SECTION_HEADING, SECTION_SUB_HEADING} from '../styling/css-classes.mjs';
|
||||
|
||||
/**
|
||||
* Custom renderer for marked that will be used to transform markdown files to HTML
|
||||
* files that can be used in the Angular docs.
|
||||
*/
|
||||
export const renderer: Partial<Renderer> = {
|
||||
code({lang, text}): string {
|
||||
const highlightResult = codeToHtml(text, lang)
|
||||
// remove spaces/line-breaks between elements to not mess-up `pre` style
|
||||
.replace(/>\s+</g, '><');
|
||||
|
||||
return `
|
||||
<div class="docs-code" role="group">
|
||||
${highlightResult}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
image({href, title, text}): string {
|
||||
return `
|
||||
<img src="${href}" alt="${text}" title="${title}" class="docs-image">
|
||||
`;
|
||||
},
|
||||
link(this: Renderer, {href, tokens}): string {
|
||||
return `<a href="${href}">${this.parser.parseInline(tokens)}</a>`;
|
||||
},
|
||||
list(this: Renderer, {items, ordered, start}) {
|
||||
if (ordered) {
|
||||
return `
|
||||
<ol class="docs-ordered-list">
|
||||
${items.map((item) => this.listitem(item)).join('')}
|
||||
</ol>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<ul class="docs-list">
|
||||
${items.map((item) => this.listitem(item)).join('')}
|
||||
</ul>
|
||||
`;
|
||||
},
|
||||
|
||||
table(this: Renderer, {header, rows}: Tokens.Table) {
|
||||
return `
|
||||
<div class="docs-table docs-scroll-track-transparent">
|
||||
<table>
|
||||
<thead>
|
||||
${this.tablerow({text: header.map((cell) => this.tablecell(cell)).join('')})}
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows
|
||||
.map((row) => this.tablerow({text: row.map((cell) => this.tablecell(cell)).join('')}))
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
heading(this: Renderer, {text, depth, tokens}: Tokens.Heading) {
|
||||
const id = text
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
|
||||
// Since we have a code transformer `addApiLinksToHtml` which adds anchors
|
||||
// to code blocks of known symbols, we add an additional `data-skip-anchor`
|
||||
// attribute that prevents the transformation. This is needed since nested
|
||||
// anchor tags are illegal and break the HTML.
|
||||
const textRenderer = new Renderer();
|
||||
textRenderer.codespan = ({text}) => `<code data-skip-anchor>${text}</code>`;
|
||||
const parsedText = this.parser.parseInline(tokens, textRenderer);
|
||||
|
||||
// The template matches templates/section-heading.tsx
|
||||
return `
|
||||
<h${depth} id="${id}" class="${SECTION_HEADING} ${SECTION_SUB_HEADING}">
|
||||
<a href="#${id}" aria-label="Link to ${text} section" tabIndex="-1">
|
||||
${parsedText}
|
||||
</a>
|
||||
</h${depth}>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -1,196 +0,0 @@
|
||||
/*!
|
||||
* @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 * as blockC from './region-matchers/block-c.mjs';
|
||||
import * as html from './region-matchers/html.mjs';
|
||||
import * as inlineC from './region-matchers/inline-c.mjs';
|
||||
import * as inlineCOnly from './region-matchers/inline-c-only.mjs';
|
||||
import * as inlineHash from './region-matchers/inline-hash.mjs';
|
||||
import {FileType, removeEslintComments} from './remove-eslint-comments.mjs';
|
||||
|
||||
const WHOLE_FILE_REGION_NAME = '';
|
||||
const DEFAULT_PLASTER = '. . .';
|
||||
const REGION_MATCHERS = {
|
||||
ts: inlineC,
|
||||
js: inlineC,
|
||||
mjs: inlineCOnly,
|
||||
es6: inlineC,
|
||||
html: html,
|
||||
svg: html,
|
||||
css: blockC,
|
||||
conf: inlineHash,
|
||||
yaml: inlineHash,
|
||||
yml: inlineHash,
|
||||
sh: inlineHash,
|
||||
jade: inlineCOnly,
|
||||
pug: inlineCOnly,
|
||||
json: inlineCOnly,
|
||||
'json.annotated': inlineCOnly,
|
||||
};
|
||||
|
||||
interface Region {
|
||||
lines: string[];
|
||||
open?: boolean;
|
||||
ranges: {from: number; to?: number}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: We assume that the tag defining the beginning and end of the region will be in different lines in each case.
|
||||
* For example, in HTML, we don't expect to have the following code on one line: <!-- docregion name -->content<!--enddocregion name-->
|
||||
*/
|
||||
export const regionParser = (contents: string, filePath: string) => {
|
||||
const fileType: FileType | undefined = filePath?.split('.').pop() as FileType;
|
||||
|
||||
if (!fileType) {
|
||||
throw new Error(`Incorrect file type for region parser: ${filePath}!`);
|
||||
}
|
||||
|
||||
const regionMatcher = REGION_MATCHERS[fileType];
|
||||
const openRegions: string[] = [];
|
||||
const regionMap: Record<string, Region> = {};
|
||||
|
||||
let countOfRegionLines = 0;
|
||||
|
||||
if (regionMatcher) {
|
||||
let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER);
|
||||
const lines = removeEslintComments(contents, fileType)
|
||||
.split(/\r?\n/)
|
||||
.filter((line, index) => {
|
||||
const startRegion = line.match(regionMatcher.regionStartMatcher);
|
||||
const endRegion = line.match(regionMatcher.regionEndMatcher);
|
||||
const updatePlaster = line.match(regionMatcher.plasterMatcher);
|
||||
|
||||
// start region processing
|
||||
if (startRegion) {
|
||||
// open up the specified region
|
||||
handleStartRegion(
|
||||
startRegion,
|
||||
regionMap,
|
||||
index,
|
||||
countOfRegionLines,
|
||||
plaster,
|
||||
openRegions,
|
||||
);
|
||||
} else if (endRegion) {
|
||||
// end region processing
|
||||
handleEndRegion(openRegions, endRegion, regionMap, index, countOfRegionLines);
|
||||
} else if (updatePlaster) {
|
||||
// doc plaster processing
|
||||
const plasterString = updatePlaster[1].trim();
|
||||
plaster = plasterString ? regionMatcher.createPlasterComment(plasterString) : '';
|
||||
} else {
|
||||
// simple line of content processing
|
||||
openRegions.forEach((regionName) => regionMap[regionName].lines.push(line));
|
||||
// do not filter out this line from the content
|
||||
return true;
|
||||
}
|
||||
|
||||
// this line contained an annotation so let's filter it out
|
||||
countOfRegionLines++;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!regionMap[WHOLE_FILE_REGION_NAME]) {
|
||||
regionMap[WHOLE_FILE_REGION_NAME] = {lines, ranges: [{from: 1, to: lines.length + 1}]};
|
||||
}
|
||||
|
||||
return {
|
||||
contents: lines.join('\n'),
|
||||
regionMap,
|
||||
totalLinesCount: lines.length,
|
||||
};
|
||||
} else {
|
||||
return {contents, regionMap, totalLinesCount: 0};
|
||||
}
|
||||
};
|
||||
|
||||
function handleStartRegion(
|
||||
startRegion: RegExpMatchArray,
|
||||
regionMap: Record<string, Region>,
|
||||
index: number,
|
||||
countOfRegionLines: number,
|
||||
plaster: string,
|
||||
openRegions: string[],
|
||||
) {
|
||||
const regionNames = getRegionNames(startRegion[1]);
|
||||
if (regionNames.length === 0) {
|
||||
regionNames.push(WHOLE_FILE_REGION_NAME);
|
||||
}
|
||||
|
||||
for (const regionName of regionNames) {
|
||||
const region = regionMap[regionName];
|
||||
if (region) {
|
||||
if (region.open) {
|
||||
throw new Error(`Tried to open a region, named "${regionName}", that is already open`);
|
||||
}
|
||||
|
||||
// Region is opened, set from range value.
|
||||
region.open = true;
|
||||
region.ranges.push({from: getFromRangeValue(index, countOfRegionLines)});
|
||||
|
||||
if (plaster) {
|
||||
// Use the same indent as the docregion marker
|
||||
const indent = startRegion[0].split(/[^ ]/, 1);
|
||||
region.lines.push(indent + plaster);
|
||||
}
|
||||
} else {
|
||||
regionMap[regionName] = {
|
||||
lines: [],
|
||||
open: true,
|
||||
ranges: [
|
||||
{
|
||||
from: getFromRangeValue(index, countOfRegionLines),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
openRegions.push(regionName);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEndRegion(
|
||||
openRegions: string[],
|
||||
endRegion: RegExpMatchArray,
|
||||
regionMap: Record<string, Region>,
|
||||
index: number,
|
||||
countOfRegionLines: number,
|
||||
) {
|
||||
if (openRegions.length === 0) {
|
||||
throw new Error('Tried to close a region when none are open');
|
||||
}
|
||||
// close down the specified region (or most recent if no name is given)
|
||||
const regionNames = getRegionNames(endRegion[1]);
|
||||
if (regionNames.length === 0) {
|
||||
regionNames.push(openRegions[openRegions.length - 1]);
|
||||
}
|
||||
|
||||
for (const regionName of regionNames) {
|
||||
const region = regionMap[regionName];
|
||||
if (!region || !region.open) {
|
||||
throw new Error(`Tried to close a region, named "${regionName}", that is not open`);
|
||||
}
|
||||
|
||||
// Region is closed, we can define the last line number of the region
|
||||
region.open = false;
|
||||
region.ranges[region.ranges.length - 1].to = index - countOfRegionLines;
|
||||
removeLast(openRegions, regionName);
|
||||
}
|
||||
}
|
||||
|
||||
function getFromRangeValue(index: number, countOfRegionLines: number): number {
|
||||
return index - countOfRegionLines + 1;
|
||||
}
|
||||
|
||||
function getRegionNames(input: string): string[] {
|
||||
return input.trim() === '' ? [] : input.split(',').map((name) => name.trim());
|
||||
}
|
||||
|
||||
function removeLast(array: string[], item: string): void {
|
||||
const index = array.lastIndexOf(item);
|
||||
array.splice(index, 1);
|
||||
}
|
||||
@@ -6,41 +6,24 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
let highlighter: any;
|
||||
import {HighlighterGeneric} from 'shiki';
|
||||
|
||||
export async function initHighlighter() {
|
||||
const {createHighlighter} = await import('shiki');
|
||||
highlighter = await createHighlighter({
|
||||
themes: ['github-light', 'github-dark'],
|
||||
langs: [
|
||||
'javascript',
|
||||
'typescript',
|
||||
'angular-html',
|
||||
'angular-ts',
|
||||
'shell',
|
||||
'html',
|
||||
'http',
|
||||
'json',
|
||||
'jsonc',
|
||||
'nginx',
|
||||
'markdown',
|
||||
'apache',
|
||||
],
|
||||
});
|
||||
/**
|
||||
* Singleton instance of the Shiki highlighter used during API page generation.
|
||||
*
|
||||
* TODO: Consider removing this singleton pattern if we find a better alternative.
|
||||
*/
|
||||
let highlighter: HighlighterGeneric<any, any>;
|
||||
export function setHighlighterInstance(instance: HighlighterGeneric<any, any>) {
|
||||
highlighter = instance;
|
||||
}
|
||||
|
||||
export function codeToHtml(code: string, language: string | undefined): string {
|
||||
const html = highlighter.codeToHtml(code, {
|
||||
lang: language ?? 'text',
|
||||
themes: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark',
|
||||
},
|
||||
cssVariablePrefix: '--shiki-',
|
||||
defaultColor: false,
|
||||
});
|
||||
|
||||
return html;
|
||||
export function getHighlighterInstance(): HighlighterGeneric<any, any> {
|
||||
if (!highlighter) {
|
||||
throw new Error(
|
||||
'Shiki highlighter has not been initialized. Make sure to call `setHighlighterInstance`.',
|
||||
);
|
||||
}
|
||||
return highlighter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,30 +6,17 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {getSymbolUrl as sharedGetSymbolUrl} from '../../shared/linking.mjs';
|
||||
|
||||
/**
|
||||
* API pages are generated each package at a time.
|
||||
* This allows to use a global context to store the symbols and their corresponding module names.
|
||||
* This allows us to use a global context to store the symbols and their corresponding module names.
|
||||
*/
|
||||
|
||||
let symbols = new Map<string, string>();
|
||||
let symbols: Record<string, string> = {};
|
||||
|
||||
// This is used to store the currently processed symbol (usually a class or an interface)
|
||||
let currentSymbol: string | undefined;
|
||||
|
||||
export function setSymbols(newSymbols: Map<string, string>): void {
|
||||
symbols = newSymbols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module name of a symbol.
|
||||
* eg: 'ApplicationRef' => 'core', 'FormControl' => 'forms'
|
||||
* Also supports class.member, 'NgZone.runOutsideAngular => 'core'
|
||||
*/
|
||||
export function getModuleName(symbol: string): string | undefined {
|
||||
const moduleName = symbols.get(symbol);
|
||||
return moduleName?.replace('@angular/', '');
|
||||
}
|
||||
|
||||
export function setCurrentSymbol(symbol: string): void {
|
||||
currentSymbol = symbol;
|
||||
}
|
||||
@@ -38,6 +25,14 @@ export function getCurrentSymbol(): string | undefined {
|
||||
return currentSymbol;
|
||||
}
|
||||
|
||||
export function setSymbols(newSymbols: Record<string, string>): void {
|
||||
symbols = newSymbols;
|
||||
}
|
||||
|
||||
export function getSymbolUrl(symbol: string): string | undefined {
|
||||
return sharedGetSymbolUrl(symbol, symbols);
|
||||
}
|
||||
|
||||
export function unknownSymbolMessage(link: string, symbol: string): string {
|
||||
return `WARNING: {@link ${link}} is invalid, ${symbol} or ${currentSymbol}.${symbol} is unknown in this context`;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ import {SectionDescription} from './section-description';
|
||||
import {SectionUsageNotes} from './section-usage-notes';
|
||||
import {SectionApi} from './section-api';
|
||||
import {SectionHeading} from './section-heading';
|
||||
import {codeToHtml} from '../shiki/shiki.mjs';
|
||||
import {RawHtml} from './raw-html';
|
||||
import {DeprecationWarning} from './deprecation-warning';
|
||||
import {codeToHtml} from '../../../shared/shiki.mjs';
|
||||
import {getHighlighterInstance} from '../shiki/shiki.mjs';
|
||||
|
||||
/** Component to render a class API reference document. */
|
||||
export function ClassReference(entry: ClassEntryRenderable | PipeEntryRenderable) {
|
||||
@@ -33,7 +34,13 @@ export function ClassReference(entry: ClassEntryRenderable | PipeEntryRenderable
|
||||
<>
|
||||
<div className={SECTION_CONTAINER + ' docs-reference-api-section'}>
|
||||
<SectionHeading name="Pipe usage" />
|
||||
<RawHtml value={codeToHtml((entry as PipeEntry).usage, 'angular-html')} />
|
||||
<RawHtml
|
||||
value={codeToHtml(
|
||||
getHighlighterInstance(),
|
||||
(entry as PipeEntry).usage,
|
||||
'angular-html',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
*/
|
||||
|
||||
import {h} from 'preact';
|
||||
import {getModuleName} from '../symbol-context.mjs';
|
||||
import {getLinkToModule} from '../transforms/url-transforms.mjs';
|
||||
import {shouldLinkSymbol} from '../../../shared/link-exemption.mjs';
|
||||
import {getSymbolUrl} from '../symbol-context.mjs';
|
||||
|
||||
const symbolRegex = /([a-zA-Z_$][a-zA-Z_$0-9\.]*)/;
|
||||
|
||||
@@ -24,11 +22,8 @@ export function CodeSymbol(props: {code: string}) {
|
||||
// Every even index is a non-match when the regex has 1 capturing group
|
||||
if (index % 2 === 0) return rawSymbol;
|
||||
|
||||
let [symbol, subSymbol] = rawSymbol.split('.'); // Also takes care of methods, enum value etc.
|
||||
const moduleName = getModuleName(symbol);
|
||||
|
||||
if (shouldLinkSymbol(symbol) && moduleName) {
|
||||
const url = getLinkToModule(moduleName, symbol, subSymbol);
|
||||
const url = getSymbolUrl(rawSymbol);
|
||||
if (url) {
|
||||
return <a href={url}>{rawSymbol}</a>;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
import {h} from 'preact';
|
||||
import {RawHtml} from './raw-html';
|
||||
import {codeToHtml} from '../shiki/shiki.mjs';
|
||||
import {codeToHtml} from '../../../shared/shiki.mjs';
|
||||
import {get} from 'http';
|
||||
import {getHighlighterInstance} from '../shiki/shiki.mjs';
|
||||
|
||||
/** Component to render a header of the CLI page. */
|
||||
export function HighlightTypeScript(props: {code: string}) {
|
||||
const result = codeToHtml(props.code, 'typescript');
|
||||
const result = codeToHtml(getHighlighterInstance(), props.code, 'typescript');
|
||||
const withScrollTrack = result.replace(/^(<pre class="shiki)/, '$1 docs-mini-scroll-track');
|
||||
|
||||
return <RawHtml value={withScrollTrack} className="docs-code" />;
|
||||
|
||||
@@ -9,18 +9,17 @@
|
||||
import {resolve} from 'path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {configureMarkedGlobally} from '../marked/configuration.mjs';
|
||||
import {getRenderable} from '../processing.mjs';
|
||||
import {renderEntry} from '../rendering.mjs';
|
||||
import {initHighlighter} from '../shiki/shiki.mjs';
|
||||
import {initHighlighter} from '../../../shared/shiki.mjs';
|
||||
import {setHighlighterInstance} from '../shiki/shiki.mjs';
|
||||
|
||||
describe('CLI docs to html', () => {
|
||||
let fragment: DocumentFragment;
|
||||
let entryJson: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await configureMarkedGlobally();
|
||||
setHighlighterInstance(await initHighlighter());
|
||||
|
||||
const entryContent = await readFile(resolve('./fake-cli-entries.json'), {
|
||||
encoding: 'utf-8',
|
||||
|
||||
@@ -1276,14 +1276,14 @@
|
||||
}
|
||||
],
|
||||
"symbols": [
|
||||
["AfterRenderPhase", "core"],
|
||||
["afterRender", "core"],
|
||||
["EmbeddedViewRef", "core"],
|
||||
["ChangeDetectionStrategy", "core"],
|
||||
["ChangeDetectorRef", "core"],
|
||||
["withNoHttpTransferCache", "platform-browser"],
|
||||
["withHttpTransferCacheOptions", "platform-browser"],
|
||||
["withI18nSupport", "platform-browser"],
|
||||
["withEventReplay", "platform-browser"]
|
||||
["AfterRenderPhase", "@angular/core"],
|
||||
["afterRender", "@angular/core"],
|
||||
["EmbeddedViewRef", "@angular/core"],
|
||||
["ChangeDetectionStrategy", "@angular/core"],
|
||||
["ChangeDetectorRef", "@angular/core"],
|
||||
["withNoHttpTransferCache", "@angular/platform-browser"],
|
||||
["withHttpTransferCacheOptions", "@angular/platform-browser"],
|
||||
["withI18nSupport", "@angular/platform-browser"],
|
||||
["withEventReplay", "@angular/platform-browser"]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {configureMarkedGlobally} from '../marked/configuration.mjs';
|
||||
import {getRenderable} from '../processing.mjs';
|
||||
import {renderEntry} from '../rendering.mjs';
|
||||
import {initHighlighter} from '../shiki/shiki.mjs';
|
||||
import {setSymbols} from '../symbol-context.mjs';
|
||||
import {resolve} from 'path';
|
||||
import {initHighlighter} from '../../../shared/shiki.mjs';
|
||||
import {setHighlighterInstance} from '../shiki/shiki.mjs';
|
||||
|
||||
// Note: The tests will probably break if the schema of the api extraction changes.
|
||||
// All entries in the fake-entries are extracted from Angular's api.
|
||||
@@ -24,14 +24,13 @@ describe('markdown to html', () => {
|
||||
const entries2 = new Map<string, string>();
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await configureMarkedGlobally();
|
||||
setHighlighterInstance(await initHighlighter());
|
||||
|
||||
const entryContent = await readFile(resolve('./fake-entries.json'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const entryJson = JSON.parse(entryContent) as any;
|
||||
const symbols = new Map<string, string>([
|
||||
const symbols = Object.fromEntries([
|
||||
['AfterRenderPhase', 'core'],
|
||||
['afterRender', 'core'],
|
||||
['EmbeddedViewRef', 'core'],
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
import {readFile} from 'fs/promises';
|
||||
import {DocEntryRenderable} from '../entities/renderables.mjs';
|
||||
import {configureMarkedGlobally} from '../marked/configuration.mjs';
|
||||
import {getRenderable} from '../processing.mjs';
|
||||
import {initHighlighter} from '../shiki/shiki.mjs';
|
||||
import {setSymbols} from '../symbol-context.mjs';
|
||||
import {resolve} from 'path';
|
||||
import {initHighlighter} from '../../../shared/shiki.mjs';
|
||||
import {setHighlighterInstance} from '../shiki/shiki.mjs';
|
||||
|
||||
// Note: The tests will probably break if the schema of the api extraction changes.
|
||||
// All entries in the fake-entries are extracted from Angular's api.
|
||||
@@ -22,14 +22,13 @@ describe('renderable', () => {
|
||||
const entries = new Map<string, DocEntryRenderable>();
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await configureMarkedGlobally();
|
||||
setHighlighterInstance(await initHighlighter());
|
||||
|
||||
const entryContent = await readFile(resolve('./fake-entries.json'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const entryJson = JSON.parse(entryContent) as any;
|
||||
const symbols = new Map<string, string>([
|
||||
const symbols = Object.fromEntries([
|
||||
['AfterRenderPhase', 'core'],
|
||||
['afterRender', 'core'],
|
||||
['EmbeddedViewRef', 'core'],
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ describe('jsdoc transforms', () => {
|
||||
it('should transform links', () => {
|
||||
setCurrentSymbol('Router');
|
||||
setSymbols(
|
||||
new Map([
|
||||
Object.fromEntries([
|
||||
['Route', 'test'],
|
||||
['Router', 'test'],
|
||||
['Router.someMethod', 'test'],
|
||||
|
||||
@@ -34,17 +34,16 @@ import {
|
||||
} from '../entities/categorization.mjs';
|
||||
import {CodeLineRenderable} from '../entities/renderables.mjs';
|
||||
import {HasModuleName, HasRenderableToc} from '../entities/traits.mjs';
|
||||
import {getModuleName} from '../symbol-context.mjs';
|
||||
import {getSymbolUrl} from '../symbol-context.mjs';
|
||||
import {
|
||||
codeToHtml,
|
||||
getHighlighterInstance,
|
||||
insertParenthesesForDecoratorInShikiHtml,
|
||||
replaceKeywordFromShikiHtml,
|
||||
} from '../shiki/shiki.mjs';
|
||||
|
||||
import {filterLifecycleMethods, mergeGettersAndSetters} from './member-transforms.mjs';
|
||||
import {getLinkToModule} from './url-transforms.mjs';
|
||||
import {formatJs} from './format-code.mjs';
|
||||
import {shouldLinkSymbol} from '../../../shared/link-exemption.mjs';
|
||||
import {codeToHtml} from '../../../shared/shiki.mjs';
|
||||
|
||||
const INDENT = ' ';
|
||||
const SPACE = ' ';
|
||||
@@ -86,7 +85,11 @@ export async function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
|
||||
formattedCode = await formatJs(metadata.contents);
|
||||
}
|
||||
|
||||
let codeWithSyntaxHighlighting = codeToHtml(formattedCode ?? metadata?.contents, 'typescript');
|
||||
let codeWithSyntaxHighlighting = codeToHtml(
|
||||
getHighlighterInstance(),
|
||||
formattedCode ?? metadata?.contents,
|
||||
'typescript',
|
||||
);
|
||||
|
||||
if (isDecoratorEntry(entry)) {
|
||||
// Shiki requires a keyword for correct formating of Decorators
|
||||
@@ -526,15 +529,9 @@ export function addApiLinksToHtml(htmlString: string): string {
|
||||
// The captured content ==> vvvvvvvv
|
||||
/(?<!<a[^>]*>)(<(?:(?:span)|(?:code))(?!\sdata-skip-anchor)[^>]*>\s*)([^<]*?)(\s*<\/(?:span|code)>)/g,
|
||||
(type: string, span1: string, potentialSymbolName: string, span2: string) => {
|
||||
// mySymbol() => mySymbol
|
||||
const symbolWithoutInvocation = potentialSymbolName.replace(/\([^)]*\);?/g, '');
|
||||
|
||||
const [symbol, subSymbol] = symbolWithoutInvocation.split(/(?:#|\.)/) as [string, string?];
|
||||
|
||||
const moduleName = getModuleName(symbol);
|
||||
|
||||
if (shouldLinkSymbol(symbol) && moduleName) {
|
||||
return `${span1}<a href="${getLinkToModule(moduleName, symbol, subSymbol)}">${potentialSymbolName}</a>${span2}`;
|
||||
const url = getSymbolUrl(potentialSymbolName);
|
||||
if (url) {
|
||||
return `${span1}<a href="${url}">${potentialSymbolName}</a>${span2}`;
|
||||
}
|
||||
|
||||
return type;
|
||||
|
||||
@@ -25,9 +25,8 @@ import {
|
||||
HasStableFlag,
|
||||
} from '../entities/traits.mjs';
|
||||
|
||||
import {getLinkToModule} from './url-transforms.mjs';
|
||||
import {addApiLinksToHtml} from './code-transforms.mjs';
|
||||
import {getCurrentSymbol, getModuleName, unknownSymbolMessage} from '../symbol-context.mjs';
|
||||
import {getCurrentSymbol, getSymbolUrl, unknownSymbolMessage} from '../symbol-context.mjs';
|
||||
|
||||
const JS_DOC_USAGE_NOTE_TAGS: Set<string> = new Set(['remarks', 'usageNotes', 'example']);
|
||||
export const JS_DOC_SEE_TAG = 'see';
|
||||
@@ -141,7 +140,11 @@ function getHtmlAdditionalLinks<T extends HasJsDocTags>(entry: T): LinkEntryRend
|
||||
|
||||
if (linkMatch) {
|
||||
const link = linkMatch[1];
|
||||
const {url, label} = parseAtLink(link);
|
||||
const parsed = parseAtLink(link);
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
const {url, label} = parsed;
|
||||
return {label, url};
|
||||
}
|
||||
|
||||
@@ -168,13 +171,17 @@ function wrapExampleHtmlElementsWithCode(text: string) {
|
||||
*/
|
||||
function convertLinks(text: string) {
|
||||
return text.replace(jsDoclinkRegexGlobal, (_, link) => {
|
||||
const {label, url} = parseAtLink(link);
|
||||
const parsed = parseAtLink(link);
|
||||
if (!parsed) {
|
||||
return `<code>${link}</code>`; // leave the link as-is if we can't parse it
|
||||
}
|
||||
const {label, url} = parsed;
|
||||
|
||||
return `<a href="${url}"><code>${label}</code></a>`;
|
||||
});
|
||||
}
|
||||
|
||||
function parseAtLink(link: string): {label: string; url: string} {
|
||||
function parseAtLink(link: string): {label: string; url: string} | undefined {
|
||||
// Because of microsoft/TypeScript/issues/59679
|
||||
// getTextOfJSDocComment introduces an extra space between the symbol and a trailing ()
|
||||
link = link.replace(/ \(\)$/, '');
|
||||
@@ -195,24 +202,18 @@ function parseAtLink(link: string): {label: string; url: string} {
|
||||
};
|
||||
}
|
||||
|
||||
let [symbol, subSymbol] = rawSymbol.replace(/\(\)$/, '').split(/(?:#|\.)/);
|
||||
|
||||
let moduleName = getModuleName(symbol);
|
||||
let url = getSymbolUrl(rawSymbol);
|
||||
const label = description ?? rawSymbol;
|
||||
|
||||
const currentSymbol = getCurrentSymbol();
|
||||
if (!url) {
|
||||
const currentSymbol = getCurrentSymbol();
|
||||
// 2nd attempt, try to get the module name in the context of the current symbol
|
||||
url = getSymbolUrl(`${currentSymbol}.${rawSymbol}`);
|
||||
|
||||
if (!moduleName) {
|
||||
// 2nd attemp, try to get the module name in the context of the current symbol
|
||||
moduleName = getModuleName(`${currentSymbol}.${symbol}`);
|
||||
|
||||
if (!moduleName || !currentSymbol) {
|
||||
throw unknownSymbolMessage(link, symbol);
|
||||
if (!url || !currentSymbol) {
|
||||
throw unknownSymbolMessage(link, rawSymbol);
|
||||
}
|
||||
|
||||
subSymbol = symbol;
|
||||
symbol = currentSymbol;
|
||||
}
|
||||
|
||||
return {label, url: getLinkToModule(moduleName, symbol, subSymbol)};
|
||||
return {label, url};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
export const API_PREFIX = 'api';
|
||||
export const MODULE_NAME_PREFIX = '@angular/';
|
||||
|
||||
export function getLinkToModule(moduleName: string, symbol: string, subSymbol?: string) {
|
||||
return `/${API_PREFIX}/${moduleName}/${symbol}${subSymbol ? `#${subSymbol}` : ''}`;
|
||||
|
||||
@@ -18,7 +18,7 @@ ts_project(
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev:node_modules/tinyglobby",
|
||||
"//adev/shared-docs/pipeline/examples/shared",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/regions",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import {readFile} from 'fs/promises';
|
||||
import {copyFolder, createFolder, removeFolder} from '../shared/file-system.mjs';
|
||||
import jsdom from 'jsdom';
|
||||
import {glob} from 'tinyglobby';
|
||||
import {regionParser} from '../../guides/extensions/docs-code/regions/region-parser.mjs';
|
||||
import {appendCopyrightToFile} from '../shared/copyright.mjs';
|
||||
import {FileType} from '../../guides/extensions/docs-code/sanitizers/eslint.mjs';
|
||||
import {EXCLUDE_FILES_FOR_STACKBLITZ, STACKBLITZ_CONFIG_FILENAME} from './defaults.mjs';
|
||||
import {regionParser} from '../../shared/regions/region-parser.mjs';
|
||||
import {FileType} from '../../shared/regions/remove-eslint-comments.mjs';
|
||||
|
||||
interface StackblitzConfig {
|
||||
title: string;
|
||||
|
||||
@@ -18,7 +18,7 @@ ts_project(
|
||||
"//adev:node_modules/tinyglobby",
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/pipeline/examples/shared",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/regions",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import {join} from 'path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {copyFolder, createFolder} from '../shared/file-system.mjs';
|
||||
import {glob} from 'tinyglobby';
|
||||
import {regionParser} from '../../guides/extensions/docs-code/regions/region-parser.mjs';
|
||||
import {appendCopyrightToFile} from '../shared/copyright.mjs';
|
||||
import {FileType} from '../../guides/extensions/docs-code/sanitizers/eslint.mjs';
|
||||
import {EXCLUDE_FILES, CONFIG_FILENAME} from './defaults.mjs';
|
||||
import {zip, strToU8} from 'fflate';
|
||||
|
||||
import {FileAndContent} from '../../../interfaces';
|
||||
import {FileType} from '../../shared/regions/remove-eslint-comments.mjs';
|
||||
import {regionParser} from '../../shared/regions/region-parser.mjs';
|
||||
|
||||
interface ZipConfig {
|
||||
ignore: string[];
|
||||
|
||||
@@ -53,7 +53,6 @@ ts_project(
|
||||
srcs = glob(
|
||||
[
|
||||
"**/*.mts",
|
||||
"shiki.d.ts",
|
||||
],
|
||||
exclude = ["index.mts"],
|
||||
),
|
||||
@@ -61,17 +60,8 @@ ts_project(
|
||||
"//adev/shared-docs:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/@types/node",
|
||||
"//adev:node_modules/diff",
|
||||
"//adev:node_modules/emoji-regex",
|
||||
"//adev:node_modules/html-entities",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev:node_modules/marked",
|
||||
"//adev:node_modules/mermaid",
|
||||
"//adev:node_modules/playwright-core",
|
||||
"//adev:node_modules/shiki",
|
||||
"//adev/shared-docs/pipeline/shared",
|
||||
"//adev/shared-docs/pipeline/shared:shiki",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// These kind of comments are used CSS and other languages that do not support inline comments
|
||||
export const regionStartMatcher = /^\s*\/\*\s*#docregion\s*(.*)\s*\*\/\s*$/;
|
||||
export const regionEndMatcher = /^\s*\/\*\s*#enddocregion\s*(.*)\s*\*\/\s*$/;
|
||||
export const plasterMatcher = /^\s*\/\*\s*#docplaster\s*(.*)\s*\*\/\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `/* ${plaster} */`;
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// These kind of comments are used in HTML
|
||||
|
||||
export const regionStartMatcher = /^\s*<!--\s*#docregion\s*(.*?)\s*(?:-->)?\s*$/;
|
||||
export const regionEndMatcher = /^\s*<!--\s*#enddocregion\s*(.*?)\s*-->\s*$/;
|
||||
export const plasterMatcher = /^\s*<!--\s*#docplaster\s*(.*?)\s*-->\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `<!-- ${plaster} -->`;
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// These kind of comments are used in languages that do not support block comments, such as Jade
|
||||
export const regionStartMatcher = /^\s*\/\/\s*#docregion\s*(.*)\s*$/;
|
||||
export const regionEndMatcher = /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/;
|
||||
export const plasterMatcher = /^\s*\/\/\s*#docplaster\s*(.*)\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `// ${plaster}`;
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// This comment type is used in C like languages such as JS, TS, etc
|
||||
export const regionStartMatcher = /^\s*\/\/\s*#docregion\s*(.*)\s*$/;
|
||||
export const regionEndMatcher = /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/;
|
||||
export const plasterMatcher = /^\s*\/\/\s*#docplaster\s*(.*)\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `/* ${plaster} */`;
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// These type of comments are used in hash comment based languages such as bash and Yaml
|
||||
export const regionStartMatcher = /^\s*#\s*#docregion\s*(.*)\s*$/;
|
||||
export const regionEndMatcher = /^\s*#\s*#enddocregion\s*(.*)\s*$/;
|
||||
export const plasterMatcher = /^\s*#\s*#docplaster\s*(.*)\s*$/;
|
||||
export const createPlasterComment = (plaster: string) => `# ${plaster}`;
|
||||
@@ -1,61 +0,0 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
export type FileType =
|
||||
| 'ts'
|
||||
| 'js'
|
||||
| 'mjs'
|
||||
| 'es6'
|
||||
| 'html'
|
||||
| 'svg'
|
||||
| 'css'
|
||||
| 'conf'
|
||||
| 'yaml'
|
||||
| 'yml'
|
||||
| 'sh'
|
||||
| 'jade'
|
||||
| 'pug'
|
||||
| 'json'
|
||||
| 'json.annotated';
|
||||
|
||||
/**
|
||||
* Remove any ESLint comments from the provided code string. This is particularly useful as we may
|
||||
* want to store code formatted for an example that would not be allowed in our repo. For instance,
|
||||
* we can disable ESLint to allow for a `console` usage, despite not allowing it typically, so that
|
||||
* it can be used in an example in documenation.
|
||||
*/
|
||||
export function removeEslintComments(input: string, fileType: FileType) {
|
||||
if (!input || (fileType !== 'ts' && fileType !== 'js' && fileType !== 'html')) {
|
||||
return input;
|
||||
}
|
||||
return input.replace(regexesForFileTypes[fileType], '');
|
||||
}
|
||||
|
||||
const jsRegexes = [
|
||||
/\/\/ *eslint-disable(?:-next-line)?(?: .*)?(?:\n *|$)/,
|
||||
/\n? *\/\/ *eslint-(?:disable-line|enable)(?: .*)?(?=\n|$)/,
|
||||
/\/\*\s*eslint-disable(?:-next-line)?(?: [\s\S]*?)?\*\/ *(?:\n *)?/,
|
||||
/\n? *\/\*\s*eslint-(?:disable-line|enable)(?: [\s\S]*?)?\*\//,
|
||||
];
|
||||
|
||||
const htmlRegexes = [
|
||||
/<!--\s*eslint-disable(?:-next-line)?(?: [\s\S]*?)?--> *(?:\n *)?/,
|
||||
/\n? *<!--\s*eslint-(?:disable-line|enable)(?: [\s\S]*?)?-->/,
|
||||
];
|
||||
|
||||
const joinRegexes = (regexes: any) =>
|
||||
new RegExp(regexes.map((regex: any) => `(?:${regex.source})`).join('|'), 'g');
|
||||
const htmlRegex = joinRegexes(htmlRegexes);
|
||||
// Note: the js regex needs to also include the html ones to account for inline templates in @Components
|
||||
const jsRegex = joinRegexes([...jsRegexes, ...htmlRegexes]);
|
||||
|
||||
const regexesForFileTypes = {
|
||||
js: jsRegex,
|
||||
ts: jsRegex,
|
||||
html: htmlRegex,
|
||||
};
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
import {readFileSync, writeFileSync} from 'fs';
|
||||
import path from 'path';
|
||||
import {parseMarkdown} from './parse.mjs';
|
||||
import {initHighlighter} from './extensions/docs-code/format/highlight.mjs';
|
||||
import {initHighlighter} from '../shared/shiki.mjs';
|
||||
import {parseMarkdownAsync} from '../shared/marked/parse.mjs';
|
||||
|
||||
type ApiManifest = ApiManifestPackage[];
|
||||
interface ApiManifestPackage {
|
||||
@@ -24,7 +24,7 @@ async function main() {
|
||||
|
||||
// The highlighter needs to be setup asynchronously
|
||||
// so we're doing it at the start of the pipeline
|
||||
await initHighlighter();
|
||||
const highlighter = await initHighlighter();
|
||||
|
||||
for (const filePath of srcs.split(',')) {
|
||||
if (!filePath.endsWith('.md')) {
|
||||
@@ -42,9 +42,10 @@ async function main() {
|
||||
}
|
||||
|
||||
const markdownContent = readFileSync(filePath, {encoding: 'utf8'});
|
||||
const htmlOutputContent = await parseMarkdown(markdownContent, {
|
||||
const htmlOutputContent = await parseMarkdownAsync(markdownContent, {
|
||||
markdownFilePath: filePath,
|
||||
apiEntries: mapManifestToEntries(apiManifest),
|
||||
highlighter,
|
||||
});
|
||||
|
||||
// The expected file name structure is the [name of the file].md.html.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*!
|
||||
* @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 {marked} from 'marked';
|
||||
import {hooks} from './hooks.mjs';
|
||||
import {Renderer} from './renderer.mjs';
|
||||
import {docsAlertExtension} from './extensions/docs-alert.mjs';
|
||||
import {docsCalloutExtension} from './extensions/docs-callout.mjs';
|
||||
import {docsPillExtension} from './extensions/docs-pill/docs-pill.mjs';
|
||||
import {docsPillRowExtension} from './extensions/docs-pill/docs-pill-row.mjs';
|
||||
import {docsVideoExtension} from './extensions/docs-video.mjs';
|
||||
import {docsWorkflowExtension} from './extensions/docs-workflow/docs-workflow.mjs';
|
||||
import {docsStepExtension} from './extensions/docs-workflow/docs-step.mjs';
|
||||
import {docsCardExtension} from './extensions/docs-card/docs-card.mjs';
|
||||
import {docsCardContainerExtension} from './extensions/docs-card/docs-card-container.mjs';
|
||||
import {docsDecorativeHeaderExtension} from './extensions/docs-decorative-header.mjs';
|
||||
import {docsCodeBlockExtension} from './extensions/docs-code/docs-code-block.mjs';
|
||||
import {docsCodeExtension} from './extensions/docs-code/docs-code.mjs';
|
||||
import {docsCodeMultifileExtension} from './extensions/docs-code/docs-code-multifile.mjs';
|
||||
import {ParserContext, setContext} from './utils.mjs';
|
||||
import {walkTokens} from './walk-tokens.mjs';
|
||||
|
||||
export async function parseMarkdown(
|
||||
markdownContent: string,
|
||||
context: ParserContext,
|
||||
): Promise<string> {
|
||||
setContext(context);
|
||||
|
||||
marked.use({
|
||||
hooks,
|
||||
extensions: [
|
||||
docsAlertExtension,
|
||||
docsCalloutExtension,
|
||||
docsPillExtension,
|
||||
docsPillRowExtension,
|
||||
docsVideoExtension,
|
||||
docsWorkflowExtension,
|
||||
docsStepExtension,
|
||||
docsCardExtension,
|
||||
docsCardContainerExtension,
|
||||
docsDecorativeHeaderExtension,
|
||||
docsCodeBlockExtension,
|
||||
docsCodeExtension,
|
||||
docsCodeMultifileExtension,
|
||||
],
|
||||
walkTokens,
|
||||
// The async option causes marked to await walkTokens functions before parsing the tokens and returning an HTML string.
|
||||
// We leverage this to allow us to use async libraries like mermaid and building stackblitz examples.
|
||||
async: true,
|
||||
});
|
||||
|
||||
return marked.parse(markdownContent, {renderer: new Renderer()});
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* @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 {Renderer as _Renderer} from 'marked';
|
||||
import {linkRender} from './tranformations/link.mjs';
|
||||
import {tableRender} from './tranformations/table.mjs';
|
||||
import {listRender} from './tranformations/list.mjs';
|
||||
import {imageRender} from './tranformations/image.mjs';
|
||||
import {textRender} from './tranformations/text.mjs';
|
||||
import {headingRender} from './tranformations/heading.mjs';
|
||||
import {codespanRender} from './tranformations/code.mjs';
|
||||
|
||||
/**
|
||||
* Custom renderer for marked that will be used to transform markdown files to HTML
|
||||
* files that can be used in the Angular docs.
|
||||
*/
|
||||
export class Renderer extends _Renderer {
|
||||
defaultRenderer = new _Renderer();
|
||||
|
||||
override link = linkRender;
|
||||
override table = tableRender;
|
||||
override list = listRender;
|
||||
override image = imageRender;
|
||||
override text = textRender;
|
||||
override heading = headingRender;
|
||||
override codespan = codespanRender;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
load("//adev/shared-docs:defaults.bzl", "ts_project")
|
||||
|
||||
ts_project(
|
||||
name = "parser_context",
|
||||
srcs = glob(["*.mts"]),
|
||||
visibility = ["//adev/shared-docs/pipeline/guides/test:__subpackages__"],
|
||||
deps = ["//adev/shared-docs/pipeline/guides:guides_lib"],
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* @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 {ParserContext} from '../utils.mjs';
|
||||
|
||||
export const parserContext: ParserContext = {
|
||||
apiEntries: {
|
||||
CommonModule: 'angular/common',
|
||||
bootstrapApplication: '@angular/platform-browser',
|
||||
ApplicationRef: 'angular/core',
|
||||
Router: 'angular/router',
|
||||
},
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
/*!
|
||||
* @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 {existsSync, readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {cwd} from 'process';
|
||||
import {shouldLinkSymbol} from '../shared/link-exemption.mjs';
|
||||
|
||||
// TODO(josephperrott): Set edit content url based on the owner, repo and branch.
|
||||
|
||||
/** The base url for edting the a file in the repository. */
|
||||
const GITHUB_EDIT_CONTENT_URL = 'https://github.com/angular/angular/edit/main';
|
||||
|
||||
/** Get the page title with edit button to modify the page source. */
|
||||
export function getPageTitle(text: string): string {
|
||||
return `
|
||||
<!-- Page title -->
|
||||
<div class="docs-page-title">
|
||||
<h1 tabindex="-1">${text}</h1>
|
||||
<a class="docs-github-links" target="_blank" href="${GITHUB_EDIT_CONTENT_URL}/${context?.markdownFilePath}" title="Edit this page" aria-label="Edit this page">
|
||||
<!-- Pencil -->
|
||||
<docs-icon role="presentation">edit</docs-icon>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** Configuration using environment for parser, providing context. */
|
||||
export interface ParserContext {
|
||||
markdownFilePath?: string;
|
||||
apiEntries?: Record<string, string>;
|
||||
}
|
||||
|
||||
let context: ParserContext = {};
|
||||
export function setContext(envContext: Partial<ParserContext>) {
|
||||
context = envContext;
|
||||
}
|
||||
|
||||
export function getApiLink(entryName: string): string | undefined {
|
||||
if (!context.apiEntries || hasMoreThanOneDot(entryName) || !shouldLinkSymbol(entryName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let propName: string | undefined;
|
||||
// we want to match functions when they have parentheses
|
||||
if (entryName.endsWith('()')) {
|
||||
entryName = entryName.slice(0, -2);
|
||||
}
|
||||
|
||||
if (entryName.startsWith('@')) {
|
||||
entryName = entryName.slice(1);
|
||||
}
|
||||
if (entryName.includes('.')) {
|
||||
[entryName, propName] = entryName.split('.');
|
||||
}
|
||||
|
||||
// We don't want to match entries like "constructor"
|
||||
const apiEntry = Object.hasOwn(context.apiEntries, entryName) && context.apiEntries[entryName];
|
||||
|
||||
return apiEntry ? `/api/${apiEntry}/${entryName}${propName ? `#${propName}` : ''}` : undefined;
|
||||
}
|
||||
|
||||
/** The base directory of the workspace the script is running in. */
|
||||
const WORKSPACE_DIR = cwd();
|
||||
|
||||
export function loadWorkspaceRelativeFile(filePath: string): string {
|
||||
const fullFilePath = join(WORKSPACE_DIR, filePath);
|
||||
if (!existsSync(fullFilePath)) {
|
||||
throw Error(`Cannot find: ${filePath}`);
|
||||
}
|
||||
return readFileSync(fullFilePath, {encoding: 'utf-8'});
|
||||
}
|
||||
|
||||
function hasMoreThanOneDot(str: string) {
|
||||
return str.split('.').length - 1 > 1;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* @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 {Token} from 'marked';
|
||||
import {DocsCodeToken} from './extensions/docs-code/docs-code.mjs';
|
||||
|
||||
/**
|
||||
* Describe a HANDLE_MERMAID value which esbuild will use at build time to determine if the mermaid
|
||||
* related code should be included in the bundle.
|
||||
* THIS VALUE IS NOT AVAILABLE AT RUNTIME.
|
||||
*/
|
||||
export declare const HANDLE_MERMAID: boolean;
|
||||
|
||||
/** Type guard for if a provided token is the DocsCodeToken. */
|
||||
function isDocsCodeToken(token: Token): token is DocsCodeToken {
|
||||
return !!(token as DocsCodeToken).language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the provided token based on the token itself replacing its content/data in place
|
||||
* as appropriate.
|
||||
*/
|
||||
let mermaid: typeof import('./mermaid/index.mjs');
|
||||
export async function walkTokens(token: Token): Promise<void> {
|
||||
if (!isDocsCodeToken(token) || token.language !== 'mermaid') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (HANDLE_MERMAID) {
|
||||
mermaid ??= await import('./mermaid/index.mjs');
|
||||
return mermaid.processMermaidCodeBlock(token);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,20 @@ load("//adev/shared-docs:defaults.bzl", "ts_project")
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_project(
|
||||
name = "shared",
|
||||
srcs = glob(
|
||||
[
|
||||
"*.mts",
|
||||
],
|
||||
),
|
||||
name = "linking",
|
||||
srcs = ["linking.mts"],
|
||||
visibility = [
|
||||
"//adev/shared-docs:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/pipeline:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "shiki",
|
||||
srcs = ["shiki.mts"],
|
||||
visibility = [
|
||||
"//adev/shared-docs/pipeline:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//adev:node_modules/shiki",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// In some case we know that we don't want to link a symbol
|
||||
// Example when there is a conflict between API entries and compiler features.
|
||||
// eg: "animate" is both an Animation API entry and an template instruction "animation.enter"
|
||||
|
||||
// TODO: We should have more shared logic for linking between API references & Guides
|
||||
|
||||
const LINK_EXEMPT = new Set(['animate', 'animate.enter', 'animate.leave']);
|
||||
|
||||
export function shouldLinkSymbol(symbol: string): boolean {
|
||||
return !LINK_EXEMPT.has(symbol);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
// In some case we know that we don't want to link a symbol
|
||||
// Example when there is a conflict between API entries and compiler features.
|
||||
// eg: "animate" is both an Animation API entry and an template instruction "animation.enter"
|
||||
const LINK_EXEMPT = new Set(['animate', 'animate.enter', 'animate.leave']);
|
||||
|
||||
export function shouldLinkSymbol(symbol: string): boolean {
|
||||
return !LINK_EXEMPT.has(symbol);
|
||||
}
|
||||
|
||||
export type ApiEntries = Record<string, string>; // symbolName -> moduleName (without @angular/ prefix)
|
||||
|
||||
/**
|
||||
* Extracts the symbol name and property name from a symbol string.
|
||||
* eg:
|
||||
* foobar() => {symbolName: 'foobar', propName: null}
|
||||
* ApplicationRef.tick = > {symbolName: 'ApplicationRef', propName: 'tick'}
|
||||
* ApplicationRef.tick() = > {symbolName: 'ApplicationRef', propName: 'tick'}
|
||||
* @Component => {symbolName: 'Component', propName: null}
|
||||
*/
|
||||
export function extractFromSymbol(symbol: string): {propName: string | null; symbolName: string} {
|
||||
let propName: string | undefined;
|
||||
let symbolName = symbol;
|
||||
// we want to match functions when they have parentheses
|
||||
if (symbolName.endsWith('()')) {
|
||||
symbolName = symbolName.slice(0, -2);
|
||||
}
|
||||
|
||||
if (symbolName.startsWith('@')) {
|
||||
symbolName = symbolName.slice(1);
|
||||
}
|
||||
if (symbolName.includes('#')) {
|
||||
[symbolName, propName] = symbolName.split('#');
|
||||
} else if (symbolName.includes('.')) {
|
||||
[symbolName, propName] = symbolName.split('.');
|
||||
}
|
||||
|
||||
return {propName: propName ?? null, symbolName: symbolName};
|
||||
}
|
||||
|
||||
export function getSymbolUrl(symbol: string, apiEntries: ApiEntries): string | undefined {
|
||||
if (hasMoreThanOneDot(symbol) || !shouldLinkSymbol(symbol)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {symbolName, propName} = extractFromSymbol(symbol);
|
||||
// We don't want to match entries like "constructor"
|
||||
const apiEntry = Object.hasOwn(apiEntries, symbolName) && apiEntries[symbolName];
|
||||
|
||||
return apiEntry ? `/api/${apiEntry}/${symbolName}${propName ? `#${propName}` : ''}` : undefined;
|
||||
}
|
||||
|
||||
function hasMoreThanOneDot(str: string) {
|
||||
return str.split('.').length - 1 > 1;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
load("//adev/shared-docs:defaults.bzl", "ts_project")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_project(
|
||||
name = "marked",
|
||||
srcs = glob(["**/*.mts"]),
|
||||
visibility = [
|
||||
"//adev/shared-docs/pipeline:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/@types/node",
|
||||
"//adev:node_modules/diff",
|
||||
"//adev:node_modules/emoji-regex",
|
||||
"//adev:node_modules/html-entities",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev:node_modules/marked",
|
||||
"//adev:node_modules/mermaid",
|
||||
"//adev:node_modules/playwright-core",
|
||||
"//adev/shared-docs/pipeline/shared:linking",
|
||||
"//adev/shared-docs/pipeline/shared:shiki",
|
||||
"//adev/shared-docs/pipeline/shared/regions",
|
||||
],
|
||||
)
|
||||
+2
-3
@@ -7,9 +7,8 @@
|
||||
*/
|
||||
|
||||
import {Tokens, Token, RendererThis, TokenizerThis} from 'marked';
|
||||
import {anchorTarget} from '../../helpers.mjs';
|
||||
import {loadWorkspaceRelativeFile} from '../../utils.mjs';
|
||||
import {setInsideLink} from '../../tranformations/link.mjs';
|
||||
import {loadWorkspaceRelativeFile, anchorTarget} from '../../helpers.mjs';
|
||||
import {setInsideLink} from '../../transformations/link.mjs';
|
||||
|
||||
interface DocsCardToken extends Tokens.Generic {
|
||||
type: 'docs-card';
|
||||
+2
-1
@@ -8,6 +8,7 @@
|
||||
|
||||
import {TokenizerThis, RendererThis} from 'marked';
|
||||
import {CodeToken, formatCode} from './format/index.mjs';
|
||||
import {AdevDocsRenderer} from '../../renderer.mjs';
|
||||
|
||||
export interface DocsCodeBlock extends CodeToken {
|
||||
type: 'docs-code-block';
|
||||
@@ -48,6 +49,6 @@ export const docsCodeBlockExtension = {
|
||||
if (token.language === 'mermaid') {
|
||||
return token.code;
|
||||
}
|
||||
return formatCode(token);
|
||||
return formatCode(token, (this.parser.renderer as AdevDocsRenderer).context);
|
||||
},
|
||||
};
|
||||
+4
-3
@@ -8,8 +8,9 @@
|
||||
|
||||
import {TokenizerThis, RendererThis} from 'marked';
|
||||
import {CodeToken, formatCode} from './format/index.mjs';
|
||||
import {FileType, removeEslintComments} from './sanitizers/eslint.mjs';
|
||||
import {loadWorkspaceRelativeFile} from '../../utils.mjs';
|
||||
import {loadWorkspaceRelativeFile} from '../../helpers.mjs';
|
||||
import {AdevDocsRenderer} from '../../renderer.mjs';
|
||||
import {FileType, removeEslintComments} from '../../../regions/remove-eslint-comments.mjs';
|
||||
|
||||
/** Marked token for a custom docs element. */
|
||||
export interface DocsCodeToken extends CodeToken {
|
||||
@@ -82,6 +83,6 @@ export const docsCodeExtension = {
|
||||
return undefined;
|
||||
},
|
||||
renderer(this: RendererThis, token: DocsCodeToken) {
|
||||
return formatCode(token);
|
||||
return formatCode(token, (this.parser.renderer as AdevDocsRenderer).context);
|
||||
},
|
||||
};
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
import {diffLines, Change as DiffChange} from 'diff';
|
||||
import {CodeToken} from './index.mjs';
|
||||
import {loadWorkspaceRelativeFile} from '../../../utils.mjs';
|
||||
import {loadWorkspaceRelativeFile} from '../../../helpers.mjs';
|
||||
|
||||
export interface DiffMetadata {
|
||||
code: string;
|
||||
+7
-42
@@ -10,61 +10,26 @@ import {decode} from 'html-entities';
|
||||
import {CodeToken} from './index.mjs';
|
||||
import {expandRangeStringValues} from './range.mjs';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {createHighlighter, HighlighterGeneric} from 'shiki';
|
||||
import {HighlighterGeneric} from 'shiki';
|
||||
import {codeToHtml} from '../../../../shiki.mjs';
|
||||
|
||||
const lineNumberClassName: string = 'shiki-ln-number';
|
||||
const lineAddedClassName: string = 'add';
|
||||
const lineRemovedClassName: string = 'remove';
|
||||
const lineHighlightedClassName: string = 'highlighted';
|
||||
|
||||
let highlighter: HighlighterGeneric<any, any>;
|
||||
|
||||
/**
|
||||
* Highlighter needs to setup asynchronously
|
||||
*
|
||||
* This is intended to be invoked at the start of the pipeline
|
||||
*/
|
||||
export async function initHighlighter() {
|
||||
highlighter = await createHighlighter({
|
||||
themes: ['github-light', 'github-dark'],
|
||||
langs: [
|
||||
'javascript',
|
||||
'typescript',
|
||||
'angular-html',
|
||||
'angular-ts',
|
||||
'shell',
|
||||
'html',
|
||||
'http',
|
||||
'json',
|
||||
'nginx',
|
||||
'markdown',
|
||||
'apache',
|
||||
'css',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the provided token's code value to include syntax highlighting.
|
||||
*/
|
||||
export function highlightCode(token: CodeToken) {
|
||||
export function highlightCode(highlighter: HighlighterGeneric<any, any>, token: CodeToken) {
|
||||
// TODO(josephperrott): Handle mermaid usages i.e. language == mermaidClassName
|
||||
if (token.language !== 'none' && token.language !== 'file') {
|
||||
// Decode the code content to replace HTML entities to characters
|
||||
const decodedCode = decode(token.code);
|
||||
const fallbackLanguage = guessLanguageFromPath(token.path);
|
||||
const value = highlighter.codeToHtml(decodedCode, {
|
||||
// we chose ts a fallback language as most example are ts.
|
||||
// Idealy all examples should have a specified language
|
||||
lang: token.language ?? fallbackLanguage,
|
||||
themes: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark',
|
||||
},
|
||||
cssVariablePrefix: '--shiki-',
|
||||
defaultColor: false,
|
||||
});
|
||||
token.code = value;
|
||||
const language = token.language ?? guessLanguageFromPath(token.path);
|
||||
|
||||
const codeAsHtml = codeToHtml(highlighter, decodedCode, language);
|
||||
token.code = codeAsHtml;
|
||||
}
|
||||
|
||||
const dom = new JSDOM(token.code);
|
||||
+7
-6
@@ -12,7 +12,8 @@ import {highlightCode} from './highlight.mjs';
|
||||
import {extractRegions} from './region.mjs';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {expandRangeStringValues} from './range.mjs';
|
||||
import {getApiLink} from '../../../utils.mjs';
|
||||
import {ApiEntries, getSymbolUrl} from '../../../../linking.mjs';
|
||||
import {RendererContext} from '../../../renderer.mjs';
|
||||
|
||||
/** Marked token for a custom docs element. */
|
||||
export interface CodeToken extends Tokens.Generic {
|
||||
@@ -45,14 +46,14 @@ export interface CodeToken extends Tokens.Generic {
|
||||
classes?: string[];
|
||||
}
|
||||
|
||||
export function formatCode(token: CodeToken) {
|
||||
export function formatCode(token: CodeToken, context: RendererContext): string {
|
||||
if (token.visibleLines !== undefined && token.visibleRegion !== undefined) {
|
||||
throw Error('Cannot define visible lines and visible region at the same time');
|
||||
}
|
||||
|
||||
extractRegions(token);
|
||||
calculateDiff(token);
|
||||
highlightCode(token);
|
||||
highlightCode(context.highlighter, token);
|
||||
|
||||
const containerEl = JSDOM.fragment(`
|
||||
<div class="docs-code">
|
||||
@@ -62,7 +63,7 @@ export function formatCode(token: CodeToken) {
|
||||
`).firstElementChild!;
|
||||
|
||||
applyContainerAttributesAndClasses(containerEl, token);
|
||||
processForApiLinks(containerEl);
|
||||
processForApiLinks(containerEl, context.apiEntries);
|
||||
|
||||
return containerEl.outerHTML;
|
||||
}
|
||||
@@ -70,7 +71,7 @@ export function formatCode(token: CodeToken) {
|
||||
/**
|
||||
* Process a DOM element to find spans (created by Shiki) and converts them to API links if they match entries.
|
||||
*/
|
||||
export function processForApiLinks(fragment: Element): void {
|
||||
export function processForApiLinks(fragment: Element, apiEntries: ApiEntries): void {
|
||||
const spans = fragment.querySelectorAll('span:not(:has(span))');
|
||||
|
||||
spans.forEach((span) => {
|
||||
@@ -80,7 +81,7 @@ export function processForApiLinks(fragment: Element): void {
|
||||
// Yes, index 0 is not interesting for us here
|
||||
const [, before, symbol, after] = symbolMatch;
|
||||
|
||||
const apiLink = getApiLink(symbol);
|
||||
const apiLink = getSymbolUrl(symbol, apiEntries);
|
||||
if (apiLink) {
|
||||
// Create a new link element
|
||||
const linkElement = fragment.ownerDocument!.createElement('a');
|
||||
+2
-2
@@ -6,9 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {regionParser} from '../../../../regions/region-parser.mjs';
|
||||
import {FileType} from '../../../../regions/remove-eslint-comments.mjs';
|
||||
import {CodeToken} from './index.mjs';
|
||||
import {regionParser} from '../regions/region-parser.mjs';
|
||||
import {FileType} from '../sanitizers/eslint.mjs';
|
||||
|
||||
/**
|
||||
* Updates the provided token to include the extracted region as the visible lines for the token.
|
||||
+4
-2
@@ -7,7 +7,9 @@
|
||||
*/
|
||||
|
||||
import {TokenizerThis, Tokens, RendererThis} from 'marked';
|
||||
import {getPageTitle, loadWorkspaceRelativeFile} from '../utils.mjs';
|
||||
import {loadWorkspaceRelativeFile} from '../helpers.mjs';
|
||||
import {getPageTitle} from '../transformations/heading.mjs';
|
||||
import {AdevDocsRenderer} from '../renderer.mjs';
|
||||
|
||||
interface DocsDecorativeHeaderToken extends Tokens.Generic {
|
||||
type: 'docs-decorative-header';
|
||||
@@ -63,7 +65,7 @@ export const docsDecorativeHeaderExtension = {
|
||||
<div class="docs-header-content">
|
||||
<docs-breadcrumb></docs-breadcrumb>
|
||||
|
||||
${getPageTitle(token.title)}
|
||||
${getPageTitle(token.title, (this.parser.renderer as AdevDocsRenderer).context.markdownFilePath)}
|
||||
|
||||
<p>${token.body}</p>
|
||||
</div>
|
||||
+3
-2
@@ -7,7 +7,8 @@
|
||||
*/
|
||||
|
||||
import {Token, Tokens, RendererThis, TokenizerThis} from 'marked';
|
||||
import {formatHeading} from '../../tranformations/heading.mjs';
|
||||
import {formatHeading} from '../../transformations/heading.mjs';
|
||||
import {AdevDocsRenderer} from '../../renderer.mjs';
|
||||
|
||||
interface DocsStepToken extends Tokens.Generic {
|
||||
type: 'docs-step';
|
||||
@@ -51,7 +52,7 @@ export const docsStepExtension = {
|
||||
return `
|
||||
<li>
|
||||
<span class="docs-step-number" aria-hidden="true"></span>
|
||||
${formatHeading({text: token.title, depth: 3})}
|
||||
${formatHeading({text: token.title, depth: 3}, (this.parser.renderer as AdevDocsRenderer).context.markdownFilePath)}
|
||||
${this.parser.parse(token.tokens)}
|
||||
</li>
|
||||
`;
|
||||
@@ -0,0 +1,32 @@
|
||||
/*!
|
||||
* @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 {existsSync, readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {cwd} from 'process';
|
||||
|
||||
/** Whether the link provided is external to the application. */
|
||||
export function isExternalLink(href: string | undefined | null) {
|
||||
return href?.startsWith('http') ?? false;
|
||||
}
|
||||
|
||||
/** Provide the correct target for the anchor tag based on the link provided. */
|
||||
export function anchorTarget(href: string | undefined | null) {
|
||||
return isExternalLink(href) ? ` target="_blank"` : '';
|
||||
}
|
||||
|
||||
/** The base directory of the workspace the script is running in. */
|
||||
const WORKSPACE_DIR = cwd();
|
||||
|
||||
export function loadWorkspaceRelativeFile(filePath: string): string {
|
||||
const fullFilePath = join(WORKSPACE_DIR, filePath);
|
||||
if (!existsSync(fullFilePath)) {
|
||||
throw Error(`Cannot find: ${filePath}`);
|
||||
}
|
||||
return readFileSync(fullFilePath, {encoding: 'utf-8'});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @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 {marked, Token} from 'marked';
|
||||
import {AdevDocsRenderer, RendererContext} from './renderer.mjs';
|
||||
import {docsAlertExtension} from './extensions/docs-alert.mjs';
|
||||
import {docsCalloutExtension} from './extensions/docs-callout.mjs';
|
||||
import {docsPillExtension} from './extensions/docs-pill/docs-pill.mjs';
|
||||
import {docsPillRowExtension} from './extensions/docs-pill/docs-pill-row.mjs';
|
||||
import {docsVideoExtension} from './extensions/docs-video.mjs';
|
||||
import {docsWorkflowExtension} from './extensions/docs-workflow/docs-workflow.mjs';
|
||||
import {docsStepExtension} from './extensions/docs-workflow/docs-step.mjs';
|
||||
import {docsCardExtension} from './extensions/docs-card/docs-card.mjs';
|
||||
import {docsCardContainerExtension} from './extensions/docs-card/docs-card-container.mjs';
|
||||
import {docsDecorativeHeaderExtension} from './extensions/docs-decorative-header.mjs';
|
||||
import {docsCodeBlockExtension} from './extensions/docs-code/docs-code-block.mjs';
|
||||
import {docsCodeExtension, DocsCodeToken} from './extensions/docs-code/docs-code.mjs';
|
||||
import {docsCodeMultifileExtension} from './extensions/docs-code/docs-code-multifile.mjs';
|
||||
import {hooks} from './hooks.mjs';
|
||||
|
||||
let markedInstance: typeof marked;
|
||||
const extensions = [
|
||||
docsAlertExtension,
|
||||
docsCalloutExtension,
|
||||
docsPillExtension,
|
||||
docsPillRowExtension,
|
||||
docsVideoExtension,
|
||||
docsWorkflowExtension,
|
||||
docsStepExtension,
|
||||
docsCardExtension,
|
||||
docsCardContainerExtension,
|
||||
docsDecorativeHeaderExtension,
|
||||
docsCodeBlockExtension,
|
||||
docsCodeExtension,
|
||||
docsCodeMultifileExtension,
|
||||
];
|
||||
|
||||
export async function parseMarkdownAsync(
|
||||
markdownContent: string,
|
||||
context: RendererContext,
|
||||
): Promise<string> {
|
||||
markedInstance ??= marked.use({hooks, extensions, walkTokens, async: true});
|
||||
return markedInstance.parse(markdownContent, {renderer: new AdevDocsRenderer(context)});
|
||||
}
|
||||
|
||||
export function parseMarkdown(markdownContent: string, context: RendererContext): string {
|
||||
markedInstance ??= marked.use({hooks, extensions, walkTokens});
|
||||
return markedInstance.parse(markdownContent, {renderer: new AdevDocsRenderer(context)}) as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe a HANDLE_MERMAID value which esbuild will use at build time to determine if the mermaid
|
||||
* related code should be included in the bundle.
|
||||
* THIS VALUE IS NOT AVAILABLE AT RUNTIME.
|
||||
*/
|
||||
export declare const HANDLE_MERMAID: boolean;
|
||||
|
||||
/** Type guard for if a provided token is the DocsCodeToken. */
|
||||
function isDocsCodeToken(token: Token): token is DocsCodeToken {
|
||||
return !!(token as DocsCodeToken).language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the provided token based on the token itself replacing its content/data in place
|
||||
* as appropriate.
|
||||
*/
|
||||
let mermaid: typeof import('./mermaid/index.mjs');
|
||||
export async function walkTokens(token: Token): Promise<void> {
|
||||
if (!isDocsCodeToken(token) || token.language !== 'mermaid') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (HANDLE_MERMAID) {
|
||||
mermaid ??= await import('./mermaid/index.mjs');
|
||||
return mermaid.processMermaidCodeBlock(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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 {Renderer} from 'marked';
|
||||
import {linkRender} from './transformations/link.mjs';
|
||||
import {tableRender} from './transformations/table.mjs';
|
||||
import {listRender} from './transformations/list.mjs';
|
||||
import {imageRender} from './transformations/image.mjs';
|
||||
import {textRender} from './transformations/text.mjs';
|
||||
import {headingRender} from './transformations/heading.mjs';
|
||||
import {codespanRender} from './transformations/code.mjs';
|
||||
import {HighlighterGeneric} from 'shiki';
|
||||
|
||||
export interface RendererContext {
|
||||
markdownFilePath: string;
|
||||
apiEntries: Record<string, string>;
|
||||
highlighter: HighlighterGeneric<any, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom renderer for marked that will be used to transform markdown files to HTML
|
||||
* files that can be used in the Angular docs.
|
||||
*/
|
||||
export class AdevDocsRenderer extends Renderer {
|
||||
constructor(public context: RendererContext) {
|
||||
super();
|
||||
}
|
||||
|
||||
defaultRenderer = new Renderer();
|
||||
|
||||
override link = linkRender;
|
||||
override table = tableRender;
|
||||
override list = listRender;
|
||||
override image = imageRender;
|
||||
override text = textRender;
|
||||
override heading = headingRender;
|
||||
override codespan = codespanRender;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
load("//adev/shared-docs:defaults.bzl", "ts_project")
|
||||
|
||||
ts_project(
|
||||
name = "renderer_context",
|
||||
srcs = ["renderer-context.mts"],
|
||||
visibility = ["//adev/shared-docs/pipeline/shared/marked/test:__subpackages__"],
|
||||
deps = ["//adev/shared-docs/pipeline/shared/marked"],
|
||||
)
|
||||
+3
-5
@@ -3,14 +3,12 @@ load("//adev/shared-docs:defaults.bzl", "ts_project", "zoneless_jasmine_test")
|
||||
ts_project(
|
||||
name = "code",
|
||||
testonly = True,
|
||||
srcs = glob([
|
||||
"**/*.spec.mts",
|
||||
]),
|
||||
srcs = glob(["*.spec.mts"]),
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/guides/test:parser_context",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
+2
-2
@@ -9,13 +9,13 @@
|
||||
import {readFile} from 'fs/promises';
|
||||
import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {parserContext} from '../parser-context.mjs';
|
||||
import {rendererContext} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let parsedMarkdown: string;
|
||||
beforeAll(async () => {
|
||||
const markdownContent = await readFile(resolve('./code.md'), {encoding: 'utf-8'});
|
||||
parsedMarkdown = await parseMarkdown(markdownContent, parserContext);
|
||||
parsedMarkdown = await parseMarkdown(markdownContent, rendererContext);
|
||||
});
|
||||
|
||||
it('should render symbol with link', () => {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
+2
-1
@@ -12,13 +12,14 @@ import {JSDOM} from 'jsdom';
|
||||
|
||||
import {AlertSeverityLevel} from '../../extensions/docs-alert.mjs';
|
||||
import {parseMarkdown} from '../../parse.mjs';
|
||||
import {rendererContext} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
|
||||
beforeAll(async () => {
|
||||
const markdownContent = await readFile(resolve('docs-alert.md'), {encoding: 'utf-8'});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, {}));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
for (const [key, level] of Object.entries(AlertSeverityLevel)) {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
+2
-1
@@ -10,13 +10,14 @@ import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {rendererContext} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
|
||||
beforeAll(async () => {
|
||||
const markdownContent = await readFile(resolve('docs-callout.md'), {encoding: 'utf-8'});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, {}));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
it(`defaults to a helpful callout`, () => {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
+2
-1
@@ -10,13 +10,14 @@ import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {rendererContext} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
|
||||
beforeAll(async () => {
|
||||
const markdownContent = await readFile(resolve('docs-card-container.md'), {encoding: 'utf-8'});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, {}));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
it('creates card containers containing multiple cards', () => {
|
||||
+2
-2
@@ -7,8 +7,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/guides/test:parser_context",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
+2
-3
@@ -10,15 +10,14 @@ import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {parser} from 'marked';
|
||||
import {parserContext} from '../parser-context.mjs';
|
||||
import {rendererContext} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
|
||||
beforeAll(async () => {
|
||||
const markdownContent = await readFile(resolve('docs-card.md'), {encoding: 'utf-8'});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, parserContext));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
it('creates cards with no links', () => {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
+3
-3
@@ -10,15 +10,15 @@ import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {initHighlighter} from '../../extensions/docs-code/format/highlight.mjs';
|
||||
import {rendererContext, setHighlighter} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await setHighlighter();
|
||||
const markdownContent = await readFile(resolve('./docs-code-block.md'), {encoding: 'utf-8'});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, {}));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
it('converts triple ticks into a code block', () => {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
+3
-3
@@ -10,17 +10,17 @@ import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {initHighlighter} from '../../extensions/docs-code/format/highlight.mjs';
|
||||
import {rendererContext, setHighlighter} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await setHighlighter();
|
||||
const markdownContent = await readFile(resolve('./docs-code-multifile.md'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, {}));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
it('converts triple ticks into a code block', () => {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
+3
-3
@@ -10,15 +10,15 @@ import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {initHighlighter} from '../../extensions/docs-code/format/highlight.mjs';
|
||||
import {rendererContext, setHighlighter} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await setHighlighter();
|
||||
const markdownContent = await readFile(resolve('./docs-code.md'), {encoding: 'utf-8'});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, {}));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
it('converts docs-code elements into a code block', () => {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
+2
-1
@@ -10,6 +10,7 @@ import {parseMarkdown} from '../../parse.mjs';
|
||||
import {resolve} from 'node:path';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {rendererContext} from '../renderer-context.mjs';
|
||||
|
||||
describe('markdown to html', () => {
|
||||
let markdownDocument: DocumentFragment;
|
||||
@@ -18,7 +19,7 @@ describe('markdown to html', () => {
|
||||
const markdownContent = await readFile(resolve('./docs-decorative-header.md'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, {}));
|
||||
markdownDocument = JSDOM.fragment(await parseMarkdown(markdownContent, rendererContext));
|
||||
});
|
||||
|
||||
it('sets the custom title in the header', () => {
|
||||
+2
-1
@@ -9,7 +9,8 @@ ts_project(
|
||||
deps = [
|
||||
"//adev:node_modules/@types/jsdom",
|
||||
"//adev:node_modules/jsdom",
|
||||
"//adev/shared-docs/pipeline/guides:guides_lib",
|
||||
"//adev/shared-docs/pipeline/shared/marked",
|
||||
"//adev/shared-docs/pipeline/shared/marked/test:renderer_context",
|
||||
],
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user