All files / src/utils/articles filename.ts

96.66% Statements 29/30
75% Branches 12/16
100% Functions 6/6
96.42% Lines 27/28

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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115                                            15x 1x 1x     14x 20x                   83x   83x 4x     79x 79x       79x                                     2x   2x 4x     2x 6x 6x 6x 6x 6x         2x 4x 4x 4x       2x                                       5x 5x    
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Utils/Articles/Filename
 * @description Article-filename parsing, directory enumeration, and
 * per-language grouping helpers.
 */
 
import fs from 'fs';
import path from 'path';
import { NEWS_DIR, ARTICLE_FILENAME_PATTERN } from '../../constants/config.js';
import { ALL_LANGUAGES } from '../../constants/language-core.js';
import type { LanguageCode, ParsedArticle } from '../../types/index.js';
 
/**
 * Get all news article HTML files from the news directory
 *
 * @param newsDir - News directory path (defaults to NEWS_DIR)
 * @returns List of article filenames
 */
export function getNewsArticles(newsDir: string = NEWS_DIR): string[] {
  if (!fs.existsSync(newsDir)) {
    console.log('📁 News directory does not exist yet');
    return [];
  }
 
  const files = fs.readdirSync(newsDir);
  return files.filter((f) => f.endsWith('.html') && !f.startsWith('index-'));
}
 
/**
 * Parse article filename to extract metadata
 *
 * @param filename - Article filename (e.g., "2025-01-15-week-ahead-en.html")
 * @returns Parsed metadata or null if filename doesn't match pattern
 */
export function parseArticleFilename(filename: string): ParsedArticle | null {
  const match = filename.match(ARTICLE_FILENAME_PATTERN);
 
  if (!match) {
    return null;
  }
 
  const langCandidate = match[3] as string;
  Iif (!ALL_LANGUAGES.includes(langCandidate as LanguageCode)) {
    return null;
  }
 
  return {
    date: match[1] as string,
    slug: match[2] as string,
    lang: langCandidate as LanguageCode,
    filename,
  };
}
 
/**
 * Group articles by language code
 *
 * @param articles - List of article filenames
 * @param languages - Supported language codes
 * @returns Articles grouped by language, sorted newest first
 */
export function groupArticlesByLanguage(
  articles: string[],
  languages: readonly string[]
): Record<string, ParsedArticle[]> {
  const grouped: Record<string, ParsedArticle[]> = {};
 
  for (const lang of languages) {
    grouped[lang] = [];
  }
 
  for (const article of articles) {
    const parsed = parseArticleFilename(article);
    Eif (parsed) {
      const bucket = grouped[parsed.lang];
      Eif (bucket) {
        bucket.push(parsed);
      }
    }
  }
 
  for (const lang in grouped) {
    const bucket = grouped[lang];
    Eif (bucket) {
      bucket.sort((a, b) => b.date.localeCompare(a.date));
    }
  }
 
  return grouped;
}
 
/**
 * Check whether a news article file already exists on disk.
 *
 * This is used by generation pipelines to skip work when a prior workflow run
 * (or the same run) has already produced the article, avoiding unnecessary
 * regeneration and potential merge conflicts.
 *
 * @param slug - Article slug including date prefix (e.g. `"2025-01-15-week-ahead"`)
 * @param lang - Language code (e.g. `"en"`)
 * @param newsDir - Absolute path to the news output directory (defaults to NEWS_DIR)
 * @returns `true` when the article file exists
 */
export function checkArticleExists(
  slug: string,
  lang: string,
  newsDir: string = NEWS_DIR
): boolean {
  const filename = `${slug}-${lang}.html`;
  return fs.existsSync(path.join(newsDir, filename));
}