mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
b6e83c5e72
Two headings on the same page can generate the same anchor id, and every
link to it then resolves to whichever comes first. Nothing caught this
because the route manifest keeps anchors in a `Set`, so a repeat collapses
into one entry before any check runs, and the existing link validation only
asks whether an anchor exists, which a duplicate satisfies.
The generator now inspects each page's headings while they are still an
ordered list and fails with the offending pages and anchors. The scanning
and the duplicate check move to `heading.mts` so they are covered by tests
next to `getIdFromHeading`, which should keep this from coming back.
(cherry picked from commit 5e8a70a010)
94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
/*!
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {extractHeadingIds, findDuplicateIds, getIdFromHeading} from '../heading.mjs';
|
|
|
|
describe('getIdFromHeading', () => {
|
|
it('should generate id from simple text', () => {
|
|
expect(getIdFromHeading('My Heading')).toBe('my-heading');
|
|
});
|
|
|
|
it('should generate id from text with special characters', () => {
|
|
expect(getIdFromHeading('Step 2 - Add component')).toBe('step-2---add-component');
|
|
});
|
|
|
|
it('should extract custom id when present', () => {
|
|
expect(getIdFromHeading('My Heading {#custom-id}')).toBe('custom-id');
|
|
});
|
|
|
|
it('should extract custom id ignoring surrounding spaces', () => {
|
|
expect(getIdFromHeading('My Heading {# custom-id }')).toBe('custom-id');
|
|
});
|
|
|
|
it('should prioritize custom id over text content', () => {
|
|
expect(getIdFromHeading('Duplicate Heading {#unique-id}')).toBe('unique-id');
|
|
});
|
|
});
|
|
|
|
describe('extractHeadingIds', () => {
|
|
it('should collect ids for every heading below the page title', () => {
|
|
const content = [
|
|
'# Page title',
|
|
'## First section',
|
|
'### A subsection',
|
|
'#### Deeper still',
|
|
].join('\n');
|
|
|
|
expect(extractHeadingIds(content)).toEqual(['first-section', 'a-subsection', 'deeper-still']);
|
|
});
|
|
|
|
it('should collect ids for headings with leading spaces', () => {
|
|
expect(extractHeadingIds(' ## Indented heading')).toEqual(['indented-heading']);
|
|
});
|
|
|
|
it('should collect ids for docs-step titles', () => {
|
|
const content = [
|
|
'## A section',
|
|
'<docs-step title="Install the package">',
|
|
'</docs-step>',
|
|
].join('\n');
|
|
|
|
expect(extractHeadingIds(content)).toEqual(['a-section', 'install-the-package']);
|
|
});
|
|
|
|
it('should use the custom id when a heading declares one', () => {
|
|
expect(extractHeadingIds('## Before {#migration-before}')).toEqual(['migration-before']);
|
|
});
|
|
|
|
it('should ignore headings inside fenced code blocks', () => {
|
|
const content = [
|
|
'## A real heading',
|
|
'```md',
|
|
'## Not a heading',
|
|
'### Also not a heading',
|
|
'```',
|
|
'## Another real heading',
|
|
].join('\n');
|
|
|
|
expect(extractHeadingIds(content)).toEqual(['a-real-heading', 'another-real-heading']);
|
|
});
|
|
});
|
|
|
|
describe('findDuplicateIds', () => {
|
|
it('should return an empty array when every id is unique', () => {
|
|
expect(findDuplicateIds(['one', 'two', 'three'])).toEqual([]);
|
|
});
|
|
|
|
it('should return an id that appears twice', () => {
|
|
expect(findDuplicateIds(['one', 'two', 'one'])).toEqual(['one']);
|
|
});
|
|
|
|
it('should report an id repeated many times only once', () => {
|
|
expect(findDuplicateIds(['one', 'one', 'one', 'one'])).toEqual(['one']);
|
|
});
|
|
|
|
it('should return every duplicated id', () => {
|
|
expect(findDuplicateIds(['before', 'after', 'before', 'after'])).toEqual(['before', 'after']);
|
|
});
|
|
});
|