euparliamentmonitor 0.9.0 → 0.9.2
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.
- package/README.md +2 -2
- package/package.json +4 -4
- package/scripts/aggregator/analysis-aggregator.js +17 -15
- package/scripts/aggregator/article-generator.d.ts +28 -3
- package/scripts/aggregator/article-generator.js +98 -34
- package/scripts/aggregator/article-html.d.ts +10 -0
- package/scripts/aggregator/article-html.js +210 -12
- package/scripts/aggregator/article-metadata.d.ts +75 -0
- package/scripts/aggregator/article-metadata.js +482 -32
- package/scripts/aggregator/cli/parse.d.ts +19 -1
- package/scripts/aggregator/cli/parse.js +27 -26
- package/scripts/aggregator/reader-intelligence-guide.js +17 -4
- package/scripts/config/article-horizons.js +17 -2
- package/scripts/constants/language-ui.d.ts +33 -0
- package/scripts/constants/language-ui.js +534 -0
- package/scripts/constants/languages.d.ts +1 -1
- package/scripts/constants/languages.js +1 -1
- package/scripts/generators/news-indexes.js +3 -1
- package/scripts/generators/seo-copy.js +13 -0
- package/scripts/index.d.ts +1 -1
- package/scripts/index.js +1 -1
- package/scripts/mcp/ep-mcp-client.d.ts +6 -6
- package/scripts/mcp/ep-mcp-client.js +8 -8
- package/scripts/mcp/fetch-proxy-server.js +10 -1
- package/scripts/mcp/imf-mcp-client.js +75 -9
- package/scripts/templates/section-builders.d.ts +2 -3
- package/scripts/templates/section-builders.js +15 -12
- package/scripts/templates/sync-template-frontmatter.js +19 -1
- package/scripts/types/mcp.d.ts +1 -1
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ The published site is the audience-facing companion to this npm/TypeScript packa
|
|
|
136
136
|
|
|
137
137
|
**MCP Server Integration**: The project uses the
|
|
138
138
|
[European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server)
|
|
139
|
-
v1.3.
|
|
139
|
+
v1.3.2 for accessing real EU Parliament data via the Model Context Protocol.
|
|
140
140
|
|
|
141
141
|
- **MCP Server Status**: ✅ Fully operational — 60+ EP data tools available
|
|
142
142
|
(feeds, direct lookups, analytical tools, intelligence correlation)
|
|
@@ -432,7 +432,7 @@ import type { ArticleCategory, LanguageCode } from 'euparliamentmonitor/types';
|
|
|
432
432
|
|
|
433
433
|
## 🔌 Data Sources
|
|
434
434
|
|
|
435
|
-
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.3.
|
|
435
|
+
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.3.2+, fully operational):
|
|
436
436
|
|
|
437
437
|
- 🗳️ Plenary sessions, voting records, roll-call votes
|
|
438
438
|
- 📜 Adopted texts, motions, resolutions, urgency files
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euparliamentmonitor",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
|
|
6
6
|
"main": "scripts/index.js",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"@playwright/test": "1.59.1",
|
|
147
147
|
"@types/d3": "7.4.3",
|
|
148
148
|
"@types/markdown-it": "^14.1.2",
|
|
149
|
-
"@types/node": "25.6.
|
|
149
|
+
"@types/node": "25.6.2",
|
|
150
150
|
"@types/papaparse": "5.5.2",
|
|
151
151
|
"@typescript-eslint/eslint-plugin": "8.59.2",
|
|
152
152
|
"@typescript-eslint/parser": "8.59.2",
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"husky": "9.1.7",
|
|
166
166
|
"jscpd": "4.0.9",
|
|
167
167
|
"knip": "^6.7.0",
|
|
168
|
-
"lint-staged": "17.0.
|
|
168
|
+
"lint-staged": "17.0.3",
|
|
169
169
|
"mermaid": "11.14.0",
|
|
170
170
|
"papaparse": "5.5.3",
|
|
171
171
|
"prettier": "3.8.3",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"node": ">=26"
|
|
180
180
|
},
|
|
181
181
|
"dependencies": {
|
|
182
|
-
"european-parliament-mcp-server": "1.3.
|
|
182
|
+
"european-parliament-mcp-server": "1.3.2",
|
|
183
183
|
"markdown-it": "^14.1.1",
|
|
184
184
|
"markdown-it-anchor": "^9.2.0",
|
|
185
185
|
"markdown-it-attrs": "^4.3.1",
|
|
@@ -550,30 +550,33 @@ export function aggregateAnalysisRun(options) {
|
|
|
550
550
|
const analysisIndex = renderAnalysisIndex(includedArtifacts, manifestRelPath);
|
|
551
551
|
const readerGuide = renderReaderIntelligenceGuide(emittedSections, includedArtifacts);
|
|
552
552
|
// Deterministic 3–7 bullet "Key takeaways" block, harvested from the
|
|
553
|
-
// synthesis-summary / intelligence-assessment artifacts.
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
553
|
+
// synthesis-summary / intelligence-assessment artifacts. Sits between
|
|
554
|
+
// the Reader Intelligence Guide and the deep sections: the reader gets
|
|
555
|
+
// the BLUF (Executive Brief) → a navigation map (Reader Guide) → a
|
|
556
|
+
// bullet digest of the strongest findings (Key Takeaways) → the deep
|
|
557
|
+
// analysis. This is the order requested for reader UX so navigation
|
|
558
|
+
// is established before the reader commits to scanning takeaways.
|
|
557
559
|
const keyTakeaways = buildKeyTakeaways({ runDir });
|
|
558
|
-
// TOC ordering
|
|
560
|
+
// TOC ordering must match the rendered Markdown body 1:1. Order:
|
|
559
561
|
// Executive Brief (already first in emittedSections via appendSection) →
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
+
// Reader Intelligence Guide (inserted right after the brief when present) →
|
|
563
|
+
// Key Takeaways (inserted right after the guide when present) →
|
|
564
|
+
// remaining sections → audit appendices.
|
|
562
565
|
let postBriefIdx = emittedSections.length > 0 &&
|
|
563
566
|
emittedSections[0]?.id === namespacedSectionId(execBriefSection?.id ?? '')
|
|
564
567
|
? 1
|
|
565
568
|
: 0;
|
|
566
|
-
if (
|
|
569
|
+
if (readerGuide) {
|
|
567
570
|
emittedSections.splice(postBriefIdx, 0, {
|
|
568
|
-
id:
|
|
569
|
-
title:
|
|
571
|
+
id: READER_GUIDE_SECTION_ID,
|
|
572
|
+
title: READER_GUIDE_SECTION_TITLE,
|
|
570
573
|
});
|
|
571
574
|
postBriefIdx += 1;
|
|
572
575
|
}
|
|
573
|
-
if (
|
|
576
|
+
if (keyTakeaways) {
|
|
574
577
|
emittedSections.splice(postBriefIdx, 0, {
|
|
575
|
-
id:
|
|
576
|
-
title:
|
|
578
|
+
id: KEY_TAKEAWAYS_SECTION_ID,
|
|
579
|
+
title: KEY_TAKEAWAYS_SECTION_TITLE,
|
|
577
580
|
});
|
|
578
581
|
}
|
|
579
582
|
emittedSections.push({ id: TRADECRAFT_SECTION_ID, title: TRADECRAFT_SECTION_TITLE });
|
|
@@ -583,9 +586,8 @@ export function aggregateAnalysisRun(options) {
|
|
|
583
586
|
'',
|
|
584
587
|
...execBriefMarkdown,
|
|
585
588
|
'',
|
|
589
|
+
...(readerGuide ? [readerGuide, ''] : []),
|
|
586
590
|
...(keyTakeaways ? [keyTakeaways, ''] : []),
|
|
587
|
-
readerGuide,
|
|
588
|
-
'',
|
|
589
591
|
...sectionMarkdown,
|
|
590
592
|
'',
|
|
591
593
|
provenance,
|
|
@@ -19,7 +19,11 @@ export interface CliOptions {
|
|
|
19
19
|
* is earlier are skipped.
|
|
20
20
|
*/
|
|
21
21
|
readonly since?: string;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Languages to render. Defaults to all 14. The CLI always populates
|
|
24
|
+
* this with `[...ALL_LANGUAGES]` (the `--lang/--language` flags have
|
|
25
|
+
* been removed); programmatic callers (tests) can override it.
|
|
26
|
+
*/
|
|
23
27
|
readonly langs: readonly LanguageCode[];
|
|
24
28
|
/** Output directory for HTML files (defaults to `news/`). */
|
|
25
29
|
readonly outDir: string;
|
|
@@ -30,8 +34,11 @@ export interface CliOptions {
|
|
|
30
34
|
/** Optional: override the auto-derived article description (single-run only). */
|
|
31
35
|
readonly description?: string;
|
|
32
36
|
/**
|
|
33
|
-
* When true, only the source Markdown is written (no HTML) —
|
|
34
|
-
*
|
|
37
|
+
* When true, only the source Markdown is written (no HTML) — preserved
|
|
38
|
+
* for programmatic callers (tests) that need to skip HTML emission for
|
|
39
|
+
* speed. The CLI no longer exposes a flag for this and always sets
|
|
40
|
+
* `false`; every workflow-driven invocation emits HTML for all 14
|
|
41
|
+
* languages.
|
|
35
42
|
*/
|
|
36
43
|
readonly markdownOnly: boolean;
|
|
37
44
|
}
|
|
@@ -93,6 +100,24 @@ export declare function sanitizeRunSuffix(runId: string): string;
|
|
|
93
100
|
* @returns Plain-text description, truncated to ≤300 characters
|
|
94
101
|
*/
|
|
95
102
|
export declare function extractDefaultDescription(markdown: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Insert the regenerated Reader Intelligence Guide HTML immediately after
|
|
105
|
+
* the Executive Brief section so the rendered article body matches the
|
|
106
|
+
* documented order (Executive Brief → Reader Intelligence Guide → Key
|
|
107
|
+
* Takeaways → deep sections). The Executive Brief section ends where the
|
|
108
|
+
* next H2 begins; we splice at that boundary. When the brief is absent
|
|
109
|
+
* (sparse runs) we fall back to prepending so the guide still appears
|
|
110
|
+
* at the top of the body.
|
|
111
|
+
*
|
|
112
|
+
* Implementation uses `indexOf` rather than a regex so the splice point
|
|
113
|
+
* is deterministic and immune to polynomial-regex backtracking on
|
|
114
|
+
* pathological input.
|
|
115
|
+
*
|
|
116
|
+
* @param bodyHtml - Rendered article body
|
|
117
|
+
* @param guideHtml - Reader Intelligence Guide HTML fragment
|
|
118
|
+
* @returns Body HTML with the guide spliced after the Executive Brief
|
|
119
|
+
*/
|
|
120
|
+
export declare function insertReaderGuideAfterExecutiveBrief(bodyHtml: string, guideHtml: string): string;
|
|
96
121
|
/**
|
|
97
122
|
* Run the full aggregate → render → write pipeline for one run.
|
|
98
123
|
*
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* npm run generate-article -- --run analysis/daily/2026-01-15/breaking-run1
|
|
13
|
-
* npm run generate-article -- --run ... --lang en --lang sv
|
|
14
13
|
* npm run generate-article -- --run ... --out-dir news --title "Headline"
|
|
15
14
|
*
|
|
15
|
+
* **Always-14-languages-always-HTML contract**: every CLI invocation
|
|
16
|
+
* renders every supported language to HTML. The legacy `--lang` /
|
|
17
|
+
* `--language` / `--markdown-only` flags have been removed. The
|
|
18
|
+
* programmatic `generateArticle()` API still accepts `langs` and
|
|
19
|
+
* `markdownOnly` for tests that need to scope a render for speed.
|
|
20
|
+
*
|
|
16
21
|
* Designed to be idempotent: running again with no changes overwrites
|
|
17
22
|
* identical files byte-for-byte.
|
|
18
23
|
*/
|
|
@@ -24,7 +29,7 @@ import { resolveRunId as _resolveRunId } from './manifest/index.js';
|
|
|
24
29
|
import { resolveArticleMetadata, extractStrongProseLine, } from './article-metadata.js';
|
|
25
30
|
import { buildArticleMeta, serializeArticleMeta } from './article-meta.js';
|
|
26
31
|
import { renderMarkdown } from './markdown-renderer.js';
|
|
27
|
-
import { wrapArticleHtml, getArticleFilename } from './article-html.js';
|
|
32
|
+
import { wrapArticleHtml, getArticleFilename, localizeArticleBody } from './article-html.js';
|
|
28
33
|
import { buildReaderIntelligenceGuideHtml, stripInlineReaderGuide, } from './reader-intelligence-guide.js';
|
|
29
34
|
import { ALL_LANGUAGES } from '../constants/language-core.js';
|
|
30
35
|
import { blobUrl } from './infra/github-urls.js';
|
|
@@ -48,9 +53,6 @@ function applyFlagResult(acc, result) {
|
|
|
48
53
|
case 'since':
|
|
49
54
|
acc.since = result.value;
|
|
50
55
|
return;
|
|
51
|
-
case 'lang':
|
|
52
|
-
acc.langs.push(result.value);
|
|
53
|
-
return;
|
|
54
56
|
case 'outDir':
|
|
55
57
|
acc.outDir = result.value;
|
|
56
58
|
return;
|
|
@@ -60,9 +62,6 @@ function applyFlagResult(acc, result) {
|
|
|
60
62
|
case 'description':
|
|
61
63
|
acc.description = result.value;
|
|
62
64
|
return;
|
|
63
|
-
case 'markdownOnly':
|
|
64
|
-
acc.markdownOnly = true;
|
|
65
|
-
return;
|
|
66
65
|
default: {
|
|
67
66
|
// Exhaustiveness guard — if a new FlagResult kind is added without a
|
|
68
67
|
// matching case the compiler will surface the gap.
|
|
@@ -75,9 +74,7 @@ export function parseCliArgs(argv, repoRoot) {
|
|
|
75
74
|
const acc = {
|
|
76
75
|
runDir: null,
|
|
77
76
|
all: false,
|
|
78
|
-
langs: [],
|
|
79
77
|
outDir: path.join(repoRoot, 'news'),
|
|
80
|
-
markdownOnly: false,
|
|
81
78
|
};
|
|
82
79
|
for (let i = 0; i < argv.length; i++) {
|
|
83
80
|
const arg = argv[i] ?? '';
|
|
@@ -105,10 +102,14 @@ export function parseCliArgs(argv, repoRoot) {
|
|
|
105
102
|
const opts = {
|
|
106
103
|
runDir: acc.runDir,
|
|
107
104
|
all: acc.all,
|
|
108
|
-
|
|
105
|
+
// Always render every language — the `--lang/--language` flags have
|
|
106
|
+
// been removed in the always-14-languages contract.
|
|
107
|
+
langs: [...ALL_LANGUAGES],
|
|
109
108
|
outDir: acc.outDir,
|
|
110
109
|
repoRoot,
|
|
111
|
-
|
|
110
|
+
// Always emit HTML — the `--markdown-only` flag has been removed in
|
|
111
|
+
// the always-HTML contract.
|
|
112
|
+
markdownOnly: false,
|
|
112
113
|
...(acc.since !== undefined ? { since: acc.since } : {}),
|
|
113
114
|
...(acc.title !== undefined ? { title: acc.title } : {}),
|
|
114
115
|
...(acc.description !== undefined ? { description: acc.description } : {}),
|
|
@@ -117,7 +118,7 @@ export function parseCliArgs(argv, repoRoot) {
|
|
|
117
118
|
}
|
|
118
119
|
/**
|
|
119
120
|
* Resolve one CLI flag to a {@link FlagResult}. Throws `Error` for any
|
|
120
|
-
* unsupported flag
|
|
121
|
+
* unsupported flag.
|
|
121
122
|
*
|
|
122
123
|
* @param flag - Flag name (e.g. `--run`)
|
|
123
124
|
* @param takeValue - Lazily returns the value argument for value-bearing flags
|
|
@@ -137,14 +138,6 @@ function applyCliFlag(flag, takeValue) {
|
|
|
137
138
|
}
|
|
138
139
|
return { kind: 'since', value };
|
|
139
140
|
}
|
|
140
|
-
case '--lang':
|
|
141
|
-
case '--language': {
|
|
142
|
-
const value = takeValue();
|
|
143
|
-
if (!ALL_LANGUAGES.includes(value)) {
|
|
144
|
-
throw new Error(`Unsupported language code: ${value}`);
|
|
145
|
-
}
|
|
146
|
-
return { kind: 'lang', value: value };
|
|
147
|
-
}
|
|
148
141
|
case '--out-dir':
|
|
149
142
|
case '--output':
|
|
150
143
|
return { kind: 'outDir', value: path.resolve(takeValue()) };
|
|
@@ -152,13 +145,19 @@ function applyCliFlag(flag, takeValue) {
|
|
|
152
145
|
return { kind: 'title', value: takeValue() };
|
|
153
146
|
case '--description':
|
|
154
147
|
return { kind: 'description', value: takeValue() };
|
|
155
|
-
case '--markdown-only':
|
|
156
|
-
return { kind: 'markdownOnly' };
|
|
157
148
|
case '--help':
|
|
158
149
|
case '-h':
|
|
159
150
|
printHelp();
|
|
160
151
|
process.exit(0);
|
|
161
152
|
// eslint-disable-next-line no-fallthrough
|
|
153
|
+
case '--lang':
|
|
154
|
+
case '--language':
|
|
155
|
+
case '--markdown-only':
|
|
156
|
+
// Removed in the always-14-languages-always-HTML contract — every
|
|
157
|
+
// article.md now always renders to all 14 supported languages and
|
|
158
|
+
// HTML emission cannot be skipped from the CLI.
|
|
159
|
+
throw new Error(`Flag ${flag} has been removed. The CLI always renders all 14 languages with HTML output. ` +
|
|
160
|
+
`See Article-Generation.md § "CLI contract" for the new always-on contract.`);
|
|
162
161
|
default:
|
|
163
162
|
throw new Error(`Unknown argument: ${flag}`);
|
|
164
163
|
}
|
|
@@ -183,19 +182,22 @@ function printHelp() {
|
|
|
183
182
|
' generate-article --all [--since YYYY-MM-DD] [options]',
|
|
184
183
|
'',
|
|
185
184
|
'Aggregate analysis artifacts from an `analysis/daily/**/<run>` directory',
|
|
186
|
-
'into a canonical Markdown document and render it to HTML in all 14',
|
|
187
|
-
'languages
|
|
188
|
-
'
|
|
185
|
+
'into a canonical Markdown document and render it to HTML in **all 14',
|
|
186
|
+
'supported languages** (en, sv, da, no, fi, de, fr, es, nl, ar, he, ja,',
|
|
187
|
+
'ko, zh). The `--all` form walks every run under `analysis/daily/` and',
|
|
188
|
+
'regenerates the full historic catalogue in one pass.',
|
|
189
|
+
'',
|
|
190
|
+
'The 14-language HTML render is **always on** — there is no flag to',
|
|
191
|
+
'scope a render to a single language or to skip HTML emission. Every',
|
|
192
|
+
'article.md always produces 14 corresponding `<slug>-<lang>.html` files.',
|
|
189
193
|
'',
|
|
190
194
|
'Options:',
|
|
191
195
|
' --run <path> Analysis run directory (single-run mode)',
|
|
192
196
|
' --all Batch-regenerate every run under analysis/daily/',
|
|
193
197
|
' --since YYYY-MM-DD With --all: skip runs dated before this cut-off',
|
|
194
|
-
' --lang <code> Language to render (repeatable; default: all 14)',
|
|
195
198
|
' --out-dir <path> Output directory (default: news/)',
|
|
196
199
|
' --title <text> Override article title (single-run only)',
|
|
197
200
|
' --description <text> Override article meta description (single-run only)',
|
|
198
|
-
' --markdown-only Write only the source .md (skip HTML)',
|
|
199
201
|
' --help, -h Show this help',
|
|
200
202
|
'',
|
|
201
203
|
].join('\n'));
|
|
@@ -347,13 +349,21 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
|
|
|
347
349
|
bodyHtml = bodyHtml.replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/, '');
|
|
348
350
|
const guideHtml = buildReaderIntelligenceGuideHtml(lang, aggregated.sectionToc, aggregated.includedArtifacts);
|
|
349
351
|
if (guideHtml) {
|
|
350
|
-
//
|
|
351
|
-
// the rendered
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
352
|
+
// Insert the guide IMMEDIATELY AFTER the Executive Brief section so
|
|
353
|
+
// the rendered HTML body order matches the documented article
|
|
354
|
+
// skeleton (Article-Generation.md §"Article skeleton"):
|
|
355
|
+
// 1. Executive Brief
|
|
356
|
+
// 2. Reader Intelligence Guide
|
|
357
|
+
// 3. Key Takeaways
|
|
358
|
+
// 4. … deep sections
|
|
359
|
+
// We splice at the start of the next H2 after the Executive Brief
|
|
360
|
+
// anchor; when the brief is missing (sparse runs) we fall back to
|
|
361
|
+
// prepending so the guide still appears at the top of the body.
|
|
362
|
+
bodyHtml = insertReaderGuideAfterExecutiveBrief(bodyHtml, guideHtml);
|
|
356
363
|
}
|
|
364
|
+
// Localize Tradecraft References, Analysis Index, and other appendix
|
|
365
|
+
// section headings and content into the target language.
|
|
366
|
+
bodyHtml = localizeArticleBody(bodyHtml, lang);
|
|
357
367
|
// When a per-language translated source exists, prefer a summary derived
|
|
358
368
|
// from it so the `<meta description>` matches the visible prose. The
|
|
359
369
|
// editorial title still comes from the English resolver (per-language
|
|
@@ -380,6 +390,60 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
|
|
|
380
390
|
fs.writeFileSync(path.join(opts.outDir, filename), html, 'utf8');
|
|
381
391
|
return filename;
|
|
382
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Insert the regenerated Reader Intelligence Guide HTML immediately after
|
|
395
|
+
* the Executive Brief section so the rendered article body matches the
|
|
396
|
+
* documented order (Executive Brief → Reader Intelligence Guide → Key
|
|
397
|
+
* Takeaways → deep sections). The Executive Brief section ends where the
|
|
398
|
+
* next H2 begins; we splice at that boundary. When the brief is absent
|
|
399
|
+
* (sparse runs) we fall back to prepending so the guide still appears
|
|
400
|
+
* at the top of the body.
|
|
401
|
+
*
|
|
402
|
+
* Implementation uses `indexOf` rather than a regex so the splice point
|
|
403
|
+
* is deterministic and immune to polynomial-regex backtracking on
|
|
404
|
+
* pathological input.
|
|
405
|
+
*
|
|
406
|
+
* @param bodyHtml - Rendered article body
|
|
407
|
+
* @param guideHtml - Reader Intelligence Guide HTML fragment
|
|
408
|
+
* @returns Body HTML with the guide spliced after the Executive Brief
|
|
409
|
+
*/
|
|
410
|
+
export function insertReaderGuideAfterExecutiveBrief(bodyHtml, guideHtml) {
|
|
411
|
+
const execBriefAnchor = 'id="section-executive-brief"';
|
|
412
|
+
const briefIdx = bodyHtml.indexOf(execBriefAnchor);
|
|
413
|
+
if (briefIdx === -1) {
|
|
414
|
+
return guideHtml + '\n' + bodyHtml;
|
|
415
|
+
}
|
|
416
|
+
// Skip the Executive Brief opening tag itself, then walk forward to the
|
|
417
|
+
// next H2 — that's where the next section starts and where we want to
|
|
418
|
+
// splice the guide. `<h2 ` matches a tag with attributes; `<h2>` matches
|
|
419
|
+
// a bare tag (defensive).
|
|
420
|
+
const afterBrief = briefIdx + execBriefAnchor.length;
|
|
421
|
+
const nextH2Tagged = bodyHtml.indexOf('<h2 ', afterBrief);
|
|
422
|
+
const nextH2Bare = bodyHtml.indexOf('<h2>', afterBrief);
|
|
423
|
+
const nextH2 = pickEarliestIndex(nextH2Tagged, nextH2Bare);
|
|
424
|
+
if (nextH2 === -1) {
|
|
425
|
+
// Executive Brief is the only section — append the guide at the end.
|
|
426
|
+
return bodyHtml + '\n' + guideHtml;
|
|
427
|
+
}
|
|
428
|
+
return bodyHtml.slice(0, nextH2) + guideHtml + '\n' + bodyHtml.slice(nextH2);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Return the smaller of two `indexOf` results, treating `-1` as "not
|
|
432
|
+
* found" so the caller gets `-1` only when both probes failed. Extracted
|
|
433
|
+
* to keep {@link insertReaderGuideAfterExecutiveBrief} under the
|
|
434
|
+
* useless-assignment lint.
|
|
435
|
+
*
|
|
436
|
+
* @param a - First `indexOf` result
|
|
437
|
+
* @param b - Second `indexOf` result
|
|
438
|
+
* @returns Smaller non-negative index, or `-1` when both are `-1`
|
|
439
|
+
*/
|
|
440
|
+
function pickEarliestIndex(a, b) {
|
|
441
|
+
if (a === -1)
|
|
442
|
+
return b;
|
|
443
|
+
if (b === -1)
|
|
444
|
+
return a;
|
|
445
|
+
return Math.min(a, b);
|
|
446
|
+
}
|
|
383
447
|
/**
|
|
384
448
|
* Safely look up one language entry in a {@link ResolvedMetadata} map.
|
|
385
449
|
* The runtime shape is always complete (one entry per language), but the
|
|
@@ -84,6 +84,16 @@ export declare function buildArticleHreflangLinks(articleSlug: string): string;
|
|
|
84
84
|
* @returns HTML fragment for the sidebar, or `""` when no TOC is needed
|
|
85
85
|
*/
|
|
86
86
|
export declare function buildArticleToc(entries: readonly ArticleTocEntry[], lang: LanguageCode): string;
|
|
87
|
+
/**
|
|
88
|
+
* Localize the Tradecraft References and Analysis Index sections in the
|
|
89
|
+
* rendered article body HTML. Replaces English headings, introductions,
|
|
90
|
+
* sub-headings, and table headers with translated equivalents.
|
|
91
|
+
*
|
|
92
|
+
* @param bodyHtml - The rendered HTML body (from Markdown)
|
|
93
|
+
* @param lang - Target language code
|
|
94
|
+
* @returns HTML body with localized appendix sections
|
|
95
|
+
*/
|
|
96
|
+
export declare function localizeArticleBody(bodyHtml: string, lang: LanguageCode): string;
|
|
87
97
|
/**
|
|
88
98
|
* Render the full article HTML document with the shared chrome.
|
|
89
99
|
*
|