All files / src/utils/html validate.ts

100% Statements 13/13
100% Branches 6/6
100% Functions 3/3
100% Lines 13/13

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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73                              5x 5x 5x   1x                               36x                                             3x   3x 21x 21x 4x   21x 7x       3x    
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Utils/Html/Validate
 * @description URL safety and generated-article HTML structural validation.
 */
 
/**
 * Validate that a URL uses a safe scheme (http or https)
 *
 * @param url - URL string to validate
 * @returns true if URL has a safe scheme
 */
export function isSafeURL(url: string): boolean {
  try {
    const parsed = new URL(url);
    return parsed.protocol === 'http:' || parsed.protocol === 'https:';
  } catch {
    return false;
  }
}
 
/** Result of article HTML validation */
export interface ArticleValidationResult {
  /** Whether the article passes all structural checks */
  valid: boolean;
  /** List of missing elements */
  errors: readonly string[];
}
 
/** Required structural elements that every article must contain */
const REQUIRED_ARTICLE_ELEMENTS: ReadonlyArray<{
  selector: string | readonly string[];
  label: string;
}> = [
  {
    selector: ['class="site-header__langs"', 'class="language-switcher"'],
    label: 'language switcher nav',
  },
  { selector: 'class="article-top-nav"', label: 'article-top-nav (back button)' },
  { selector: 'class="site-header"', label: 'site-header' },
  { selector: 'class="skip-link"', label: 'skip-link' },
  { selector: 'class="reading-progress"', label: 'reading-progress bar' },
  { selector: '<main id="main"', label: 'main content wrapper' },
  { selector: 'class="site-footer"', label: 'site-footer' },
] as const;
 
/**
 * Validate that generated article HTML includes all required structural elements.
 *
 * This is the primary validation gate — articles must be generated correctly
 * by the template. The fix-articles script is only a fallback for historic articles.
 *
 * @param html - Complete HTML string of the article
 * @returns Validation result with errors list (empty if valid)
 */
export function validateArticleHTML(html: string): ArticleValidationResult {
  const errors: string[] = [];
 
  for (const element of REQUIRED_ARTICLE_ELEMENTS) {
    const sel = element.selector;
    const found = Array.isArray(sel)
      ? sel.some((s) => html.includes(s))
      : html.includes(sel as string);
    if (!found) {
      errors.push(`Missing required element: ${element.label}`);
    }
  }
 
  return { valid: errors.length === 0, errors };
}