All files / scripts/validators/article slug.ts

100% Statements 8/8
75% Branches 3/4
100% Functions 2/2
100% Lines 8/8

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41                                6x                   2x 2x 2x 2x 2x 1x             2x    
/**
 * @module scripts/validators/article/slug
 * @description Permissive heading slug helper used by the empty-slug
 *              guard rule. Kept separate from the renderer's
 *              github-slugger-based production slugger so the validator
 *              has no runtime dependency on the render pipeline.
 *
 *              Rule census: extracted from
 *              `scripts/validate-article.ts` lines 487–492. Logic is
 *              byte-identical to the original.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
export function permissiveSlug(heading: string): string {
  return heading
    .toLowerCase()
    .replace(/[^\p{L}\p{N}]+/gu, '-')
    .replace(/^-+|-+$/g, '');
}
 
import type { ArticleViolation } from './types.js';
 
/** Empty-heading-slug rule. */
export function checkHeadingSlugs(rel: string, text: string): ArticleViolation[] {
  const out: ArticleViolation[] = [];
  const headingLines = text.match(/^#{2,6}\s+\S[^\n]*$/gm) ?? [];
  for (const h of headingLines) {
    const headingText = h.replace(/^#+\s+/, '').trim();
    if (!permissiveSlug(headingText)) {
      out.push({
        file: rel,
        code: 'empty-heading-slug',
        message: `Heading ${JSON.stringify(headingText)} produces an empty slug — pick a heading that contains at least one word/digit so the rendered #anchor is non-empty.`,
      });
    }
  }
  return out;
}