@tricoteuses/senat 3.1.1 → 3.1.3

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 (86) hide show
  1. package/lib/src/rich_types/dosleg.js +13 -3
  2. package/lib/src/rich_types/sens.d.ts +2 -0
  3. package/lib/src/scripts/retrieve_open_data.js +4 -2
  4. package/lib/src/scripts/shared/make_generate_zod_schemas.js +6 -2
  5. package/lib/src/server/databases_postgres.js +2 -1
  6. package/lib/src/server/documents.js +2 -2
  7. package/lib/src/server/dosleg.js +2 -2
  8. package/lib/src/server/sens.js +70 -0
  9. package/lib/src/utils/reunion_parsing.js +1 -1
  10. package/package.json +3 -1
  11. package/lib/src/config.d.ts +0 -43
  12. package/lib/src/config.js +0 -37
  13. package/lib/src/conversion_textes.d.ts +0 -11
  14. package/lib/src/conversion_textes.js +0 -320
  15. package/lib/src/databases_postgres.d.ts +0 -4
  16. package/lib/src/databases_postgres.js +0 -23
  17. package/lib/src/datasets.d.ts +0 -38
  18. package/lib/src/datasets.js +0 -247
  19. package/lib/src/git.d.ts +0 -27
  20. package/lib/src/git.js +0 -251
  21. package/lib/src/loaders.d.ts +0 -52
  22. package/lib/src/loaders.js +0 -260
  23. package/lib/src/model/agenda.d.ts +0 -6
  24. package/lib/src/model/agenda.js +0 -148
  25. package/lib/src/model/ameli.d.ts +0 -67
  26. package/lib/src/model/ameli.js +0 -150
  27. package/lib/src/model/commission.d.ts +0 -19
  28. package/lib/src/model/commission.js +0 -269
  29. package/lib/src/model/debats.d.ts +0 -39
  30. package/lib/src/model/debats.js +0 -112
  31. package/lib/src/model/documents.d.ts +0 -32
  32. package/lib/src/model/documents.js +0 -182
  33. package/lib/src/model/dosleg.d.ts +0 -144
  34. package/lib/src/model/dosleg.js +0 -468
  35. package/lib/src/model/index.d.ts +0 -7
  36. package/lib/src/model/index.js +0 -7
  37. package/lib/src/model/questions.d.ts +0 -54
  38. package/lib/src/model/questions.js +0 -91
  39. package/lib/src/model/scrutins.d.ts +0 -48
  40. package/lib/src/model/scrutins.js +0 -121
  41. package/lib/src/model/seance.d.ts +0 -3
  42. package/lib/src/model/seance.js +0 -267
  43. package/lib/src/model/sens.d.ts +0 -112
  44. package/lib/src/model/sens.js +0 -385
  45. package/lib/src/model/util.d.ts +0 -1
  46. package/lib/src/model/util.js +0 -15
  47. package/lib/src/raw_types/ameli.d.ts +0 -1762
  48. package/lib/src/raw_types/ameli.js +0 -1074
  49. package/lib/src/raw_types/debats.d.ts +0 -380
  50. package/lib/src/raw_types/debats.js +0 -266
  51. package/lib/src/raw_types/dosleg.d.ts +0 -2954
  52. package/lib/src/raw_types/dosleg.js +0 -2005
  53. package/lib/src/raw_types/questions.d.ts +0 -699
  54. package/lib/src/raw_types/questions.js +0 -493
  55. package/lib/src/raw_types/sens.d.ts +0 -7843
  56. package/lib/src/raw_types/sens.js +0 -4691
  57. package/lib/src/raw_types_schemats/ameli.d.ts +0 -541
  58. package/lib/src/raw_types_schemats/ameli.js +0 -2
  59. package/lib/src/raw_types_schemats/debats.d.ts +0 -127
  60. package/lib/src/raw_types_schemats/debats.js +0 -2
  61. package/lib/src/raw_types_schemats/dosleg.d.ts +0 -977
  62. package/lib/src/raw_types_schemats/dosleg.js +0 -2
  63. package/lib/src/raw_types_schemats/questions.d.ts +0 -237
  64. package/lib/src/raw_types_schemats/questions.js +0 -2
  65. package/lib/src/raw_types_schemats/sens.d.ts +0 -2709
  66. package/lib/src/raw_types_schemats/sens.js +0 -2
  67. package/lib/src/types/agenda.d.ts +0 -45
  68. package/lib/src/types/agenda.js +0 -1
  69. package/lib/src/types/ameli.d.ts +0 -5
  70. package/lib/src/types/ameli.js +0 -1
  71. package/lib/src/types/compte_rendu.d.ts +0 -83
  72. package/lib/src/types/compte_rendu.js +0 -1
  73. package/lib/src/types/debats.d.ts +0 -2
  74. package/lib/src/types/debats.js +0 -1
  75. package/lib/src/types/dosleg.d.ts +0 -70
  76. package/lib/src/types/dosleg.js +0 -1
  77. package/lib/src/types/questions.d.ts +0 -2
  78. package/lib/src/types/questions.js +0 -1
  79. package/lib/src/types/sens.d.ts +0 -8
  80. package/lib/src/types/sens.js +0 -1
  81. package/lib/src/types/sessions.d.ts +0 -6
  82. package/lib/src/types/sessions.js +0 -19
  83. package/lib/src/types/texte.d.ts +0 -72
  84. package/lib/src/types/texte.js +0 -15
  85. package/lib/src/validators/config.d.ts +0 -9
  86. package/lib/src/validators/config.js +0 -10
@@ -39,7 +39,7 @@ function getDateSortValue(value) {
39
39
  function compareByDate(left, right) {
40
40
  return getDateSortValue(left.date) - getDateSortValue(right.date);
41
41
  }
42
- function getPhasePrefix(lecture, assemblee) {
42
+ function getPhasePrefix(lecture, assemblee, codeNatureDossier) {
43
43
  if (assemblee !== "Sénat")
44
44
  return null;
45
45
  const typeLibelle = (lecture.type_lecture || "").toLowerCase();
@@ -51,6 +51,11 @@ function getPhasePrefix(lecture, assemblee) {
51
51
  return "SNLDEF";
52
52
  if (typeLibelle.includes("unique"))
53
53
  return "SNLUNI";
54
+ // Les propositions de résolution (ppr) suivent la procédure de lecture unique
55
+ // (art. 73 quinquies du Règlement du Sénat). Les données brutes encodent leur
56
+ // type_lecture comme "Première lecture", mais c'est toujours une lecture unique.
57
+ if (codeNatureDossier === "ppr")
58
+ return "SNLUNI";
54
59
  if (lecture.ordre_lecture) {
55
60
  return `SN${lecture.ordre_lecture}`;
56
61
  }
@@ -69,7 +74,7 @@ export function buildActesLegislatifs(dossier) {
69
74
  for (const lecAss of lecturesAssemblee) {
70
75
  if (lecAss.assemblee !== "Sénat")
71
76
  continue;
72
- const phasePrefix = getPhasePrefix(lecture, lecAss.assemblee);
77
+ const phasePrefix = getPhasePrefix(lecture, lecAss.assemblee, dossier.code_nature_dossier);
73
78
  if (!phasePrefix)
74
79
  continue;
75
80
  const textes = lecAss.textes ?? [];
@@ -144,8 +149,13 @@ export function buildActesLegislatifs(dossier) {
144
149
  else if (origine.includes("devenue résolution")) {
145
150
  libelleStatut = "Adopté";
146
151
  }
152
+ // "devenu résolution du Sénat" : adoptée en commission sans séance publique (art. 73 quinquies RSN)
153
+ // On utilise COM-CAE-DEC plutôt que DEBATS-DEC pour refléter l'absence de séance plénière
154
+ const codeActeDecision = (texteFinal.origine || "").toLowerCase().includes("devenu résolution")
155
+ ? `${phasePrefix}-COM-CAE-DEC`
156
+ : `${phasePrefix}-DEBATS-DEC`;
147
157
  actes.push({
148
- code_acte: `${phasePrefix}-DEBATS-DEC`,
158
+ code_acte: codeActeDecision,
149
159
  date: texteFinal.date,
150
160
  libelle: `${libelleStatut === "Adopté" ? "Adoption" : "Rejet"} (Texte n°${texteFinal.numero})`,
151
161
  id: texteFinal.id,
@@ -68,9 +68,11 @@ export interface SenateurResult {
68
68
  fonctions_bureau: FonctionRow[];
69
69
  groupe_politique: string | null;
70
70
  groupes: MandatOrganismeRow[];
71
+ groupes_amitie: MandatOrganismeRow[];
71
72
  mandats_senateur: MandatSenateurRow[];
72
73
  matricule: string;
73
74
  nom_usuel: string | null;
75
+ organismes: MandatOrganismeRow[];
74
76
  points_contact: PointContactRow[];
75
77
  prenom_usuel: string;
76
78
  qualite: string;
@@ -2,9 +2,11 @@ import assert from "assert";
2
2
  import { execFileSync } from "child_process";
3
3
  import commandLineArgs from "command-line-args";
4
4
  import fs from "fs-extra";
5
- import { formatWithPrettier, makePgTsGenerator, markAsGenerated, processDatabase } from "kanel";
6
- import { makeKyselyHook } from "kanel-kysely";
5
+ import { createRequire } from "module";
7
6
  import path from "path";
7
+ const _require = createRequire(import.meta.url);
8
+ const { formatWithPrettier, makePgTsGenerator, markAsGenerated, processDatabase } = _require("kanel");
9
+ const { makeKyselyHook } = _require("kanel-kysely");
8
10
  import StreamZip from "node-stream-zip";
9
11
  import readline from "readline";
10
12
  import { pipeline, Readable } from "stream";
@@ -1,5 +1,9 @@
1
- import { escapeName, resolveType, useKanelContext, } from "kanel";
2
- import { defaultGetZodIdentifierMetadata, defaultGetZodSchemaMetadata, defaultZodTypeMap } from "kanel-zod";
1
+ import { createRequire } from "module";
2
+ import { useKanelContext, } from "kanel";
3
+ import { defaultGetZodIdentifierMetadata, defaultGetZodSchemaMetadata } from "kanel-zod";
4
+ const _require = createRequire(import.meta.url);
5
+ const { escapeName, resolveType } = _require("kanel");
6
+ const { defaultZodTypeMap } = _require("kanel-zod");
3
7
  const zImport = {
4
8
  asName: undefined,
5
9
  importAsType: false,
@@ -1,5 +1,6 @@
1
1
  import { Pool } from "pg";
2
2
  import { Kysely, PostgresDialect, sql as kyselySql } from "kysely";
3
+ import Cursor from "pg-cursor";
3
4
  import config from "./config.js";
4
5
  // ---------------------------------------------------------------------------
5
6
  // Kysely instance (uses pg driver so .stream() is available)
@@ -19,7 +20,7 @@ const pool = new Pool({
19
20
  * `db.withSchema("senat").selectFrom("sens_sen")`
20
21
  */
21
22
  export const db = new Kysely({
22
- dialect: new PostgresDialect({ pool }),
23
+ dialect: new PostgresDialect({ pool, cursor: Cursor }),
23
24
  });
24
25
  // ---------------------------------------------------------------------------
25
26
  // Streaming helper
@@ -17,7 +17,7 @@ export async function* findAllTextes() {
17
17
  sql `
18
18
  case
19
19
  when ${eb.ref("texte.texurl")} is not null then
20
- regexp_replace(regexp_replace(trim(${eb.ref("texte.texurl")}), '#+$', ''), '^(.*/)?(.*?)(\.html)?$', '\2')
20
+ regexp_replace(regexp_replace(trim(${eb.ref("texte.texurl")}), '#+$', ''), '^(.*/)?(.*?)(\\.html)?$', '\\2')
21
21
  else null
22
22
  end
23
23
  `.as("id"),
@@ -78,7 +78,7 @@ export async function* findAllRapports() {
78
78
  sql `
79
79
  case
80
80
  when ${eb.ref("rap.rapurl")} is not null then
81
- regexp_replace(regexp_replace(trim(${eb.ref("rap.rapurl")}), '#+$', ''), '^(.*/)?(.*?)(\.html)?$', '\2')
81
+ regexp_replace(regexp_replace(trim(${eb.ref("rap.rapurl")}), '#+$', ''), '^(.*/)?(.*?)(\\.html)?$', '\\2')
82
82
  else null
83
83
  end
84
84
  `.as("id"),
@@ -101,7 +101,7 @@ export async function* findAllDossiers() {
101
101
  when texte.texurl is not null then
102
102
  regexp_replace(
103
103
  regexp_replace(trim(texte.texurl), '#+$', ''),
104
- '^(.*/)?(.*?)(\.html)?$', '\2')
104
+ '^(.*/)?(.*?)(\.html)?$', '\\2')
105
105
  else null
106
106
  end
107
107
  `.as("id"),
@@ -153,7 +153,7 @@ export async function* findAllDossiers() {
153
153
  when rap.rapurl is not null then
154
154
  regexp_replace(
155
155
  regexp_replace(trim(rap.rapurl), '#+$', ''),
156
- '^(.*/)?(.*?)(\.html)?$', '\2')
156
+ '^(.*/)?(.*?)(\.html)?$', '\\2')
157
157
  else null
158
158
  end
159
159
  `.as("id"),
@@ -163,6 +163,74 @@ export async function* findAll() {
163
163
  ])
164
164
  .whereRef("memgrppol.senmat", "=", "sen.senmat")
165
165
  .orderBy("memgrppol.memgrppoldatdeb", (ob) => ob.desc().nullsLast())).as("groupes"),
166
+ // Organismes divers (groupes d'études, missions, etc.)
167
+ jsonArrayFrom(eb
168
+ .withSchema("senat")
169
+ .selectFrom("sens_memorg as memorg")
170
+ .leftJoin("sens_org as org", "org.orgcod", "memorg.orgcod")
171
+ .leftJoin("sens_typorg as typorg4", "typorg4.typorgcod", "org.typorgcod")
172
+ .select((eb2) => [
173
+ "memorg.orgcod as code_organisme",
174
+ sql `to_char(memorg.memorgdatdeb, 'YYYY-MM-DD')`.as("date_debut"),
175
+ sql `to_char(memorg.memorgdatfin, 'YYYY-MM-DD')`.as("date_fin"),
176
+ "memorg.temvalcod as etat",
177
+ "org.evelib as libelle",
178
+ "org.typorgcod as type_code_organisme",
179
+ "typorg4.typorglib as type_organisme",
180
+ "memorg.memorgdatdeb as order_date",
181
+ // Fonctions dans l'organisme
182
+ jsonArrayFrom(eb2
183
+ .withSchema("senat")
184
+ .selectFrom("sens_fonmemorg as fonmemorg")
185
+ .leftJoin("sens_fonorg as fonorg", "fonorg.fonorgcod", "fonmemorg.fonorgcod")
186
+ .select([
187
+ sql `to_char(fonmemorg.fonmemorgdatdeb, 'YYYY-MM-DD')`.as("date_debut"),
188
+ sql `to_char(fonmemorg.fonmemorgdatfin, 'YYYY-MM-DD')`.as("date_fin"),
189
+ sql `coalesce(
190
+ nullif(fonorg.fonorglib, ''),
191
+ nullif(fonorg.fonorglil, ''),
192
+ nullif(fonorg.fonorglic, ''))`.as("libelle"),
193
+ "fonmemorg.fonmemorgdatdeb as order_date",
194
+ ])
195
+ .whereRef("fonmemorg.memorgid", "=", "memorg.memorgid")
196
+ .orderBy("fonmemorg.fonmemorgdatdeb", (ob) => ob.desc().nullsLast())).as("fonctions"),
197
+ ])
198
+ .whereRef("memorg.senmat", "=", "sen.senmat")
199
+ .orderBy("memorg.memorgdatdeb", (ob) => ob.desc().nullsLast())).as("organismes"),
200
+ // Groupes sénatoriaux (groupes d'amitié)
201
+ jsonArrayFrom(eb
202
+ .withSchema("senat")
203
+ .selectFrom("sens_memgrpsen as memgrpsen")
204
+ .leftJoin("sens_grpsenami as grpsenami", "grpsenami.orgcod", "memgrpsen.orgcod")
205
+ .leftJoin("sens_typorg as typorg5", "typorg5.typorgcod", "grpsenami.typorgcod")
206
+ .select((eb2) => [
207
+ "memgrpsen.orgcod as code_organisme",
208
+ sql `to_char(memgrpsen.memgrpsendatent, 'YYYY-MM-DD')`.as("date_debut"),
209
+ sql `to_char(memgrpsen.memgrpsendatsor, 'YYYY-MM-DD')`.as("date_fin"),
210
+ "memgrpsen.temvalcod as etat",
211
+ "grpsenami.evelib as libelle",
212
+ "grpsenami.typorgcod as type_code_organisme",
213
+ "typorg5.typorglib as type_organisme",
214
+ "memgrpsen.memgrpsendatent as order_date",
215
+ // Fonctions dans le groupe sénatorial
216
+ jsonArrayFrom(eb2
217
+ .withSchema("senat")
218
+ .selectFrom("sens_fonmemgrpsen as fonmemgrpsen")
219
+ .leftJoin("sens_fongrpsen as fongrpsen", "fongrpsen.fongrpsencod", "fonmemgrpsen.fongrpsencod")
220
+ .select([
221
+ sql `to_char(fonmemgrpsen.fonmemgrpsendatdeb, 'YYYY-MM-DD')`.as("date_debut"),
222
+ sql `to_char(fonmemgrpsen.fonmemgrpsendatfin, 'YYYY-MM-DD')`.as("date_fin"),
223
+ sql `coalesce(
224
+ nullif(fongrpsen.fongrpsenlib, ''),
225
+ nullif(fongrpsen.fongrpsenlil, ''),
226
+ nullif(fongrpsen.fongrpsenlic, ''))`.as("libelle"),
227
+ "fonmemgrpsen.fonmemgrpsendatdeb as order_date",
228
+ ])
229
+ .whereRef("fonmemgrpsen.memgrpsenid", "=", "memgrpsen.memgrpsenid")
230
+ .orderBy("fonmemgrpsen.fonmemgrpsendatdeb", (ob) => ob.desc().nullsLast())).as("fonctions"),
231
+ ])
232
+ .whereRef("memgrpsen.senmat", "=", "sen.senmat")
233
+ .orderBy("memgrpsen.memgrpsendatent", (ob) => ob.desc().nullsLast())).as("groupes_amitie"),
166
234
  // Fonctions au bureau du Sénat
167
235
  jsonArrayFrom(eb
168
236
  .withSchema("senat")
@@ -226,7 +294,9 @@ export async function* findAll() {
226
294
  delegations: row.delegations ?? [],
227
295
  fonctions_bureau: row.fonctions_bureau ?? [],
228
296
  groupes: row.groupes ?? [],
297
+ groupes_amitie: row.groupes_amitie ?? [],
229
298
  mandats_senateur: row.mandats_senateur ?? [],
299
+ organismes: row.organismes ?? [],
230
300
  points_contact: row.points_contact ?? [],
231
301
  urls: row.urls ?? [],
232
302
  };
@@ -50,7 +50,7 @@ export function buildReunionsByBucket(events, dossierBySenatUrl) {
50
50
  for (const e of events) {
51
51
  const kind = classifyAgendaType(e?.type);
52
52
  if (!kind) {
53
- console.warn("Can't determine type of reunion");
53
+ console.warn("Can't determine type of reunion ", e.type, "for event", e.id, e.titre);
54
54
  continue;
55
55
  }
56
56
  const bucket = typeToSuffixStrict(kind);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tricoteuses/senat",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "Handle French Sénat's open data",
5
5
  "keywords": [
6
6
  "France",
@@ -84,6 +84,7 @@
84
84
  "luxon": "^3.7.2",
85
85
  "node-stream-zip": "^1.8.2",
86
86
  "p-limit": "^7.2.0",
87
+ "pg-cursor": "^2.19.0",
87
88
  "postgres": "^3.4.8",
88
89
  "slug": "^11.0.0",
89
90
  "windows-1252": "^3.0.4",
@@ -97,6 +98,7 @@
97
98
  "@types/luxon": "^3.7.1",
98
99
  "@types/node": "^24.10.1",
99
100
  "@types/pg": "^8.20.0",
101
+ "@types/pg-cursor": "^2.7.2",
100
102
  "@types/slug": "^5.0.9",
101
103
  "@typescript-eslint/eslint-plugin": "^8.52.0",
102
104
  "@typescript-eslint/parser": "^8.52.0",
@@ -1,43 +0,0 @@
1
- import "dotenv/config";
2
- import { z } from "zod";
3
- export declare const dbSchema: z.ZodObject<{
4
- host: z.ZodString;
5
- name: z.ZodString;
6
- password: z.ZodString;
7
- port: z.ZodCoercedNumber<unknown>;
8
- user: z.ZodString;
9
- }, z.core.$strip>;
10
- export type DbConfig = z.infer<typeof dbSchema>;
11
- export declare const configSchema: z.ZodObject<{
12
- db: z.ZodObject<{
13
- host: z.ZodString;
14
- name: z.ZodString;
15
- password: z.ZodString;
16
- port: z.ZodCoercedNumber<unknown>;
17
- user: z.ZodString;
18
- }, z.core.$strip>;
19
- stagingDb: z.ZodObject<{
20
- host: z.ZodString;
21
- name: z.ZodString;
22
- password: z.ZodString;
23
- port: z.ZodCoercedNumber<unknown>;
24
- user: z.ZodString;
25
- }, z.core.$strip>;
26
- }, z.core.$strip>;
27
- declare const _default: {
28
- db: {
29
- host: string;
30
- name: string;
31
- password: string;
32
- port: number;
33
- user: string;
34
- };
35
- stagingDb: {
36
- host: string;
37
- name: string;
38
- password: string;
39
- port: number;
40
- user: string;
41
- };
42
- };
43
- export default _default;
package/lib/src/config.js DELETED
@@ -1,37 +0,0 @@
1
- import "dotenv/config";
2
- import { z } from "zod";
3
- export const dbSchema = z.object({
4
- host: z.string().trim().min(1, "Must not be empty"),
5
- name: z.string().trim().min(1, "Must not be empty"),
6
- password: z.string().trim().min(1, "Must not be empty"),
7
- port: z.coerce.number().int().min(0).max(65535),
8
- user: z.string().trim().min(1, "Must not be empty"),
9
- });
10
- export const configSchema = z.object({
11
- db: dbSchema,
12
- stagingDb: dbSchema,
13
- });
14
- const config = {
15
- db: {
16
- host: process.env["DB_HOST"] || "localhost",
17
- name: process.env["DB_NAME"] || "senat",
18
- password: process.env["DB_PASSWORD"] || "opendata",
19
- port: process.env["DB_PORT"] || 5432,
20
- user: process.env["DB_USER"] || "opendata",
21
- },
22
- stagingDb: {
23
- host: process.env["STAGING_DB_HOST"] || process.env["DB_HOST"] || "localhost",
24
- name: process.env["STAGING_DB_NAME"] || "senat_staging",
25
- password: process.env["STAGING_DB_PASSWORD"] || process.env["DB_PASSWORD"] || "opendata",
26
- port: process.env["STAGING_DB_PORT"] || process.env["DB_PORT"] || 5432,
27
- user: process.env["STAGING_DB_USER"] || process.env["DB_USER"] || "opendata",
28
- },
29
- };
30
- const result = configSchema.safeParse(config);
31
- if (!result.success) {
32
- const issues = JSON.stringify(result.error.issues, null, 2);
33
- const serializedConfig = JSON.stringify(config, null, 2);
34
- console.error(`Error in configuration:\n${serializedConfig}\nError:\n${issues}`);
35
- process.exit(-1);
36
- }
37
- export default result.data;
@@ -1,11 +0,0 @@
1
- export interface SenatMetadata {
2
- number: string | null;
3
- session: string | null;
4
- date: string | null;
5
- type: string | null;
6
- authors: string | null;
7
- title: string | null;
8
- commission: string | null;
9
- }
10
- export declare function extractMetadata(xmlDoc: Document): SenatMetadata;
11
- export declare function convertSenatXmlToHtml(texteXml: string, outputFilePath: string): Promise<void>;
@@ -1,320 +0,0 @@
1
- import { JSDOM } from "jsdom";
2
- import fs from "fs-extra";
3
- import path from "path";
4
- import { DateTime } from "luxon";
5
- export function extractMetadata(xmlDoc) {
6
- const metadata = {
7
- number: null,
8
- session: null,
9
- date: null,
10
- type: null,
11
- authors: null,
12
- title: xmlDoc.querySelector("docTitle")?.textContent?.trim() || null,
13
- commission: null,
14
- };
15
- // Extract Number
16
- const docIdAlias = xmlDoc.querySelector('FRBRalias[name="signet-dossier-legislatif-senat"]');
17
- if (docIdAlias) {
18
- const value = docIdAlias.getAttribute("value");
19
- if (value) {
20
- const match = value.match(/\d+$/);
21
- if (match)
22
- metadata.number = match[0];
23
- }
24
- }
25
- // Extract Session
26
- const sessionUri = xmlDoc.querySelector("FRBRExpression > FRBRuri")?.getAttribute("value");
27
- if (sessionUri) {
28
- const match = sessionUri.match(/\d{4}-\d{4}/);
29
- if (match)
30
- metadata.session = match[0];
31
- }
32
- // Extract Date
33
- const depotDate = xmlDoc.querySelector('FRBRdate[name="#depot"]')?.getAttribute("date");
34
- if (depotDate) {
35
- metadata.date = DateTime.fromISO(depotDate).setLocale("fr").toFormat("d MMMM yyyy");
36
- }
37
- else {
38
- const presentationDate = xmlDoc.querySelector('FRBRdate[name="#presentation"]')?.getAttribute("date");
39
- if (presentationDate) {
40
- metadata.date = DateTime.fromISO(presentationDate).setLocale("fr").toFormat("d MMMM yyyy");
41
- }
42
- }
43
- // Extract Type
44
- const bill = xmlDoc.querySelector("bill");
45
- const typeCode = bill?.getAttribute("name");
46
- if (typeCode === "ppl") {
47
- metadata.type = "PROPOSITION DE LOI";
48
- }
49
- else if (typeCode === "pjl") {
50
- metadata.type = "PROJET DE LOI";
51
- }
52
- // Extract Authors
53
- const authorRef = xmlDoc.querySelector('FRBRWork > FRBRauthor[as="#auteur"]')?.getAttribute("href");
54
- if (authorRef) {
55
- const authorId = authorRef.replace(/^#/, "");
56
- const authorPerson = xmlDoc.querySelector(`TLCPerson[eId="${authorId}"]`);
57
- if (authorPerson) {
58
- const showAs = authorPerson.getAttribute("showAs");
59
- if (showAs) {
60
- metadata.authors = showAs.replace(/, Sénateurs$/, ", Sénateurs et Sénatrices");
61
- }
62
- }
63
- }
64
- // Extract Commission
65
- const commissionNode = xmlDoc.querySelector('TLCOrganization[eId="commission-senat"]') ||
66
- xmlDoc.querySelector('TLCOrganization[eId^="commission-"]:not([eId*="assemblee"])');
67
- if (commissionNode) {
68
- metadata.commission = commissionNode.getAttribute("showAs");
69
- }
70
- return metadata;
71
- }
72
- export async function convertSenatXmlToHtml(texteXml, outputFilePath) {
73
- let xmlDoc;
74
- try {
75
- xmlDoc = new JSDOM(texteXml, { contentType: "text/xml" }).window.document;
76
- }
77
- catch (err) {
78
- if (await fs.pathExists(outputFilePath)) {
79
- await fs.remove(outputFilePath);
80
- }
81
- throw err;
82
- }
83
- const metadata = extractMetadata(xmlDoc);
84
- const xmlBody = xmlDoc.querySelector("body");
85
- const style = `
86
- body {
87
- font-family: "URW Bookman", "Bookman Old Style", serif;
88
- max-width: 800px;
89
- margin: 40px auto;
90
- line-height: 1.5;
91
- color: #333;
92
- }
93
- .header {
94
- text-align: center;
95
- margin-bottom: 40px;
96
- border-bottom: 2px solid #333;
97
- padding-bottom: 20px;
98
- }
99
- .header-top {
100
- font-weight: bold;
101
- font-size: 1.2em;
102
- margin-bottom: 10px;
103
- }
104
- .header-session {
105
- text-transform: uppercase;
106
- font-size: 0.9em;
107
- margin-bottom: 5px;
108
- }
109
- .header-date {
110
- font-size: 0.9em;
111
- margin-bottom: 5px;
112
- }
113
- .header-number {
114
- font-weight: bold;
115
- font-size: 1.1em;
116
- margin-bottom: 20px;
117
- }
118
- .header-type {
119
- font-weight: bold;
120
- font-size: 1.5em;
121
- margin-top: 20px;
122
- }
123
- .header-authors {
124
- margin-top: 20px;
125
- font-style: italic;
126
- }
127
- .header-commission {
128
- margin-top: 15px;
129
- font-size: 0.9em;
130
- }
131
- h1 {
132
- text-align: center;
133
- font-size: 1.8em;
134
- margin-top: 10px;
135
- }
136
- p {
137
- margin: 0.6em 0;
138
- }
139
- p.has-alinea {
140
- position: relative;
141
- padding-left: 2.5em;
142
- }
143
- .alinea {
144
- position: absolute;
145
- left: 0;
146
- top: 0.15em;
147
- display: inline-flex;
148
- align-items: center;
149
- justify-content: center;
150
- min-width: 1.5em;
151
- height: 1.5em;
152
- padding: 0 0.3em;
153
- margin-right: 0.3em;
154
- font-size: 0.75em;
155
- font-weight: bold;
156
- color: #555;
157
- background-color: #f0f0f0;
158
- border: 1px solid #ccc;
159
- border-radius: 1em;
160
- }
161
- .num {
162
- font-weight: bold;
163
- margin-right: 0.2em;
164
- }
165
- .article {
166
- margin-top: 2em;
167
- }
168
- .article h3 {
169
- border-bottom: 1px solid #eee;
170
- padding-bottom: 5px;
171
- }
172
- `;
173
- const htmlDocTemplate = `<!DOCTYPE html>
174
- <html lang="fr">
175
- <head>
176
- <meta charset="utf-8">
177
- <title>${metadata.title || "Document Sénat"}</title>
178
- <style>${style}</style>
179
- </head>
180
- <body>
181
- <div class="header">
182
- <div class="header-top">SÉNAT</div>
183
- <div class="header-session">SESSION ORDINAIRE DE ${metadata.session || "...."}</div>
184
- ${metadata.date ? `<div class="header-date">Enregistré à la Présidence du Sénat le ${metadata.date}</div>` : ""}
185
- <div class="header-number">N° ${metadata.number || "...."}</div>
186
- <div class="header-type">${metadata.type || ""}</div>
187
- <div class="header-authors">${metadata.authors || ""}</div>
188
- ${metadata.commission
189
- ? [
190
- `<div class="header-commission">Envoyée à la ${metadata.commission.toLowerCase()},`,
191
- "sous réserve de la constitution éventuelle d'une commission spéciale dans les conditions prévues",
192
- "par le Règlement.</div>",
193
- ].join(" ")
194
- : ""}
195
- </div>
196
- <h1>${metadata.title || ""}</h1>
197
- </body>
198
- </html>`;
199
- const { document: htmlDoc } = new JSDOM(htmlDocTemplate).window;
200
- const body = htmlDoc.body;
201
- if (xmlBody) {
202
- const processNode = (xmlNode, htmlParent, alineaData = null) => {
203
- const children = Array.from(xmlNode.childNodes);
204
- const alineaChildren = [];
205
- const otherChildren = [];
206
- for (const child of children) {
207
- if (child.nodeType === 1 && child.tagName.toLowerCase() === "alinea") {
208
- alineaChildren.push(child);
209
- }
210
- else {
211
- otherChildren.push(child);
212
- }
213
- }
214
- for (const child of otherChildren) {
215
- if (child.nodeType === 3) {
216
- htmlParent.appendChild(htmlDoc.createTextNode(child.textContent || ""));
217
- }
218
- else if (child.nodeType === 1) {
219
- const element = child;
220
- const tagName = element.tagName.toLowerCase();
221
- let htmlElement = null;
222
- switch (tagName) {
223
- case "article": {
224
- htmlElement = htmlDoc.createElement("div");
225
- htmlElement.className = "article";
226
- const artId = element.getAttribute("eId");
227
- if (artId)
228
- htmlElement.id = artId;
229
- const artGuid = element.getAttribute("GUID");
230
- if (artGuid)
231
- htmlElement.setAttribute("data-guid", artGuid);
232
- break;
233
- }
234
- case "num": {
235
- const parentTagName = element.parentElement?.tagName.toLowerCase();
236
- if (parentTagName === "alinea" && alineaData) {
237
- alineaData.numText = element.textContent?.trim();
238
- continue;
239
- }
240
- htmlElement = htmlDoc.createElement("span");
241
- htmlElement.className = "num";
242
- break;
243
- }
244
- case "heading":
245
- htmlElement = htmlDoc.createElement("h4");
246
- break;
247
- case "p":
248
- htmlElement = htmlDoc.createElement("p");
249
- if (alineaData) {
250
- htmlElement.classList.add("has-alinea");
251
- if (alineaData.id)
252
- htmlElement.id = alineaData.id;
253
- if (alineaData.guid)
254
- htmlElement.setAttribute("data-guid", alineaData.guid);
255
- const pastille = alineaData.pastille;
256
- if (pastille) {
257
- htmlElement.setAttribute("data-pastille", pastille);
258
- if (!alineaData.pastilleApplied) {
259
- const span = htmlDoc.createElement("span");
260
- span.className = "alinea";
261
- span.setAttribute("data-alinea", pastille);
262
- span.textContent = pastille;
263
- htmlElement.appendChild(span);
264
- alineaData.pastilleApplied = true;
265
- }
266
- }
267
- if (alineaData.numText) {
268
- const xmlPText = element.textContent || "";
269
- const normalize = (s) => s.replace(/[\\s\\u00A0]+/g, " ").trim();
270
- const normalizedNum = normalize(alineaData.numText);
271
- const normalizedP = normalize(xmlPText);
272
- if (normalizedNum && !normalizedP.startsWith(normalizedNum)) {
273
- const numSpan = htmlDoc.createElement("span");
274
- numSpan.className = "num";
275
- numSpan.textContent = alineaData.numText + " ";
276
- htmlElement.appendChild(numSpan);
277
- }
278
- alineaData.numText = null;
279
- }
280
- }
281
- break;
282
- case "content":
283
- processNode(element, htmlParent, alineaData);
284
- continue;
285
- case "doctitle":
286
- continue;
287
- case "i":
288
- case "b":
289
- case "u":
290
- case "sup":
291
- case "sub":
292
- htmlElement = htmlDoc.createElement(tagName);
293
- break;
294
- default:
295
- htmlElement = htmlDoc.createElement("span");
296
- htmlElement.setAttribute("data-xml-tag", tagName);
297
- break;
298
- }
299
- if (htmlElement) {
300
- htmlParent.appendChild(htmlElement);
301
- processNode(element, htmlElement, alineaData);
302
- }
303
- }
304
- }
305
- for (const element of alineaChildren) {
306
- const nextAlineaData = {
307
- id: element.getAttribute("eId"),
308
- guid: element.getAttribute("GUID"),
309
- pastille: element.getAttribute("data:pastille"),
310
- pastilleApplied: false,
311
- };
312
- processNode(element, htmlParent, nextAlineaData);
313
- }
314
- };
315
- processNode(xmlBody, body);
316
- }
317
- const htmlContent = "<!DOCTYPE html>\n" + htmlDoc.documentElement.outerHTML;
318
- await fs.ensureDir(path.dirname(outputFilePath));
319
- await fs.outputFile(outputFilePath, htmlContent);
320
- }