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 | 39x 39x 39x 39x 33x 33x 7x 39x 13x 13x 1386x 13x 13x 13x 13x 13x 13x 13x 13x 13x 1x 12x 1x 11x 11x 13x 3x 1x 2x 2x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module Templates/Sections/QualityScore
* @description Quality scoring helpers and badge rendering for article HTML.
* Counts words, sections, visualizations, and evidence references on rendered
* article HTML and emits an `aria-hidden` badge element summarizing the
* result. Split out of `section-builders.ts` (Refactor 8/8) so each section
* builder can be unit-tested in isolation.
*/
import { escapeHTML } from '../../utils/file-utils.js';
import type { ArticleQualityScore } from '../../types/index.js';
import { stripScriptBlocks, stripHtmlTags } from '../../utils/html-sanitize.js';
/**
* Count occurrences of a regex pattern in a string.
*
* @param content - String to search.
* @param pattern - Global regex pattern to match.
* @returns Number of matches found.
*/
function countMatches(content: string, pattern: RegExp): number {
const matches = content.match(pattern);
return matches !== null ? matches.length : 0;
}
/**
* Count elements whose `class` attribute contains a given CSS class token.
*
* Extracts every `class="…"` attribute, splits the value into tokens, and
* checks for an exact match — so `"dashboard"` will NOT match nested
* classes like `"dashboard-grid"` or `"dashboard-panel"`.
*
* @param content - HTML string to search.
* @param token - Exact CSS class name to look for.
* @returns Number of elements that have the given class token.
*/
function countClassToken(content: string, token: string): number {
let count = 0;
for (const m of content.matchAll(/class="([^"]*)"/g)) {
const value = m[1] ?? '';
if (value.split(/\s+/).includes(token)) {
count += 1;
}
}
return count;
}
/**
* Compute an article quality score by analysing the rendered HTML content.
*
* @param content - Full HTML content string of the article body.
* @returns `ArticleQualityScore` with word count, section counts, and overall rating.
*/
export function computeArticleQualityScore(content: string): ArticleQualityScore {
const noScripts = stripScriptBlocks(content);
const plainText = stripHtmlTags(noScripts).replace(/\s+/g, ' ').trim();
const wordCount =
plainText.length > 0 ? plainText.split(' ').filter((w) => w.length > 0).length : 0;
const totalSections = countMatches(noScripts, /<section\b/g);
const chartCount = countMatches(noScripts, /data-chart-config/g);
const dashboardCount = countClassToken(noScripts, 'dashboard');
const mindmapCount = countClassToken(noScripts, 'mindmap-section');
const swotCount = countClassToken(noScripts, 'swot-analysis');
const visualizationCount = chartCount + dashboardCount + mindmapCount + swotCount;
const analysisSections = totalSections - dashboardCount - mindmapCount - swotCount;
const evidenceReferences = countMatches(
noScripts,
/href="https:\/\/www\.europarl\.europa\.eu\/\w[^"]*"/g
);
let overallScore: ArticleQualityScore['overallScore'];
if (wordCount >= 800 && analysisSections >= 3 && visualizationCount >= 2) {
overallScore = 'excellent';
} else if (wordCount >= 500 && analysisSections >= 2) {
overallScore = 'good';
} else Iif (wordCount >= 200 && analysisSections >= 1) {
overallScore = 'adequate';
} else {
overallScore = 'needs-improvement';
}
return { wordCount, analysisSections, visualizationCount, evidenceReferences, overallScore };
}
/**
* Build an HTML quality score badge element for an article.
*
* The badge is `aria-hidden` since it conveys metadata, not primary content.
* Returns an empty string for articles with a 'needs-improvement' score to avoid
* surfacing poor-quality signals to readers.
*
* @param score - `ArticleQualityScore` to render.
* @returns HTML string for the badge `<div>`, or empty string for needs-improvement.
*/
export function buildQualityScoreBadge(score: ArticleQualityScore): string {
if (score.overallScore === 'needs-improvement') {
return '';
}
const safeScore = escapeHTML(score.overallScore);
return `<div class="article-quality-score" data-score="${safeScore}" aria-hidden="true">
<span class="qs-words">${score.wordCount}</span>
<span class="qs-sections">${score.analysisSections}</span>
<span class="qs-visuals">${score.visualizationCount}</span>
<span class="qs-evidence">${score.evidenceReferences}</span>
</div>`;
}
|