@tricoteuses/senat 3.1.24 → 3.2.0

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 (35) hide show
  1. package/lib/src/index.d.ts +1 -0
  2. package/lib/src/other_types/plf.d.ts +21 -0
  3. package/lib/src/other_types/plf.js +1 -0
  4. package/lib/src/parsers/plf/credits.d.ts +2 -0
  5. package/lib/src/parsers/plf/credits.js +171 -0
  6. package/lib/src/parsers/plf/index.d.ts +1 -0
  7. package/lib/src/parsers/plf/index.js +1 -0
  8. package/lib/src/parsers/plf/parser_utils.d.ts +18 -0
  9. package/lib/src/parsers/plf/parser_utils.js +172 -0
  10. package/lib/src/parsers/texte.d.ts +1 -3
  11. package/lib/src/parsers/texte.js +63 -30
  12. package/lib/src/scripts/extract_plf_fixtures.d.ts +1 -0
  13. package/lib/src/scripts/extract_plf_fixtures.js +101 -0
  14. package/lib/src/scripts/retrieve_documents.js +7 -46
  15. package/lib/src/server/parse_document.d.ts +5 -0
  16. package/lib/src/server/parse_document.js +47 -0
  17. package/lib/tests/parsers/parseDocument.test.d.ts +1 -0
  18. package/lib/tests/parsers/parseDocument.test.js +73 -0
  19. package/lib/tests/parsers/plf/credits.integration.test.d.ts +1 -0
  20. package/lib/tests/parsers/plf/credits.integration.test.js +118 -0
  21. package/lib/tests/parsers/plf/credits.test.d.ts +1 -0
  22. package/lib/tests/parsers/plf/credits.test.js +467 -0
  23. package/lib/tests/parsers/plf/integration.test_utils.d.ts +8 -0
  24. package/lib/tests/parsers/plf/integration.test_utils.js +73 -0
  25. package/lib/tests/parsers/plf/test_utils.d.ts +20 -0
  26. package/lib/tests/parsers/plf/test_utils.js +85 -0
  27. package/lib/tests/parsers/texte.test.d.ts +1 -0
  28. package/lib/tests/parsers/texte.test.js +71 -0
  29. package/lib/tests/parsers/texte.test_utils.d.ts +15 -0
  30. package/lib/tests/parsers/texte.test_utils.js +73 -0
  31. package/lib/tests/plf/credits.test.d.ts +1 -0
  32. package/lib/tests/plf/credits.test.js +458 -0
  33. package/lib/tests/plf/test_utils.d.ts +20 -0
  34. package/lib/tests/plf/test_utils.js +85 -0
  35. package/package.json +3 -1
@@ -0,0 +1,85 @@
1
+ import { JSDOM } from "jsdom";
2
+ export function parseXml(xml) {
3
+ return new JSDOM(xml, { contentType: "text/xml" }).window.document;
4
+ }
5
+ export function xmlDoc(body) {
6
+ return `<?xml version="1.0" encoding="UTF-8"?>
7
+ <akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0" xmlns:data="http://data.parlement.fr/v1">
8
+ <bill>
9
+ <body>${body}</body>
10
+ </bill>
11
+ </akomaNtoso>`;
12
+ }
13
+ export function buildArticle(etatId, num, alineas) {
14
+ return `<article class="etat" eId="${etatId}"><num>${num}</num>${alineas}</article>`;
15
+ }
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers to build mission / programme / lgncred / total rows (format classé)
18
+ // ---------------------------------------------------------------------------
19
+ /* eslint-disable max-len */
20
+ export function missionRow(libelle, ae, cp) {
21
+ return `<tr class="mission"><td class="libelle" style="font-weight: bold;border-top:1px solid black"><p>${libelle}</p></td><td class="engagement" style="font-weight: bold"><p>${ae}</p></td><td class="credit" style="font-weight: bold"><p>${cp}</p></td></tr>`;
22
+ }
23
+ export function missionRow5(libelle, ae, cp, aeAnnule, cpAnnule) {
24
+ return `<tr class="mission"><td class="libelle" style="font-weight: bold;border-top:1px solid black"><p>${libelle}</p></td><td class="engagement" style="font-weight: bold"><p>${ae}</p></td><td class="credit" style="font-weight: bold"><p>${cp}</p></td><td style="font-weight: bold"><p>${aeAnnule}</p></td><td style="font-weight: bold"><p>${cpAnnule}</p></td></tr>`;
25
+ }
26
+ export function programmeRow(libelle, ae, cp) {
27
+ return `<tr class="programme"><td class="libelle" style="border-top:0px solid black"><p>${libelle}</p></td><td class="engagement"><p>${ae}</p></td><td class="credit"><p>${cp}</p></td></tr>`;
28
+ }
29
+ export function programmeRow5(libelle, ae, cp, aeAnnule, cpAnnule) {
30
+ return `<tr class="programme"><td class="libelle" style="border-top:0px solid black"><p>${libelle}</p></td><td class="engagement"><p>${ae}</p></td><td class="credit"><p>${cp}</p></td><td><p>${aeAnnule}</p></td><td><p>${cpAnnule}</p></td></tr>`;
31
+ }
32
+ export function lgncredRow(libelle, ae, cp) {
33
+ return `<tr class="lgncred"><td class="libelle" style="font-style: italic;border-top:0px solid black"><p>${libelle}</p></td><td class="engagement" style="font-style: italic"><p>${ae}</p></td><td class="credit" style="font-style: italic"><p>${cp}</p></td></tr>`;
34
+ }
35
+ export function lgncredRow5(libelle, ae, cp, aeAnnule, cpAnnule) {
36
+ return `<tr class="lgncred"><td class="libelle" style="font-style: italic;border-top:0px solid black"><p>${libelle}</p></td><td class="engagement" style="font-style: italic"><p>${ae}</p></td><td class="credit" style="font-style: italic"><p>${cp}</p></td><td style="font-style: italic"><p>${aeAnnule}</p></td><td style="font-style: italic"><p>${cpAnnule}</p></td></tr>`;
37
+ }
38
+ export function totalRow(ae, cp) {
39
+ return `<tr class="total"><td style="font-weight: bold;border-top:1px solid black;border-bottom:1px solid black"><p>Total</p></td><td style="font-weight: bold"><p>${ae}</p></td><td style="font-weight: bold"><p>${cp}</p></td></tr>`;
40
+ }
41
+ export function totalRow5(ae, cp, aeAnnule, cpAnnule) {
42
+ return `<tr class="total"><td style="font-weight: bold;border-top:1px solid black;border-bottom:1px solid black"><p>Total</p></td><td style="font-weight: bold"><p>${ae}</p></td><td style="font-weight: bold"><p>${cp}</p></td><td style="font-weight: bold"><p>${aeAnnule}</p></td><td style="font-weight: bold"><p>${cpAnnule}</p></td></tr>`;
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // Helpers for format non-classé (style only)
46
+ // ---------------------------------------------------------------------------
47
+ export function styledMissionRow(libelle, ae, cp) {
48
+ return `<tr><td style="font-weight: bold;border-top:1px solid black"><p>${libelle}</p></td><td style="font-weight: bold"><p>${ae}</p></td><td style="font-weight: bold"><p>${cp}</p></td></tr>`;
49
+ }
50
+ export function styledMissionRow5(libelle, ae, cp, aeAnnule, cpAnnule) {
51
+ return `<tr><td style="font-weight: bold;border-top:1px solid black"><p>${libelle}</p></td><td style="font-weight: bold"><p>${ae}</p></td><td style="font-weight: bold"><p>${cp}</p></td><td style="font-weight: bold"><p>${aeAnnule}</p></td><td style="font-weight: bold"><p>${cpAnnule}</p></td></tr>`;
52
+ }
53
+ export function styledProgrammeRow(libelle, ae, cp) {
54
+ return `<tr><td style="border-top:0px solid black"><p>${libelle}</p></td><td><p>${ae}</p></td><td><p>${cp}</p></td></tr>`;
55
+ }
56
+ export function styledProgrammeRow5(libelle, ae, cp, aeAnnule, cpAnnule) {
57
+ return `<tr><td style="border-top:0px solid black"><p>${libelle}</p></td><td><p>${ae}</p></td><td><p>${cp}</p></td><td><p>${aeAnnule}</p></td><td><p>${cpAnnule}</p></td></tr>`;
58
+ }
59
+ export function styledDontTitreRow(ae, cp) {
60
+ return `<tr><td style="font-style: italic;border-top:0px solid black"><p>Dont titre 2</p></td><td style="font-style: italic"><p>${ae}</p></td><td style="font-style: italic"><p>${cp}</p></td></tr>`;
61
+ }
62
+ export function styledTotalRow(ae, cp) {
63
+ return `<tr><td style="font-weight: bold;border-top:1px solid black;border-bottom:1px solid black"><p>Total</p></td><td style="font-weight: bold"><p>${ae}</p></td><td style="font-weight: bold"><p>${cp}</p></td></tr>`;
64
+ }
65
+ export function styledTotalRow5(ae, cp, aeAnnule, cpAnnule) {
66
+ return `<tr><td style="font-weight: bold;border-top:1px solid black;border-bottom:1px solid black"><p>Total</p></td><td style="font-weight: bold"><p>${ae}</p></td><td style="font-weight: bold"><p>${cp}</p></td><td style="font-weight: bold"><p>${aeAnnule}</p></td><td style="font-weight: bold"><p>${cpAnnule}</p></td></tr>`;
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Build table with header (3 or 5 columns)
70
+ // ---------------------------------------------------------------------------
71
+ export function etatTable3Col(dataCode, rows) {
72
+ return `<alinea><content><table class="alinea-table etat-tableau" data:code="${dataCode}">
73
+ <tr><td colspan="3" style="font-style: italic;border-bottom:1px solid black"><p>(En euros)</p></td></tr>
74
+ <tr><td style="font-weight: bold;border-top:1px solid black;border-bottom:1px solid black"><p>Mission / Programme</p></td><td style="font-weight: bold;border-bottom:1px solid black"><p>Autorisations d'engagement</p></td><td style="font-weight: bold;border-bottom:1px solid black"><p>Crédits de paiement</p></td></tr>
75
+ ${rows}
76
+ </table></content></alinea>`;
77
+ }
78
+ export function etatTable5Col(dataCode, rows) {
79
+ return `<alinea><content><table class="alinea-table etat-tableau" data:code="${dataCode}">
80
+ <tr><td colspan="5" style="font-style: italic;border-bottom:1px solid black"><p>(En euros)</p></td></tr>
81
+ <tr><td style="font-weight: bold;border-top:1px solid black;border-bottom:1px solid black"><p>Mission / Programme</p></td><td style="font-weight: bold;border-bottom:1px solid black"><p>AE ouvertes</p></td><td style="font-weight: bold;border-bottom:1px solid black"><p>CP ouverts</p></td><td style="font-weight: bold;border-bottom:1px solid black"><p>AE annulées</p></td><td style="font-weight: bold;border-bottom:1px solid black"><p>CP annulés</p></td></tr>
82
+ ${rows}
83
+ </table></content></alinea>`;
84
+ }
85
+ /* eslint-enable max-len */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { article, articleAlinea, articleAlineaEIds, divisionalAlinea, findArticle, findDivision, parseDoc, part, title, } from "./texte.test_utils.js";
3
+ // Realistic fragments taken from the PLF 2026 (pjl25-138): the divisional
4
+ // alineas "I. – IMPÔTS ET RESSOURCES AUTORISÉS", "A. – Autorisation de
5
+ // perception des impôts et produits" and "B. – Mesures fiscales" are structural
6
+ // headings of TITRE PREMIER (carried by alinea eIds prefixed with `aldiv_`),
7
+ // and must NOT be attached to the surrounding articles.
8
+ const articleLiminaire = article("art_liminaire", articleAlinea("al_1__art_liminaire", "<p>Prévisions de solde structurel.</p>", { pastille: "1" }));
9
+ const articlePremier = article("art_1er", articleAlinea("al_1__art_1er", "<p>I. – La perception des ressources de l’État est autorisée.</p>", {
10
+ pastille: "1",
11
+ num: "I. –",
12
+ }));
13
+ const articleDeux = article("art_2", articleAlinea("al_1__art_2", "<p>Dispositions fiscales.</p>", { pastille: "1" }));
14
+ const titrePremier = title("title_PREMIER__part_PREMIÈRE", "TITRE PREMIER", "DISPOSITIONS RELATIVES AUX RESSOURCES", divisionalAlinea("aldiv_I__title_PREMIER__part_PREMIÈRE", "I. –", "I. – IMPÔTS ET RESSOURCES AUTORISÉS") +
15
+ divisionalAlinea("aldiv_A__aldiv_I__title_PREMIER__part_PREMIÈRE", "A. –", "A. – Autorisation de perception des impôts et produits") +
16
+ articlePremier +
17
+ divisionalAlinea("aldiv_B__title_PREMIER__part_PREMIÈRE", "B. –", "B. – Mesures fiscales") +
18
+ articleDeux);
19
+ const partiePremiere = part("part_PREMIÈRE", "PARTIE PREMIÈRE", "CONDITIONS GÉNÉRALES DE L’ÉQUILIBRE FINANCIER", titrePremier);
20
+ const BODY = articleLiminaire + partiePremiere;
21
+ describe("parseTexte", () => {
22
+ it("keeps divisional alineas (aldiv) out of the surrounding articles", () => {
23
+ const flatTexte = parseDoc(BODY);
24
+ expect(articleAlineaEIds(flatTexte, "art_liminaire")).toEqual(["al_1__art_liminaire"]);
25
+ expect(articleAlineaEIds(flatTexte, "art_1er")).toEqual(["al_1__art_1er"]);
26
+ expect(articleAlineaEIds(flatTexte, "art_2")).toEqual(["al_1__art_2"]);
27
+ });
28
+ it("emits divisional alineas as their own division entries in document order", () => {
29
+ const flatTexte = parseDoc(BODY);
30
+ const eIds = flatTexte.divisions
31
+ .map((division) => division.eId)
32
+ .filter((eId) => !eId.startsWith("al_"));
33
+ expect(eIds).toEqual([
34
+ "art_liminaire",
35
+ "part_PREMIÈRE",
36
+ "title_PREMIER__part_PREMIÈRE",
37
+ "aldiv_I__title_PREMIER__part_PREMIÈRE",
38
+ "aldiv_A__aldiv_I__title_PREMIER__part_PREMIÈRE",
39
+ "art_1er",
40
+ "aldiv_B__title_PREMIER__part_PREMIÈRE",
41
+ "art_2",
42
+ ]);
43
+ });
44
+ it("keeps divisional alinea headings (num + text)", () => {
45
+ const flatTexte = parseDoc(BODY);
46
+ const divI = findDivision(flatTexte, "aldiv_I__title_PREMIER__part_PREMIÈRE");
47
+ expect(divI.tag).toBe("alinea");
48
+ expect(divI.headings.map((heading) => heading.text)).toEqual([
49
+ "I. –",
50
+ "I. – IMPÔTS ET RESSOURCES AUTORISÉS",
51
+ ]);
52
+ const divB = findDivision(flatTexte, "aldiv_B__title_PREMIER__part_PREMIÈRE");
53
+ expect(divB.headings.map((heading) => heading.text)).toEqual(["B. –", "B. – Mesures fiscales"]);
54
+ });
55
+ it("normalizes double-escaped <p> blocks in alinea content", () => {
56
+ // The Sénat XML double-escapes inner block markup (e.g. the footnote block
57
+ // "Les chiffres en comptabilité nationale…" in the article liminaire):
58
+ // a single <td>/<p> whose text is literally "<p>...</p><p>...</p>".
59
+ const note = "<p>&lt;p&gt;Les chiffres en comptabilité nationale.&lt;/p&gt;" +
60
+ "&lt;p&gt;Le scénario potentiel.&lt;/p&gt;" +
61
+ "&lt;p&gt;(1) À champ constant.&lt;/p&gt;</p>";
62
+ const flatTexte = parseDoc(article("art_22", articleAlinea("al_1__art_22", note)));
63
+ const alinea = findArticle(flatTexte, "art_22").alineas[0];
64
+ expect(alinea.html).not.toContain("&lt;");
65
+ expect(alinea.html).toContain("<p>Les chiffres en comptabilité nationale.</p>");
66
+ expect(alinea.html).toContain("<p>(1) À champ constant.</p>");
67
+ expect(alinea.text).not.toContain("<p");
68
+ expect(alinea.text).toContain("Les chiffres en comptabilité nationale.");
69
+ expect(alinea.text).toContain("(1) À champ constant.");
70
+ });
71
+ });
@@ -0,0 +1,15 @@
1
+ import { Article, FlatTexte } from "../../src/other_types/texte.js";
2
+ export declare function xmlDoc(body: string): string;
3
+ export declare function parseDoc(body: string): FlatTexte;
4
+ export declare function articleAlinea(eId: string, content: string, options?: {
5
+ pastille?: string | null;
6
+ num?: string;
7
+ }): string;
8
+ export declare function divisionalAlinea(eId: string, num: string, text: string): string;
9
+ export declare function article(eId: string, children: string): string;
10
+ export declare function structuralDivision(tag: string, eId: string, num: string, heading: string, children: string): string;
11
+ export declare function title(eId: string, num: string, heading: string, children: string): string;
12
+ export declare function part(eId: string, num: string, heading: string, children: string): string;
13
+ export declare function findDivision(flatTexte: FlatTexte, eId: string): import("../../src/other_types/texte.js").Division | undefined;
14
+ export declare function findArticle(flatTexte: FlatTexte, eId: string): Article | undefined;
15
+ export declare function articleAlineaEIds(flatTexte: FlatTexte, eId: string): string[];
@@ -0,0 +1,73 @@
1
+ import { parseTexte } from "../../src/parsers/texte.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Document wrapper
4
+ // ---------------------------------------------------------------------------
5
+ export function xmlDoc(body) {
6
+ return `<?xml version="1.0" encoding="UTF-8"?>
7
+ <akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0" xmlns:data="http://data.parlement.fr/v1">
8
+ <meta>
9
+ <identification>
10
+ <FRBRExpression>
11
+ <FRBRuri value="/akn/fr/senat/dossier/pjlf2026/2025-11-24/depot/pjl25-138"/>
12
+ </FRBRExpression>
13
+ </identification>
14
+ </meta>
15
+ <preamble>
16
+ <docTitle>Projet de loi de finances pour 2026</docTitle>
17
+ </preamble>
18
+ <body>${body}</body>
19
+ </akomaNtoso>`;
20
+ }
21
+ export function parseDoc(body) {
22
+ return parseTexte(xmlDoc(body));
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // Article alineas (the actual paragraphs of an article)
26
+ // ---------------------------------------------------------------------------
27
+ export function articleAlinea(eId, content, options = {}) {
28
+ const num = options.num ? `<num>${options.num}</num>` : "";
29
+ const pastille = options.pastille != null ? ` data:pastille="${options.pastille}"` : "";
30
+ return `<alinea eId="${eId}"${pastille}>${num}<content>${content}</content></alinea>`;
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Divisional alineas (structural headings of a title/part, eId prefix aldiv_)
34
+ // ---------------------------------------------------------------------------
35
+ export function divisionalAlinea(eId, num, text) {
36
+ return `<alinea eId="${eId}">
37
+ <num>${num}</num>
38
+ <content>
39
+ <p>${text}</p>
40
+ </content>
41
+ </alinea>`;
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Articles and structural subdivisions
45
+ // ---------------------------------------------------------------------------
46
+ export function article(eId, children) {
47
+ return `<article eId="${eId}">${children}</article>`;
48
+ }
49
+ export function structuralDivision(tag, eId, num, heading, children) {
50
+ return `<${tag} eId="${eId}">
51
+ <num>${num}</num>
52
+ <heading>${heading}</heading>
53
+ ${children}
54
+ </${tag}>`;
55
+ }
56
+ export function title(eId, num, heading, children) {
57
+ return structuralDivision("title", eId, num, heading, children);
58
+ }
59
+ export function part(eId, num, heading, children) {
60
+ return structuralDivision("part", eId, num, heading, children);
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Assertions helpers
64
+ // ---------------------------------------------------------------------------
65
+ export function findDivision(flatTexte, eId) {
66
+ return flatTexte.divisions.find((division) => division.eId === eId);
67
+ }
68
+ export function findArticle(flatTexte, eId) {
69
+ return flatTexte.divisions.find((division) => division.tag === "article" && division.eId === eId);
70
+ }
71
+ export function articleAlineaEIds(flatTexte, eId) {
72
+ return (findArticle(flatTexte, eId)?.alineas ?? []).map((alinea) => alinea.eId);
73
+ }
@@ -0,0 +1 @@
1
+ export {};