All files / scripts/statskontoret/internal text.ts

100% Statements 40/40
75% Branches 15/20
100% Functions 14/14
100% Lines 36/36

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                              24x       7x       1500x               41x       317x       285x       57x 57x     57x 57x       73x 40x 40x       34x 34x 134x   34x             238x 238x 768x 768x   106x 1616x   69x       82x 82x 82x 148x   82x       70x 70x       33x 33x 33x 33x   33x    
/**
 * @module scripts/statskontoret/internal/text
 * @description Internal shared text/number/XML helpers used across the
 * Statskontoret submodules (extractors, parsers, domain filters).
 *
 * Not part of the public re-export surface — callers must continue importing
 * the high-level symbols from `scripts/statskontoret-client.js`.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import { decodeHtmlEntities } from '../../html-utils.js';
 
export function trimTrailingSlash(value: string): string {
  return value.replace(/\/+$/, '');
}
 
export function normalizeWhitespace(value: string): string {
  return value.replace(/\s+/g, ' ').trim();
}
 
export function normalizeKey(value: string): string {
  return value
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-z0-9]+/g, '');
}
 
export function roundOneDecimal(value: number): number {
  return Math.round(value * 10) / 10;
}
 
export function decodeHtml(value: string): string {
  return decodeHtmlEntities(value).replace(/\u00a0/g, ' ');
}
 
export function decodeXml(value: string): string {
  return decodeHtml(value);
}
 
export function parseStatskontoretSwedishNumber(value: string): number | undefined {
  const compact = value.replace(/\s/g, '');
  const normalized = compact.includes(',')
    ? compact.replace(/\./g, '').replace(',', '.')
    : compact;
  const parsed = Number.parseFloat(normalized);
  return Number.isFinite(parsed) ? parsed : undefined;
}
 
export function parseStatskontoretOptionalInt(value: string | null): number | undefined {
  if (!value) return undefined;
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) ? parsed : undefined;
}
 
export function buildRecordLookup(record: Record<string, string>): Map<string, string> {
  const lookup = new Map<string, string>();
  for (const [key, value] of Object.entries(record)) {
    lookup.set(normalizeKey(key), value);
  }
  return lookup;
}
 
export function findField(
  lookup: ReadonlyMap<string, string>,
  candidates: readonly string[],
): string | undefined {
  const normalizedCandidates = candidates.map(normalizeKey);
  for (const candidate of normalizedCandidates) {
    const exact = lookup.get(candidate);
    if (exact !== undefined) return exact;
  }
  for (const [key, value] of lookup.entries()) {
    if (normalizedCandidates.some((candidate) => key.includes(candidate))) return value;
  }
  return undefined;
}
 
export function parseXmlAttributes(input: string): Map<string, string> {
  const attrs = new Map<string, string>();
  const attrRe = /([\w:-]+)=["']([^"']*)["']/g;
  for (const match of input.matchAll(attrRe)) {
    attrs.set(match[1], decodeXml(match[2] ?? ''));
  }
  return attrs;
}
 
export function firstXmlTagValue(xml: string, tag: string): string | undefined {
  const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i').exec(xml);
  return match ? decodeXml(match[1] ?? '') : undefined;
}
 
export function extractTextNodes(xml: string): string {
  const parts: string[] = [];
  const textRe = /<t\b[^>]*>([\s\S]*?)<\/t>/gi;
  for (const match of xml.matchAll(textRe)) {
    parts.push(decodeXml(match[1] ?? ''));
  }
  return parts.join('');
}