euparliamentmonitor 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1005 -0
  3. package/SECURITY.md +151 -0
  4. package/package.json +131 -0
  5. package/scripts/constants/committee-indicator-map.d.ts +199 -0
  6. package/scripts/constants/committee-indicator-map.d.ts.map +1 -0
  7. package/scripts/constants/committee-indicator-map.js +1224 -0
  8. package/scripts/constants/committee-indicator-map.js.map +1 -0
  9. package/scripts/constants/config.d.ts +38 -0
  10. package/scripts/constants/config.d.ts.map +1 -0
  11. package/scripts/constants/config.js +66 -0
  12. package/scripts/constants/config.js.map +1 -0
  13. package/scripts/constants/language-articles.d.ts +84 -0
  14. package/scripts/constants/language-articles.d.ts.map +1 -0
  15. package/scripts/constants/language-articles.js +6771 -0
  16. package/scripts/constants/language-articles.js.map +1 -0
  17. package/scripts/constants/language-core.d.ts +38 -0
  18. package/scripts/constants/language-core.d.ts.map +1 -0
  19. package/scripts/constants/language-core.js +90 -0
  20. package/scripts/constants/language-core.js.map +1 -0
  21. package/scripts/constants/language-ui.d.ts +82 -0
  22. package/scripts/constants/language-ui.d.ts.map +1 -0
  23. package/scripts/constants/language-ui.js +889 -0
  24. package/scripts/constants/language-ui.js.map +1 -0
  25. package/scripts/constants/languages.d.ts +14 -0
  26. package/scripts/constants/languages.d.ts.map +1 -0
  27. package/scripts/constants/languages.js +15 -0
  28. package/scripts/constants/languages.js.map +1 -0
  29. package/scripts/generators/analysis-builders.d.ts +266 -0
  30. package/scripts/generators/analysis-builders.d.ts.map +1 -0
  31. package/scripts/generators/analysis-builders.js +2903 -0
  32. package/scripts/generators/analysis-builders.js.map +1 -0
  33. package/scripts/generators/breaking-content.d.ts +45 -0
  34. package/scripts/generators/breaking-content.d.ts.map +1 -0
  35. package/scripts/generators/breaking-content.js +530 -0
  36. package/scripts/generators/breaking-content.js.map +1 -0
  37. package/scripts/generators/committee-helpers.d.ts +54 -0
  38. package/scripts/generators/committee-helpers.d.ts.map +1 -0
  39. package/scripts/generators/committee-helpers.js +154 -0
  40. package/scripts/generators/committee-helpers.js.map +1 -0
  41. package/scripts/generators/dashboard-content.d.ts +95 -0
  42. package/scripts/generators/dashboard-content.d.ts.map +1 -0
  43. package/scripts/generators/dashboard-content.js +630 -0
  44. package/scripts/generators/dashboard-content.js.map +1 -0
  45. package/scripts/generators/deep-analysis-content.d.ts +23 -0
  46. package/scripts/generators/deep-analysis-content.d.ts.map +1 -0
  47. package/scripts/generators/deep-analysis-content.js +831 -0
  48. package/scripts/generators/deep-analysis-content.js.map +1 -0
  49. package/scripts/generators/mindmap-content.d.ts +55 -0
  50. package/scripts/generators/mindmap-content.d.ts.map +1 -0
  51. package/scripts/generators/mindmap-content.js +512 -0
  52. package/scripts/generators/mindmap-content.js.map +1 -0
  53. package/scripts/generators/motions-content.d.ts +50 -0
  54. package/scripts/generators/motions-content.d.ts.map +1 -0
  55. package/scripts/generators/motions-content.js +391 -0
  56. package/scripts/generators/motions-content.js.map +1 -0
  57. package/scripts/generators/news-enhanced.d.ts +14 -0
  58. package/scripts/generators/news-enhanced.d.ts.map +1 -0
  59. package/scripts/generators/news-enhanced.js +169 -0
  60. package/scripts/generators/news-enhanced.js.map +1 -0
  61. package/scripts/generators/news-indexes.d.ts +31 -0
  62. package/scripts/generators/news-indexes.d.ts.map +1 -0
  63. package/scripts/generators/news-indexes.js +410 -0
  64. package/scripts/generators/news-indexes.js.map +1 -0
  65. package/scripts/generators/pipeline/fetch-stage.d.ts +352 -0
  66. package/scripts/generators/pipeline/fetch-stage.d.ts.map +1 -0
  67. package/scripts/generators/pipeline/fetch-stage.js +1522 -0
  68. package/scripts/generators/pipeline/fetch-stage.js.map +1 -0
  69. package/scripts/generators/pipeline/generate-stage.d.ts +43 -0
  70. package/scripts/generators/pipeline/generate-stage.d.ts.map +1 -0
  71. package/scripts/generators/pipeline/generate-stage.js +204 -0
  72. package/scripts/generators/pipeline/generate-stage.js.map +1 -0
  73. package/scripts/generators/pipeline/output-stage.d.ts +48 -0
  74. package/scripts/generators/pipeline/output-stage.d.ts.map +1 -0
  75. package/scripts/generators/pipeline/output-stage.js +145 -0
  76. package/scripts/generators/pipeline/output-stage.js.map +1 -0
  77. package/scripts/generators/pipeline/transform-stage.d.ts +57 -0
  78. package/scripts/generators/pipeline/transform-stage.d.ts.map +1 -0
  79. package/scripts/generators/pipeline/transform-stage.js +111 -0
  80. package/scripts/generators/pipeline/transform-stage.js.map +1 -0
  81. package/scripts/generators/propositions-content.d.ts +29 -0
  82. package/scripts/generators/propositions-content.d.ts.map +1 -0
  83. package/scripts/generators/propositions-content.js +90 -0
  84. package/scripts/generators/propositions-content.js.map +1 -0
  85. package/scripts/generators/sankey-content.d.ts +45 -0
  86. package/scripts/generators/sankey-content.d.ts.map +1 -0
  87. package/scripts/generators/sankey-content.js +227 -0
  88. package/scripts/generators/sankey-content.js.map +1 -0
  89. package/scripts/generators/sitemap.d.ts +66 -0
  90. package/scripts/generators/sitemap.d.ts.map +1 -0
  91. package/scripts/generators/sitemap.js +562 -0
  92. package/scripts/generators/sitemap.js.map +1 -0
  93. package/scripts/generators/strategies/article-strategy.d.ts +146 -0
  94. package/scripts/generators/strategies/article-strategy.d.ts.map +1 -0
  95. package/scripts/generators/strategies/article-strategy.js +4 -0
  96. package/scripts/generators/strategies/article-strategy.js.map +1 -0
  97. package/scripts/generators/strategies/breaking-news-strategy.d.ts +64 -0
  98. package/scripts/generators/strategies/breaking-news-strategy.d.ts.map +1 -0
  99. package/scripts/generators/strategies/breaking-news-strategy.js +246 -0
  100. package/scripts/generators/strategies/breaking-news-strategy.js.map +1 -0
  101. package/scripts/generators/strategies/committee-reports-strategy.d.ts +93 -0
  102. package/scripts/generators/strategies/committee-reports-strategy.d.ts.map +1 -0
  103. package/scripts/generators/strategies/committee-reports-strategy.js +447 -0
  104. package/scripts/generators/strategies/committee-reports-strategy.js.map +1 -0
  105. package/scripts/generators/strategies/month-ahead-strategy.d.ts +60 -0
  106. package/scripts/generators/strategies/month-ahead-strategy.d.ts.map +1 -0
  107. package/scripts/generators/strategies/month-ahead-strategy.js +175 -0
  108. package/scripts/generators/strategies/month-ahead-strategy.js.map +1 -0
  109. package/scripts/generators/strategies/monthly-review-strategy.d.ts +66 -0
  110. package/scripts/generators/strategies/monthly-review-strategy.d.ts.map +1 -0
  111. package/scripts/generators/strategies/monthly-review-strategy.js +204 -0
  112. package/scripts/generators/strategies/monthly-review-strategy.js.map +1 -0
  113. package/scripts/generators/strategies/motions-strategy.d.ts +61 -0
  114. package/scripts/generators/strategies/motions-strategy.d.ts.map +1 -0
  115. package/scripts/generators/strategies/motions-strategy.js +215 -0
  116. package/scripts/generators/strategies/motions-strategy.js.map +1 -0
  117. package/scripts/generators/strategies/propositions-strategy.d.ts +60 -0
  118. package/scripts/generators/strategies/propositions-strategy.d.ts.map +1 -0
  119. package/scripts/generators/strategies/propositions-strategy.js +257 -0
  120. package/scripts/generators/strategies/propositions-strategy.js.map +1 -0
  121. package/scripts/generators/strategies/week-ahead-strategy.d.ts +57 -0
  122. package/scripts/generators/strategies/week-ahead-strategy.d.ts.map +1 -0
  123. package/scripts/generators/strategies/week-ahead-strategy.js +178 -0
  124. package/scripts/generators/strategies/week-ahead-strategy.js.map +1 -0
  125. package/scripts/generators/strategies/weekly-review-strategy.d.ts +63 -0
  126. package/scripts/generators/strategies/weekly-review-strategy.d.ts.map +1 -0
  127. package/scripts/generators/strategies/weekly-review-strategy.js +211 -0
  128. package/scripts/generators/strategies/weekly-review-strategy.js.map +1 -0
  129. package/scripts/generators/swot-content.d.ts +42 -0
  130. package/scripts/generators/swot-content.d.ts.map +1 -0
  131. package/scripts/generators/swot-content.js +366 -0
  132. package/scripts/generators/swot-content.js.map +1 -0
  133. package/scripts/generators/week-ahead-content.d.ts +103 -0
  134. package/scripts/generators/week-ahead-content.d.ts.map +1 -0
  135. package/scripts/generators/week-ahead-content.js +610 -0
  136. package/scripts/generators/week-ahead-content.js.map +1 -0
  137. package/scripts/index.d.ts +40 -0
  138. package/scripts/index.d.ts.map +1 -0
  139. package/scripts/index.js +53 -0
  140. package/scripts/index.js.map +1 -0
  141. package/scripts/mcp/ep-mcp-client.d.ts +471 -0
  142. package/scripts/mcp/ep-mcp-client.d.ts.map +1 -0
  143. package/scripts/mcp/ep-mcp-client.js +734 -0
  144. package/scripts/mcp/ep-mcp-client.js.map +1 -0
  145. package/scripts/mcp/mcp-connection.d.ts +264 -0
  146. package/scripts/mcp/mcp-connection.d.ts.map +1 -0
  147. package/scripts/mcp/mcp-connection.js +790 -0
  148. package/scripts/mcp/mcp-connection.js.map +1 -0
  149. package/scripts/mcp/mcp-health.d.ts +75 -0
  150. package/scripts/mcp/mcp-health.d.ts.map +1 -0
  151. package/scripts/mcp/mcp-health.js +78 -0
  152. package/scripts/mcp/mcp-health.js.map +1 -0
  153. package/scripts/mcp/mcp-retry.d.ts +94 -0
  154. package/scripts/mcp/mcp-retry.d.ts.map +1 -0
  155. package/scripts/mcp/mcp-retry.js +127 -0
  156. package/scripts/mcp/mcp-retry.js.map +1 -0
  157. package/scripts/mcp/wb-mcp-client.d.ts +38 -0
  158. package/scripts/mcp/wb-mcp-client.d.ts.map +1 -0
  159. package/scripts/mcp/wb-mcp-client.js +112 -0
  160. package/scripts/mcp/wb-mcp-client.js.map +1 -0
  161. package/scripts/templates/article-template.d.ts +9 -0
  162. package/scripts/templates/article-template.d.ts.map +1 -0
  163. package/scripts/templates/article-template.js +378 -0
  164. package/scripts/templates/article-template.js.map +1 -0
  165. package/scripts/templates/section-builders.d.ts +28 -0
  166. package/scripts/templates/section-builders.d.ts.map +1 -0
  167. package/scripts/templates/section-builders.js +142 -0
  168. package/scripts/templates/section-builders.js.map +1 -0
  169. package/scripts/types/analysis.d.ts +115 -0
  170. package/scripts/types/analysis.d.ts.map +1 -0
  171. package/scripts/types/analysis.js +4 -0
  172. package/scripts/types/analysis.js.map +1 -0
  173. package/scripts/types/common.d.ts +584 -0
  174. package/scripts/types/common.d.ts.map +1 -0
  175. package/scripts/types/common.js +96 -0
  176. package/scripts/types/common.js.map +1 -0
  177. package/scripts/types/generation.d.ts +104 -0
  178. package/scripts/types/generation.d.ts.map +1 -0
  179. package/scripts/types/generation.js +4 -0
  180. package/scripts/types/generation.js.map +1 -0
  181. package/scripts/types/index.d.ts +24 -0
  182. package/scripts/types/index.d.ts.map +1 -0
  183. package/scripts/types/index.js +16 -0
  184. package/scripts/types/index.js.map +1 -0
  185. package/scripts/types/intelligence.d.ts +129 -0
  186. package/scripts/types/intelligence.d.ts.map +1 -0
  187. package/scripts/types/intelligence.js +4 -0
  188. package/scripts/types/intelligence.js.map +1 -0
  189. package/scripts/types/mcp.d.ts +418 -0
  190. package/scripts/types/mcp.d.ts.map +1 -0
  191. package/scripts/types/mcp.js +4 -0
  192. package/scripts/types/mcp.js.map +1 -0
  193. package/scripts/types/parliament.d.ts +388 -0
  194. package/scripts/types/parliament.d.ts.map +1 -0
  195. package/scripts/types/parliament.js +4 -0
  196. package/scripts/types/parliament.js.map +1 -0
  197. package/scripts/types/quality.d.ts +114 -0
  198. package/scripts/types/quality.d.ts.map +1 -0
  199. package/scripts/types/quality.js +4 -0
  200. package/scripts/types/quality.js.map +1 -0
  201. package/scripts/types/stakeholder.d.ts +88 -0
  202. package/scripts/types/stakeholder.d.ts.map +1 -0
  203. package/scripts/types/stakeholder.js +16 -0
  204. package/scripts/types/stakeholder.js.map +1 -0
  205. package/scripts/types/visualization.d.ts +708 -0
  206. package/scripts/types/visualization.d.ts.map +1 -0
  207. package/scripts/types/visualization.js +4 -0
  208. package/scripts/types/visualization.js.map +1 -0
  209. package/scripts/types/world-bank.d.ts +85 -0
  210. package/scripts/types/world-bank.d.ts.map +1 -0
  211. package/scripts/types/world-bank.js +4 -0
  212. package/scripts/types/world-bank.js.map +1 -0
  213. package/scripts/utils/article-category.d.ts +18 -0
  214. package/scripts/utils/article-category.d.ts.map +1 -0
  215. package/scripts/utils/article-category.js +49 -0
  216. package/scripts/utils/article-category.js.map +1 -0
  217. package/scripts/utils/article-quality-scorer.d.ts +87 -0
  218. package/scripts/utils/article-quality-scorer.d.ts.map +1 -0
  219. package/scripts/utils/article-quality-scorer.js +1048 -0
  220. package/scripts/utils/article-quality-scorer.js.map +1 -0
  221. package/scripts/utils/content-metadata.d.ts +34 -0
  222. package/scripts/utils/content-metadata.d.ts.map +1 -0
  223. package/scripts/utils/content-metadata.js +249 -0
  224. package/scripts/utils/content-metadata.js.map +1 -0
  225. package/scripts/utils/content-validator.d.ts +94 -0
  226. package/scripts/utils/content-validator.d.ts.map +1 -0
  227. package/scripts/utils/content-validator.js +489 -0
  228. package/scripts/utils/content-validator.js.map +1 -0
  229. package/scripts/utils/copy-test-reports.d.ts +9 -0
  230. package/scripts/utils/copy-test-reports.d.ts.map +1 -0
  231. package/scripts/utils/copy-test-reports.js +508 -0
  232. package/scripts/utils/copy-test-reports.js.map +1 -0
  233. package/scripts/utils/file-utils.d.ts +144 -0
  234. package/scripts/utils/file-utils.d.ts.map +1 -0
  235. package/scripts/utils/file-utils.js +374 -0
  236. package/scripts/utils/file-utils.js.map +1 -0
  237. package/scripts/utils/fix-articles.d.ts +27 -0
  238. package/scripts/utils/fix-articles.d.ts.map +1 -0
  239. package/scripts/utils/fix-articles.js +510 -0
  240. package/scripts/utils/fix-articles.js.map +1 -0
  241. package/scripts/utils/generate-docs-index.d.ts +8 -0
  242. package/scripts/utils/generate-docs-index.d.ts.map +1 -0
  243. package/scripts/utils/generate-docs-index.js +275 -0
  244. package/scripts/utils/generate-docs-index.js.map +1 -0
  245. package/scripts/utils/html-sanitize.d.ts +18 -0
  246. package/scripts/utils/html-sanitize.d.ts.map +1 -0
  247. package/scripts/utils/html-sanitize.js +57 -0
  248. package/scripts/utils/html-sanitize.js.map +1 -0
  249. package/scripts/utils/intelligence-analysis.d.ts +173 -0
  250. package/scripts/utils/intelligence-analysis.d.ts.map +1 -0
  251. package/scripts/utils/intelligence-analysis.js +936 -0
  252. package/scripts/utils/intelligence-analysis.js.map +1 -0
  253. package/scripts/utils/intelligence-index.d.ts +126 -0
  254. package/scripts/utils/intelligence-index.d.ts.map +1 -0
  255. package/scripts/utils/intelligence-index.js +731 -0
  256. package/scripts/utils/intelligence-index.js.map +1 -0
  257. package/scripts/utils/metadata-utils.d.ts +14 -0
  258. package/scripts/utils/metadata-utils.d.ts.map +1 -0
  259. package/scripts/utils/metadata-utils.js +18 -0
  260. package/scripts/utils/metadata-utils.js.map +1 -0
  261. package/scripts/utils/news-metadata.d.ts +47 -0
  262. package/scripts/utils/news-metadata.d.ts.map +1 -0
  263. package/scripts/utils/news-metadata.js +259 -0
  264. package/scripts/utils/news-metadata.js.map +1 -0
  265. package/scripts/utils/validate-articles.d.ts +2 -0
  266. package/scripts/utils/validate-articles.d.ts.map +1 -0
  267. package/scripts/utils/validate-articles.js +284 -0
  268. package/scripts/utils/validate-articles.js.map +1 -0
  269. package/scripts/utils/validate-ep-api.d.ts +51 -0
  270. package/scripts/utils/validate-ep-api.d.ts.map +1 -0
  271. package/scripts/utils/validate-ep-api.js +160 -0
  272. package/scripts/utils/validate-ep-api.js.map +1 -0
  273. package/scripts/utils/world-bank-data.d.ts +84 -0
  274. package/scripts/utils/world-bank-data.d.ts.map +1 -0
  275. package/scripts/utils/world-bank-data.js +311 -0
  276. package/scripts/utils/world-bank-data.js.map +1 -0
@@ -0,0 +1,378 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @module Templates/ArticleTemplate
5
+ * @description Generates HTML templates for news articles with proper structure and metadata
6
+ */
7
+ import { createHash } from 'crypto';
8
+ import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, SKIP_LINK_TEXTS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
9
+ import { escapeHTML, isSafeURL } from '../utils/file-utils.js';
10
+ import { APP_VERSION } from '../constants/config.js';
11
+ /** Pattern for valid article dates (YYYY-MM-DD) */
12
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
13
+ /** Pattern for valid article slugs (lowercase letters, digits, hyphens) */
14
+ const SLUG_PATTERN = /^[a-z0-9-]+$/u;
15
+ /** Pattern for valid SRI integrity hashes (sha256/sha384/sha512 + base64) */
16
+ const SRI_HASH_PATTERN = /^sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2}$/u;
17
+ /** Words per minute for read-time calculation */
18
+ const TEMPLATE_WORDS_PER_MINUTE = 250;
19
+ /**
20
+ * BCP47 / Open Graph locale mapping for og:locale meta tag.
21
+ * Maps our 2-letter language codes to proper BCP47 locale strings.
22
+ */
23
+ const OG_LOCALE_MAP = {
24
+ en: 'en_GB',
25
+ sv: 'sv_SE',
26
+ da: 'da_DK',
27
+ no: 'nb_NO',
28
+ fi: 'fi_FI',
29
+ de: 'de_DE',
30
+ fr: 'fr_FR',
31
+ es: 'es_ES',
32
+ nl: 'nl_NL',
33
+ ar: 'ar_SA',
34
+ he: 'he_IL',
35
+ ja: 'ja_JP',
36
+ ko: 'ko_KR',
37
+ zh: 'zh_CN',
38
+ };
39
+ /**
40
+ * Build the article language switcher nav HTML.
41
+ * Links to the same article in all available languages using the filename pattern {date}-{slug}-{lang}.html.
42
+ *
43
+ * @param date - Article date (YYYY-MM-DD)
44
+ * @param slug - Article slug
45
+ * @param currentLang - Active language code
46
+ * @param availableLanguages - Languages for which the article exists; defaults to all supported languages
47
+ * @returns HTML string
48
+ */
49
+ function buildArticleLangSwitcher(date, slug, currentLang, availableLanguages) {
50
+ if (!DATE_PATTERN.test(date)) {
51
+ throw new Error(`Invalid article date format: "${date}"`);
52
+ }
53
+ if (!SLUG_PATTERN.test(slug)) {
54
+ throw new Error(`Invalid article slug format: "${slug}"`);
55
+ }
56
+ const safeDate = escapeHTML(date);
57
+ const safeSlug = escapeHTML(slug);
58
+ const langs = availableLanguages ?? ALL_LANGUAGES;
59
+ return langs
60
+ .map((code) => {
61
+ const flag = getLocalizedString(LANGUAGE_FLAGS, code);
62
+ const name = getLocalizedString(LANGUAGE_NAMES, code);
63
+ const active = code === currentLang ? ' active' : '';
64
+ const href = `${safeDate}-${safeSlug}-${code}.html`;
65
+ const safeTitle = escapeHTML(name);
66
+ return `<a href="${href}" class="lang-link${active}" hreflang="${code}" lang="${code}" title="${safeTitle}">${flag} ${code.toUpperCase()}</a>`;
67
+ })
68
+ .join('\n ');
69
+ }
70
+ /**
71
+ * Build a single footer section with title and content.
72
+ *
73
+ * @param title - Section heading text
74
+ * @param content - Inner HTML content
75
+ * @returns HTML string for one footer section
76
+ */
77
+ function buildFooterSection(title, content) {
78
+ return `<div class="footer-section">
79
+ <h3>${title}</h3>
80
+ ${content}
81
+ </div>`;
82
+ }
83
+ /**
84
+ * Build the language grid for the article footer.
85
+ *
86
+ * @param currentLang - Active language code
87
+ * @returns HTML string for the language grid
88
+ */
89
+ function buildArticleFooterLanguageGrid(currentLang) {
90
+ return ALL_LANGUAGES.map((code) => {
91
+ const flag = getLocalizedString(LANGUAGE_FLAGS, code);
92
+ const safeName = escapeHTML(getLocalizedString(LANGUAGE_NAMES, code));
93
+ const href = code === 'en' ? '../index.html' : `../index-${code}.html`;
94
+ const active = code === currentLang ? ' class="active"' : '';
95
+ return `<a href="${href}"${active} hreflang="${code}">${flag} ${safeName}</a>`;
96
+ }).join('\n ');
97
+ }
98
+ /**
99
+ * Generate complete HTML for a news article
100
+ *
101
+ * @param options - Article generation options
102
+ * @returns Complete HTML document string
103
+ */
104
+ export function generateArticleHTML(options) {
105
+ const { slug, title, subtitle, date, category, readTime, lang, content, keywords = [], sources = [], stylesHash, availableLanguages, } = options;
106
+ const dir = getTextDirection(lang);
107
+ const year = new Date().getFullYear();
108
+ // Format date for display
109
+ const displayDate = new Date(date).toLocaleDateString(lang, {
110
+ year: 'numeric',
111
+ month: 'long',
112
+ day: 'numeric',
113
+ });
114
+ const languageName = getLocalizedString(LANGUAGE_NAMES, lang);
115
+ const categoryLabels = getLocalizedString(ARTICLE_TYPE_LABELS, lang);
116
+ const categoryLabel = categoryLabels[category] ?? category;
117
+ const readTimeFormatter = getLocalizedString(READ_TIME_LABELS, lang);
118
+ // Auto-compute read-time from content word count if not explicitly set
119
+ const contentWordCount = content
120
+ .replace(/<[^>]+>/gu, ' ')
121
+ .replace(/\s+/gu, ' ')
122
+ .trim()
123
+ .split(' ').length;
124
+ const computedReadTime = Math.max(1, Math.ceil(contentWordCount / TEMPLATE_WORDS_PER_MINUTE));
125
+ const effectiveReadTime = readTime > 0 ? readTime : computedReadTime;
126
+ const readTimeLabel = readTimeFormatter(effectiveReadTime);
127
+ const backLabel = getLocalizedString(BACK_TO_NEWS_LABELS, lang);
128
+ const articleNavLabel = getLocalizedString(ARTICLE_NAV_LABELS, lang);
129
+ const skipLinkText = getLocalizedString(SKIP_LINK_TEXTS, lang);
130
+ const headerSubtitle = escapeHTML(getLocalizedString(HEADER_SUBTITLE_LABELS, lang));
131
+ const footerAboutHeading = escapeHTML(getLocalizedString(FOOTER_ABOUT_HEADING_LABELS, lang));
132
+ const footerAboutText = escapeHTML(getLocalizedString(FOOTER_ABOUT_TEXT_LABELS, lang));
133
+ const footerQuickLinksHeading = escapeHTML(getLocalizedString(FOOTER_QUICK_LINKS_LABELS, lang));
134
+ const footerBuiltByHeading = escapeHTML(getLocalizedString(FOOTER_BUILT_BY_LABELS, lang));
135
+ const footerLanguagesHeading = escapeHTML(getLocalizedString(FOOTER_LANGUAGES_LABELS, lang));
136
+ const indexHref = lang === 'en' ? '../index.html' : `../index-${lang}.html`;
137
+ // Escape values for safe HTML embedding
138
+ const safeTitle = escapeHTML(title);
139
+ const safeSubtitle = escapeHTML(subtitle);
140
+ const safeKeywords = keywords.map((k) => escapeHTML(k)).join(', ');
141
+ const safeCategoryLabel = escapeHTML(categoryLabel);
142
+ // Build JSON-LD as object for safe serialization
143
+ const jsonLd = JSON.stringify({
144
+ '@context': 'https://schema.org',
145
+ '@type': 'NewsArticle',
146
+ headline: title,
147
+ description: subtitle,
148
+ datePublished: date,
149
+ dateModified: date,
150
+ inLanguage: lang,
151
+ articleSection: categoryLabel,
152
+ author: {
153
+ '@type': 'Organization',
154
+ name: 'EU Parliament Monitor',
155
+ },
156
+ publisher: {
157
+ '@type': 'Organization',
158
+ name: 'EU Parliament Monitor',
159
+ url: 'https://hack23.github.io/euparliamentmonitor',
160
+ },
161
+ keywords: keywords.join(', '),
162
+ mainEntityOfPage: {
163
+ '@type': 'WebPage',
164
+ '@id': `https://hack23.github.io/euparliamentmonitor/news/${date}-${slug}-${lang}.html`,
165
+ },
166
+ }, null, 4);
167
+ // Validate and escape stylesHash — only allow valid SRI hash format
168
+ const safeSriAttrs = stylesHash && SRI_HASH_PATTERN.test(stylesHash)
169
+ ? ` integrity="${escapeHTML(stylesHash)}" crossorigin="anonymous"`
170
+ : '';
171
+ // Compute SHA-256 hash of the inline JSON-LD script content for CSP.
172
+ // IMPORTANT: The whitespace here ("\n " prefix and "\n " suffix) must exactly
173
+ // match the script tag content in the HTML template below:
174
+ // <script type="application/ld+json">
175
+ // ${jsonLd}
176
+ // </script>
177
+ const jsonLdScriptContent = `\n ${jsonLd}\n `;
178
+ const jsonLdHash = `sha256-${createHash('sha256').update(jsonLdScriptContent).digest('base64')}`;
179
+ // Reading-progress script hash — content must exactly match the <script> block.
180
+ const readingProgressScript = `\n (function(){\n var bar=document.querySelector('.reading-progress');\n if(!bar)return;\n bar.style.display='block';\n var ticking=false;\n window.addEventListener('scroll',function(){\n if(!ticking){\n window.requestAnimationFrame(function(){\n var h=document.documentElement;\n var scrollTop=h.scrollTop||document.body.scrollTop;\n var scrollHeight=h.scrollHeight-h.clientHeight;\n bar.style.width=scrollHeight>0?((scrollTop/scrollHeight)*100)+'%':'0%';\n ticking=false;\n });\n ticking=true;\n }\n },{passive:true});\n })();\n `;
181
+ const readingProgressHash = `sha256-${createHash('sha256').update(readingProgressScript).digest('base64')}`;
182
+ return `<!DOCTYPE html>
183
+ <html lang="${lang}" dir="${dir}">
184
+ <head>
185
+ <meta charset="UTF-8">
186
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
187
+ <meta http-equiv="X-Content-Type-Options" content="nosniff">
188
+ <meta name="referrer" content="no-referrer">
189
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' '${jsonLdHash}' '${readingProgressHash}'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; font-src 'self'; connect-src 'self'; frame-src 'none'; base-uri 'self'; form-action 'none'">
190
+ <title>${safeTitle} | EU Parliament Monitor</title>
191
+ <meta name="description" content="${safeSubtitle}">
192
+ <meta name="keywords" content="${safeKeywords}">
193
+ <meta name="author" content="EU Parliament Monitor">
194
+ <meta name="generator" content="EU Parliament Monitor v${escapeHTML(APP_VERSION)}">
195
+ <meta name="date" content="${date}">
196
+ <meta property="article:published_time" content="${date}">
197
+ <meta property="article:modified_time" content="${date}">
198
+ <meta property="article:author" content="EU Parliament Monitor">
199
+ <meta property="article:section" content="${safeCategoryLabel}">
200
+ ${keywords
201
+ .slice(0, 10)
202
+ .map((k) => `<meta property="article:tag" content="${escapeHTML(k)}">`)
203
+ .join('\n ')}
204
+
205
+ <!-- Favicons -->
206
+ <link rel="icon" type="image/x-icon" href="../favicon.ico">
207
+ <link rel="icon" type="image/png" sizes="32x32" href="../images/favicon-32x32.png">
208
+ <link rel="icon" type="image/png" sizes="16x16" href="../images/favicon-16x16.png">
209
+ <link rel="apple-touch-icon" sizes="180x180" href="../images/apple-touch-icon.png">
210
+ <link rel="manifest" href="../site.webmanifest">
211
+ <meta name="theme-color" content="#003399">
212
+ <link rel="alternate" type="application/rss+xml" title="EU Parliament Monitor RSS" href="../rss.xml">
213
+
214
+ <!-- Open Graph -->
215
+ <meta property="og:type" content="article">
216
+ <meta property="og:title" content="${safeTitle}">
217
+ <meta property="og:description" content="${safeSubtitle}">
218
+ <meta property="og:url" content="https://hack23.github.io/euparliamentmonitor/news/${date}-${slug}-${lang}.html">
219
+ <meta property="og:site_name" content="EU Parliament Monitor">
220
+ <meta property="og:locale" content="${OG_LOCALE_MAP[lang] ?? lang}">
221
+ <meta property="og:image" content="https://hack23.github.io/euparliamentmonitor/images/og-image.jpg">
222
+ <meta property="og:image:width" content="1200">
223
+ <meta property="og:image:height" content="630">
224
+ <meta property="og:image:alt" content="EU Parliament Monitor — AI-Disrupted Parliamentary Intelligence">
225
+
226
+ <!-- Twitter Card -->
227
+ <meta name="twitter:card" content="summary_large_image">
228
+ <meta name="twitter:title" content="${safeTitle}">
229
+ <meta name="twitter:description" content="${safeSubtitle}">
230
+ <meta name="twitter:image" content="https://hack23.github.io/euparliamentmonitor/images/og-image.jpg">
231
+ <meta name="twitter:image:alt" content="EU Parliament Monitor — AI-Disrupted Parliamentary Intelligence">
232
+
233
+ <link rel="canonical" href="https://hack23.github.io/euparliamentmonitor/news/${date}-${slug}-${lang}.html">
234
+ <link rel="stylesheet" href="../styles.css"${safeSriAttrs}>
235
+
236
+ <!-- Schema.org structured data -->
237
+ <script type="application/ld+json">
238
+ ${jsonLd}
239
+ </script>
240
+ </head>
241
+ <body>
242
+ <div class="reading-progress" aria-hidden="true"></div>
243
+ <a href="#main" class="skip-link">${skipLinkText}</a>
244
+
245
+ <header class="site-header" role="banner">
246
+ <div class="site-header__inner">
247
+ <a href="${indexHref}" class="site-header__brand" aria-label="EU Parliament Monitor">
248
+ <picture class="site-header__logo-picture">
249
+ <source srcset="../images/favicon-96x96.webp" type="image/webp">
250
+ <img class="site-header__logo" src="../images/favicon-96x96.png" alt="" width="96" height="96" aria-hidden="true">
251
+ </picture>
252
+ <span>
253
+ <span class="site-header__title">EU Parliament Monitor</span>
254
+ <span class="site-header__subtitle">${headerSubtitle}</span>
255
+ </span>
256
+ </a>
257
+ <nav class="site-header__langs" role="navigation" aria-label="Language selection">
258
+ ${buildArticleLangSwitcher(date, slug, lang, availableLanguages)}
259
+ </nav>
260
+ </div>
261
+ </header>
262
+
263
+ <nav class="article-top-nav" aria-label="${escapeHTML(articleNavLabel)}">
264
+ <a href="${indexHref}" class="back-to-news">${backLabel}</a>
265
+ </nav>
266
+
267
+ <main id="main" class="site-main">
268
+ <article class="news-article" lang="${lang}">
269
+ <header class="article-header">
270
+ <div class="article-meta">
271
+ <span class="article-type">${safeCategoryLabel}</span>
272
+ <span class="article-date">${displayDate}</span>
273
+ <span class="article-read-time">${readTimeLabel}</span>
274
+ <span class="article-lang">${languageName}</span>
275
+ </div>
276
+ <h1>${safeTitle}</h1>
277
+ <p class="article-subtitle">${safeSubtitle}</p>
278
+ </header>
279
+
280
+ ${content}
281
+
282
+ ${renderSourcesSection(sources, lang)}
283
+
284
+ <nav class="article-nav" aria-label="${escapeHTML(articleNavLabel)}">
285
+ <a href="${indexHref}" class="back-to-news">${backLabel}</a>
286
+ </nav>
287
+ </article>
288
+ </main>
289
+
290
+ <footer class="site-footer" role="contentinfo">
291
+ <div class="footer-content">
292
+ ${buildFooterSection(footerAboutHeading, `<p>${footerAboutText}</p>`)}
293
+ ${buildFooterSection(footerQuickLinksHeading, `<ul>
294
+ <li><a href="../index.html">Home</a></li>
295
+ <li><a href="../sitemap.html">Sitemap</a></li>
296
+ <li><a href="../rss.xml">RSS Feed</a></li>
297
+ <li><a href="https://github.com/Hack23/euparliamentmonitor">GitHub Repository</a></li>
298
+ <li><a href="https://github.com/Hack23/euparliamentmonitor/blob/main/LICENSE">Apache-2.0 License</a></li>
299
+ <li><a href="https://www.europarl.europa.eu/">European Parliament</a></li>
300
+ </ul>`)}
301
+ ${buildFooterSection(footerBuiltByHeading, `<ul>
302
+ <li><a href="https://hack23.com">hack23.com</a></li>
303
+ <li><a href="https://www.linkedin.com/company/hack23">LinkedIn</a></li>
304
+ <li><a href="https://github.com/Hack23/ISMS-PUBLIC">Security &amp; Privacy Policy</a></li>
305
+ <li><a href="mailto:james@hack23.com">Contact</a></li>
306
+ </ul>`)}
307
+ ${buildFooterSection(footerLanguagesHeading, `<div class="language-grid">
308
+ ${buildArticleFooterLanguageGrid(lang)}
309
+ </div>`)}
310
+ </div>
311
+ <div class="footer-bottom">
312
+ <p>&copy; 2008-${year} <a href="https://hack23.com">Hack23 AB</a> (Org.nr 5595347807) | Gothenburg, Sweden | v${escapeHTML(APP_VERSION)}</p>
313
+ <p class="footer-disclaimer"><span aria-hidden="true">⚠️</span> This platform is under ongoing improvement. Please <a href="https://github.com/Hack23/euparliamentmonitor/issues">report any issues on GitHub</a>.</p>
314
+ </div>
315
+ </footer>
316
+
317
+ <script>
318
+ (function(){
319
+ var bar=document.querySelector('.reading-progress');
320
+ if(!bar)return;
321
+ bar.style.display='block';
322
+ var ticking=false;
323
+ window.addEventListener('scroll',function(){
324
+ if(!ticking){
325
+ window.requestAnimationFrame(function(){
326
+ var h=document.documentElement;
327
+ var scrollTop=h.scrollTop||document.body.scrollTop;
328
+ var scrollHeight=h.scrollHeight-h.clientHeight;
329
+ bar.style.width=scrollHeight>0?((scrollTop/scrollHeight)*100)+'%':'0%';
330
+ ticking=false;
331
+ });
332
+ ticking=true;
333
+ }
334
+ },{passive:true});
335
+ })();
336
+ </script>${content.includes('data-chart-config')
337
+ ? `
338
+ <script src="../js/vendor/chart.umd.min.js" defer></script>
339
+ <script src="../js/vendor/chartjs-plugin-annotation.min.js" defer></script>
340
+ <script src="../js/chart-init.js" defer></script>`
341
+ : ''}${content.includes('mindmap-container') || content.includes('swot-matrix')
342
+ ? `
343
+ <script src="../js/vendor/d3.min.js" defer></script>
344
+ <script src="../js/d3-init.js" defer></script>`
345
+ : ''}
346
+ </body>
347
+ </html>`;
348
+ }
349
+ /**
350
+ * Render the sources section if sources are provided
351
+ *
352
+ * @param sources - Article source references
353
+ * @param lang - Language code for localized heading
354
+ * @returns HTML string for sources section or empty string
355
+ */
356
+ function renderSourcesSection(sources, lang) {
357
+ if (sources.length === 0) {
358
+ return '';
359
+ }
360
+ const sourcesHeading = escapeHTML(getLocalizedString(SOURCES_HEADING_LABELS, lang));
361
+ return `
362
+ <footer class="article-footer">
363
+ <section class="article-sources">
364
+ <h2>${sourcesHeading}</h2>
365
+ <ul>
366
+ ${sources
367
+ .map((source) => {
368
+ const safeSourceTitle = escapeHTML(source.title);
369
+ const href = isSafeURL(source.url) ? escapeHTML(source.url) : '#';
370
+ return `<li><a href="${href}" target="_blank" rel="noopener noreferrer">${safeSourceTitle}</a></li>`;
371
+ })
372
+ .join('\n ')}
373
+ </ul>
374
+ </section>
375
+ </footer>
376
+ `;
377
+ }
378
+ //# sourceMappingURL=article-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"article-template.js","sourceRoot":"","sources":["../../src/templates/article-template.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { ArticleQualityScore, TOCEntry, LanguageCode } from '../types/index.js';
2
+ /**
3
+ * Compute an article quality score by analysing the rendered HTML content.
4
+ *
5
+ * @param content - Full HTML content string of the article body.
6
+ * @returns {@link ArticleQualityScore} with word count, section counts, and overall rating.
7
+ */
8
+ export declare function computeArticleQualityScore(content: string): ArticleQualityScore;
9
+ /**
10
+ * Build an HTML table of contents navigation element from a list of entries.
11
+ *
12
+ * @param entries - Ordered list of {@link TOCEntry} items to render.
13
+ * @param lang - Language code used for the localised aria-label.
14
+ * @returns HTML string for the TOC `<nav>` element, or empty string when entries is empty.
15
+ */
16
+ export declare function buildTableOfContents(entries: TOCEntry[], lang: LanguageCode): string;
17
+ /**
18
+ * Build an HTML quality score badge element for an article.
19
+ *
20
+ * The badge is `aria-hidden` since it conveys metadata, not primary content.
21
+ * Returns an empty string for articles with a 'needs-improvement' score to avoid
22
+ * surfacing poor-quality signals to readers.
23
+ *
24
+ * @param score - {@link ArticleQualityScore} to render.
25
+ * @returns HTML string for the badge `<div>`, or empty string for needs-improvement.
26
+ */
27
+ export declare function buildQualityScoreBadge(score: ArticleQualityScore): string;
28
+ //# sourceMappingURL=section-builders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section-builders.d.ts","sourceRoot":"","sources":["../../src/templates/section-builders.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAwCrF;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAiD/E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CAsBpF;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,CAYzE"}
@@ -0,0 +1,142 @@
1
+ // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * @module Templates/SectionBuilders
5
+ * @description Reusable section builder utilities for article template architecture.
6
+ * Provides quality scoring, table of contents generation, and quality badge rendering.
7
+ */
8
+ import { escapeHTML } from '../utils/file-utils.js';
9
+ import { getLocalizedString, TOC_ARIA_LABELS } from '../constants/languages.js';
10
+ import { stripScriptBlocks } from '../utils/html-sanitize.js';
11
+ /**
12
+ * Count occurrences of a regex pattern in a string.
13
+ *
14
+ * @param content - String to search.
15
+ * @param pattern - Global regex pattern to match.
16
+ * @returns Number of matches found.
17
+ */
18
+ function countMatches(content, pattern) {
19
+ const matches = content.match(pattern);
20
+ return matches !== null ? matches.length : 0;
21
+ }
22
+ /**
23
+ * Count elements whose `class` attribute contains a given CSS class token.
24
+ *
25
+ * Extracts every `class="…"` attribute, splits the value into tokens, and
26
+ * checks for an exact match — so `"dashboard"` will NOT match nested
27
+ * classes like `"dashboard-grid"` or `"dashboard-panel"`.
28
+ *
29
+ * @param content - HTML string to search.
30
+ * @param token - Exact CSS class name to look for.
31
+ * @returns Number of elements that have the given class token.
32
+ */
33
+ function countClassToken(content, token) {
34
+ let count = 0;
35
+ for (const m of content.matchAll(/class="([^"]*)"/g)) {
36
+ const value = m[1] ?? '';
37
+ if (value.split(/\s+/).includes(token)) {
38
+ count += 1;
39
+ }
40
+ }
41
+ return count;
42
+ }
43
+ // stripScriptBlocks is imported from html-sanitize.ts
44
+ /**
45
+ * Compute an article quality score by analysing the rendered HTML content.
46
+ *
47
+ * @param content - Full HTML content string of the article body.
48
+ * @returns {@link ArticleQualityScore} with word count, section counts, and overall rating.
49
+ */
50
+ export function computeArticleQualityScore(content) {
51
+ // Remove script blocks before tag-stripping to avoid inflating word count.
52
+ // Uses iterative scanning instead of regex to avoid CodeQL js/bad-tag-filter.
53
+ const noScripts = stripScriptBlocks(content);
54
+ // Strip HTML tags to get plain text, then count words
55
+ const plainText = noScripts
56
+ .replace(/<[^>]*>/g, ' ')
57
+ .replace(/\s+/g, ' ')
58
+ .trim();
59
+ const wordCount = plainText.length > 0 ? plainText.split(' ').filter((w) => w.length > 0).length : 0;
60
+ // All further counting uses script-stripped HTML to avoid false positives
61
+ // from embedded JSON-LD or interactive script blocks.
62
+ const totalSections = countMatches(noScripts, /<section\b/g);
63
+ // Count data visualizations using exact class-token matching.
64
+ // countClassToken splits the class attribute value into tokens, so nested
65
+ // classes like "dashboard-grid" or "dashboard-panel" are NOT counted.
66
+ const chartCount = countMatches(noScripts, /data-chart-config/g);
67
+ const dashboardCount = countClassToken(noScripts, 'dashboard');
68
+ const mindmapCount = countClassToken(noScripts, 'mindmap-section');
69
+ const swotCount = countClassToken(noScripts, 'swot-analysis');
70
+ const visualizationCount = chartCount + dashboardCount + mindmapCount + swotCount;
71
+ // Exclude visualization sections from analysis section count
72
+ const analysisSections = totalSections - dashboardCount - mindmapCount - swotCount;
73
+ // Count EP document links (with a real path, not just the bare homepage).
74
+ // This excludes the generic footer link `https://www.europarl.europa.eu/`
75
+ // while counting links to specific EP resources like /doceo/, /plenary/, etc.
76
+ const evidenceReferences = countMatches(noScripts, /href="https:\/\/www\.europarl\.europa\.eu\/\w[^"]*"/g);
77
+ // Determine overall quality score
78
+ let overallScore;
79
+ if (wordCount >= 800 && analysisSections >= 3 && visualizationCount >= 2) {
80
+ overallScore = 'excellent';
81
+ }
82
+ else if (wordCount >= 500 && analysisSections >= 2) {
83
+ overallScore = 'good';
84
+ }
85
+ else if (wordCount >= 200 && analysisSections >= 1) {
86
+ overallScore = 'adequate';
87
+ }
88
+ else {
89
+ overallScore = 'needs-improvement';
90
+ }
91
+ return { wordCount, analysisSections, visualizationCount, evidenceReferences, overallScore };
92
+ }
93
+ /**
94
+ * Build an HTML table of contents navigation element from a list of entries.
95
+ *
96
+ * @param entries - Ordered list of {@link TOCEntry} items to render.
97
+ * @param lang - Language code used for the localised aria-label.
98
+ * @returns HTML string for the TOC `<nav>` element, or empty string when entries is empty.
99
+ */
100
+ export function buildTableOfContents(entries, lang) {
101
+ if (entries.length === 0) {
102
+ return '';
103
+ }
104
+ const ariaLabel = escapeHTML(getLocalizedString(TOC_ARIA_LABELS, lang));
105
+ const items = entries
106
+ .map((entry) => {
107
+ const safeLabel = escapeHTML(entry.label);
108
+ // Strip leading # to prevent href="##foo"
109
+ const safeId = escapeHTML(entry.id.replace(/^#/, ''));
110
+ const classAttr = entry.level === 2 ? ' class="toc-sub"' : '';
111
+ return `<li${classAttr}><a href="#${safeId}">${safeLabel}</a></li>`;
112
+ })
113
+ .join('\n ');
114
+ return `<nav class="article-toc" aria-label="${ariaLabel}">
115
+ <ol>
116
+ ${items}
117
+ </ol>
118
+ </nav>`;
119
+ }
120
+ /**
121
+ * Build an HTML quality score badge element for an article.
122
+ *
123
+ * The badge is `aria-hidden` since it conveys metadata, not primary content.
124
+ * Returns an empty string for articles with a 'needs-improvement' score to avoid
125
+ * surfacing poor-quality signals to readers.
126
+ *
127
+ * @param score - {@link ArticleQualityScore} to render.
128
+ * @returns HTML string for the badge `<div>`, or empty string for needs-improvement.
129
+ */
130
+ export function buildQualityScoreBadge(score) {
131
+ if (score.overallScore === 'needs-improvement') {
132
+ return '';
133
+ }
134
+ const safeScore = escapeHTML(score.overallScore);
135
+ return `<div class="article-quality-score" data-score="${safeScore}" aria-hidden="true">
136
+ <span class="qs-words">${score.wordCount}</span>
137
+ <span class="qs-sections">${score.analysisSections}</span>
138
+ <span class="qs-visuals">${score.visualizationCount}</span>
139
+ <span class="qs-evidence">${score.evidenceReferences}</span>
140
+ </div>`;
141
+ }
142
+ //# sourceMappingURL=section-builders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section-builders.js","sourceRoot":"","sources":["../../src/templates/section-builders.ts"],"names":[],"mappings":"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"}