Skip to content

Commit cb52b02

Browse files
committed
fix(docs-infra): don't link API symbols inside code comments
Identifiers mentioned inside code comments in docs-code blocks were incorrectly turned into API links (for example, `// CanActivate ...` or `<!-- Component template -->` rendered the symbol as a link). Two separate mechanisms produced these links, so both are fixed: - `linkApiEntriesTransformer` runs a TypeScript scanner over the raw source regardless of language. It already skips `//` and `/* */` comments but not HTML comments, so identifiers inside `<!-- ... -->` were still linked. Compute HTML comment ranges using `/<!--[\s\S]*?-->/g` and skip identifier tokens that fall within those ranges. - `processForApiLinks` links symbols in every leaf span at the DOM level. Skip comment-colored spans and track `<!--`/`-->` delimiters across spans so text inside split HTML comments is not linked.
1 parent 1fb4678 commit cb52b02

4 files changed

Lines changed: 87 additions & 9 deletions

File tree

‎adev/shared-docs/pipeline/shared/marked/extensions/docs-code/format/index.mts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,41 @@ export function formatCode(token: CodeToken, context: RendererContext): string {
6767
return containerEl.outerHTML;
6868
}
6969

70+
/** Shiki comment color for the configured github-light / github-dark themes. */
71+
const COMMENT_COLOR = '#6A737D';
72+
7073
/**
7174
* Process a DOM element to find spans (created by Shiki) and converts them to API links if they match entries.
7275
*/
7376
export function processForApiLinks(fragment: Element, apiEntries: ApiEntries): void {
7477
const spans = fragment.querySelectorAll('span:not(:has(span))');
7578

79+
// Shiki may split an HTML/template comment (`<!-- ... -->`) across several
80+
// sibling spans whose inner text is NOT comment-colored, so a per-span color
81+
// check cannot detect it. Track the open/close delimiters across spans instead.
82+
let insideHtmlComment = false;
83+
7684
spans.forEach((span) => {
77-
const symbolMatch = span.textContent?.match(/^(.*?)(\w+)(.*)$/);
85+
const text = span.textContent ?? '';
86+
87+
const opensComment = text.includes('<!--');
88+
const closesComment = text.includes('-->');
89+
90+
if (opensComment) {
91+
insideHtmlComment = true;
92+
}
93+
94+
// Whether this span sits within an HTML comment (covers the opening,
95+
// closing, and any spans in between).
96+
const skip = insideHtmlComment || isCommentSpan(span);
97+
98+
if (closesComment) {
99+
insideHtmlComment = false;
100+
}
101+
102+
if (skip) return;
103+
104+
const symbolMatch = text.match(/^(.*?)(\w+)(.*)$/);
78105
if (!symbolMatch) return;
79106

80107
// Yes, index 0 is not interesting for us here
@@ -95,6 +122,13 @@ export function processForApiLinks(fragment: Element, apiEntries: ApiEntries): v
95122
});
96123
}
97124

125+
/** Returns whether a Shiki-generated span represents a code comment. */
126+
function isCommentSpan(span: Element): boolean {
127+
const style = span.getAttribute('style');
128+
if (!style) return false;
129+
return style.toUpperCase().includes(COMMENT_COLOR.toUpperCase());
130+
}
131+
98132
/** Build the header element if a header is provided in the token. */
99133
function buildHeaderElement(token: CodeToken) {
100134
let header = '';

‎adev/shared-docs/pipeline/shared/marked/test/docs-code/docs-code.md‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,25 @@ this is code
77

88
<docs-code header="src/locale/messages.fr.xlf (<trans-unit>)" path="./messages.fr.xlf" />
99

10-
<docs-code header="Property names should not be linked" language="ts">
11-
const form = {
12-
state: ['']
13-
};
10+
<docs-code header="Property names should not be linked" language="ts">
11+
const form = {
12+
state: ['']
13+
};
1414
</docs-code>
1515

1616
<docs-code hideDollar code="echo 'hello world'" />
17+
18+
<docs-code header="TS line comment should not be linked" language="ts">
19+
// CanActivateChild - protects all child routes
20+
const x = 1;
21+
</docs-code>
22+
23+
<docs-code header="HTML comment (uniform color) should not be linked" language="html">
24+
<!-- Component template -->
25+
<div></div>
26+
</docs-code>
27+
28+
<docs-code header="HTML comment (split delimiters) should not be linked" language="angular-html">
29+
<!-- Component template -->
30+
<div></div>
31+
</docs-code>

‎adev/shared-docs/pipeline/shared/marked/test/docs-code/docs-code.spec.mts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,19 @@ describe('markdown to html', () => {
5757
const codeBlock = markdownDocument.querySelectorAll('.docs-code')[5];
5858
expect(codeBlock.getAttribute('hideDollar')).toBe('true');
5959
});
60+
61+
it('should not link API symbols inside single-line (//) comments', () => {
62+
const codeBlock = markdownDocument.querySelectorAll('code')[6];
63+
expect(codeBlock?.innerHTML).not.toContain('href="/api/router/CanActivateChild"');
64+
});
65+
66+
it('should not link API symbols inside HTML comments (uniform comment color)', () => {
67+
const codeBlock = markdownDocument.querySelectorAll('code')[7];
68+
expect(codeBlock?.innerHTML).not.toContain('href="/api/core/Component"');
69+
});
70+
71+
it('should not link API symbols inside HTML comments (split delimiter color)', () => {
72+
const codeBlock = markdownDocument.querySelectorAll('code')[8];
73+
expect(codeBlock?.innerHTML).not.toContain('href="/api/core/Component"');
74+
});
6075
});

‎adev/shared-docs/pipeline/shared/shiki.mts‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ function removeWhitespaceTransformer(): ShikiTransformer {
8484
};
8585
}
8686

87+
/** Matches HTML/template comments, which the TypeScript scanner does not recognize. */
88+
const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
89+
8790
/** A custom transformer which adds a link to local API entries whenever a matching identifier is discovered in the code block. */
8891
function linkApiEntriesTransformer(apiEntries?: ApiEntries): ShikiTransformer {
8992
if (apiEntries === undefined) {
@@ -92,25 +95,36 @@ function linkApiEntriesTransformer(apiEntries?: ApiEntries): ShikiTransformer {
9295
return {
9396
preprocess(code, options) {
9497
options.decorations ??= [];
98+
99+
// Collect HTML/template comment ranges the TS scanner cannot see.
100+
const commentRanges: Array<[number, number]> = [];
101+
let match: RegExpExecArray | null;
102+
HTML_COMMENT_REGEX.lastIndex = 0;
103+
while ((match = HTML_COMMENT_REGEX.exec(code)) !== null) {
104+
commentRanges.push([match.index, match.index + match[0].length]);
105+
}
106+
const isInsideHtmlComment = (start: number): boolean =>
107+
commentRanges.some(([from, to]) => start >= from && start < to);
108+
95109
scanner.setText(code);
96110
let token = scanner.scan();
97111

98112
while (token !== ts.SyntaxKind.EndOfFileToken) {
99113
if (token === ts.SyntaxKind.Identifier) {
114+
const tokenStart = scanner.getTokenStart();
100115
const symbolUrl = getSymbolUrl(scanner.getTokenText(), apiEntries);
101-
if (symbolUrl !== undefined) {
116+
if (symbolUrl !== undefined && !isInsideHtmlComment(tokenStart)) {
102117
options.decorations.push({
103-
transform: (el) => {
118+
transform: (el: any) => {
104119
el.tagName = 'a';
105120
el.properties['href'] = symbolUrl;
106121
return el;
107122
},
108-
start: scanner.getTokenStart(),
123+
start: tokenStart,
109124
end: scanner.getTokenEnd(),
110125
});
111126
}
112127
}
113-
114128
token = scanner.scan();
115129
}
116130

0 commit comments

Comments
 (0)