PageSourceSearch

https://creaconnect.world/src/crea-document.js

js creaconnect.world collected 2026-09-24 10:09:49 UTC 26,708 bytes, 518 lines download raw bytes

1/* ══════════════════════════════════════════════════════════════════════════
2 * LE DOCUMENT CRÉA — une seule représentation, trois formes, quatre modes
3 * ══════════════════════════════════════════════════════════════════════════
4 *
5 * « L'unité est le SUJET, pas le fichier. »
6 *
7 * Un plan d'investissement se lit, se présente et se calcule. Ce sont trois
8 * FORMES du même sujet, pas trois documents. Ce fichier tient la chose dont
9 * les trois formes sont des rendus — et rien d'autre.
10 *
11 * ── CE QUI EXISTAIT, ET POURQUOI ÇA NE SUFFISAIT PLUS ─────────────────────
12 *
13 * Le document vivait en Markdown DANS le message (`src/crea-doc.js`), et
14 * l'export le relisait à chaque fois. Deux chemins parallèles vers le même
15 * livrable : `crea-doc.js` fabrique un `.doc` qui est du HTML avec un en-tête
16 * Word, pendant que la fiche `docx` de `server/crea-skills-place.js` dit en
17 * toutes lettres « jamais du HTML renommé ». Le Markdown dans un message ne
18 * peut pas non plus s'ouvrir depuis un modèle, ni se rendre en diaporama sans
19 * être réécrit.
20 *
21 * ── LA RÈGLE QUI TIENT CE FICHIER PETIT ───────────────────────────────────
22 *
23 * UN TYPE DE BLOC N'ENTRE QUE SI AU MOINS DEUX FORMES SAVENT LE RENDRE.
24 *
25 * Sans elle, ce modèle grossit à chaque demande et redevient Word — sept
26 * cents notions dont personne ne se sert, et un aller-retour qui perd la
27 * moitié de ce qu'il transporte à chaque passage.
28 *
29 * Elle mord tout de suite, et c'est le signe qu'elle sert :
30 *   · `code` ne rentre pas — seule la forme texte le rend. Il appartient au
31 *     Studio Code, et un document qui embarque du code exécutable n'est plus
32 *     un document.
33 *   · les notes d'orateur ne rentrent pas — seule la diapo les rend. Ce n'est
34 *     pas un type de bloc, c'est un DÉTAIL DE RENDU de la forme diapo, dérivé
35 *     des paragraphes de la section.
36 *
37 * Elle n'est pas une intention : `RENDUS` la déclare, et
38 * `tests/document-crea-smoke.mjs` la vérifie dans les deux sens sur la table
39 * exécutée — un type qu'aucune forme ne rend, comme une forme qui prétend
40 * rendre un type qui n'existe pas, font échouer la garde.
41 *
42 * ── CE QUE CE FICHIER N'EST PAS ───────────────────────────────────────────
43 *
44 * Ce n'est pas un éditeur, ni une bouche. Il ne compose aucun prompt, ne
45 * porte aucune identité, et ne sait pas dans quel studio il est. Il tient une
46 * structure et ses rendus — c'est tout son poste.
47 * ══════════════════════════════════════════════════════════════════════════ */
48
49/* ── LES HUIT TYPES, ET CE QUE CHACUN PORTE ───────────────────────────────
50 *
51 * Huit, parce que huit ont passé la règle des deux formes. Ce n'est pas un
52 * chiffre choisi : c'est ce qui reste. */
53const BLOCS_CREA = {
54  titre:      { champs: ['texte'] },
55  section:    { champs: ['texte', 'niveau'] },
56  paragraphe: { champs: ['texte'] },
57  liste:      { champs: ['items', 'ordonnee'] },
58  tableau:    { champs: ['entetes', 'lignes'] },
59  image:      { champs: ['source', 'alt'] },
60  encadre:    { champs: ['texte', 'intitule'] },
61  kpi:        { champs: ['libelle', 'valeur', 'cible', 'unite'] },
62}
63
64/* ── CE QUE CHAQUE FORME SAIT RENDRE ──────────────────────────────────────
65 *
66 * Cette table est la RÈGLE ELLE-MÊME, pas son commentaire. La garde la lit,
67 * compte les formes par type, et refuse un type qui n'en aurait qu'une.
68 *
69 * Un type absent d'une forme n'est pas une perte silencieuse : le rendu le
70 * laisse tomber sciemment, et c'est écrit ici. Une image n'a pas de place
71 * dans un tableur, un KPI n'a pas de place dans un paragraphe suivi. */
72const RENDUS_CREA = {
73  texte:   ['titre', 'section', 'paragraphe', 'liste', 'tableau', 'image', 'encadre', 'kpi'],
74  diapo:   ['titre', 'section', 'paragraphe', 'liste', 'tableau', 'image', 'encadre', 'kpi'],
75  tableur: ['titre', 'section', 'tableau', 'kpi'],
76}
77
78/* ── LECTURE DU MARKDOWN ──────────────────────────────────────────────────
79 *
80 * Le mode 1 fait écrire Créa en Markdown, et c'est le contrat de sortie du
81 * Studio Documents. Cette porte-là doit donc rester ouverte : le document
82 * naît d'un Markdown, et sait y retourner sans se déformer.
83 *
84 * ON NE RÉ-IMPLÉMENTE PAS UN PARSEUR MARKDOWN COMPLET. On lit les sept
85 * formes que le contrat de sortie promet — titre, parties, paragraphes,
86 * listes, tableaux, images, encadrés — et rien de plus. Ce qui n'est pas
87 * reconnu devient un paragraphe : on ne perd pas de texte, on perd du
88 * balisage, et c'est le bon sens de la perte. */
89function documentDepuisMarkdown(md) {
90  const lignes = String(md == null ? '' : md).replace(/\r\n?/g, '\n').split('\n')
91  const doc = { titre: '', blocs: [] }
92  let i = 0
93
94  const estTableau = (l, suivante) =>
95    /^\s*\|.*\|\s*$/.test(l) && /^\s*\|[\s:|-]+\|\s*$/.test(suivante || '')
96
97  const cellules = (l) => l.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim())
98
99  while (i < lignes.length) {
100    const l = lignes[i]
101
102    if (!l.trim()) { i++; continue }
103
104    // # titre — le premier seulement ; les suivants deviennent des sections,
105    // parce qu'un document n'a qu'un titre et que Word non plus n'en a qu'un.
106    const h = l.match(/^(#{1,6})\s+(.*)$/)
107    if (h) {
108      const niveau = h[1].length
109      const texte = h[2].trim()
110      if (niveau === 1 && !doc.titre) doc.titre = texte
111      else doc.blocs.push({ type: 'section', texte: texte, niveau: Math.max(2, niveau) })
112      i++; continue
113    }
114
115    // | tableau |
116    if (estTableau(l, lignes[i + 1])) {
117      const entetes = cellules(l)
118      i += 2
119      const corps = []
120      while (i < lignes.length && /^\s*\|.*\|\s*$/.test(lignes[i])) { corps.push(cellules(lignes[i])); i++ }
121      doc.blocs.push({ type: 'tableau', entetes: entetes, lignes: corps })
122      continue
123    }
124
125    // ![alt](source)
126    const img = l.match(/^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/)
127    if (img) { doc.blocs.push({ type: 'image', alt: img[1], source: img[2] }); i++; continue }
128
129    // > encadré — les lignes consécutives forment UN bloc. Un « **Intitulé —** »
130    // en tête devient l'intitulé : c'est la forme que le contrat de sortie du
131    // Studio Documents produit pour les hypothèses.
132    if (/^\s*>\s?/.test(l)) {
133      const parts = []
134      while (i < lignes.length && /^\s*>\s?/.test(lignes[i])) { parts.push(lignes[i].replace(/^\s*>\s?/, '')); i++ }
135      let texte = parts.join(' ').trim()
136      let intitule = ''
137      const tete = texte.match(/^\*\*([^*]+)\*\*\s*[—:-]\s*(.*)$/)
138      if (tete) { intitule = tete[1].trim(); texte = tete[2].trim() }
139      doc.blocs.push({ type: 'encadre', intitule: intitule, texte: texte })
140      continue
141    }
142
143    // - liste / 1. liste
144    if (/^\s*([-*+]|\d+\.)\s+/.test(l)) {
145      const ordonnee = /^\s*\d+\.\s+/.test(l)
146      const items = []
147      while (i < lignes.length && /^\s*([-*+]|\d+\.)\s+/.test(lignes[i])) {
148        items.push(lignes[i].replace(/^\s*([-*+]|\d+\.)\s+/, '').trim()); i++
149      }
150      doc.blocs.push({ type: 'liste', items: items, ordonnee: ordonnee })
151      continue
152    }
153
154    // paragraphe — les lignes consécutives se recollent, comme en Markdown.
155    const parts = []
156    while (i < lignes.length && lignes[i].trim() &&
157           !/^(#{1,6})\s/.test(lignes[i]) && !/^\s*>/.test(lignes[i]) &&
158           !/^\s*([-*+]|\d+\.)\s+/.test(lignes[i]) && !/^\s*\|/.test(lignes[i]) &&
159           !/^\s*!\[/.test(lignes[i])) { parts.push(lignes[i].trim()); i++ }
160    if (parts.length) doc.blocs.push({ type: 'paragraphe', texte: parts.join(' ') })
161    else i++
162  }
163
164  return doc
165}
166
167/* ── RETOUR AU MARKDOWN ───────────────────────────────────────────────────
168 *
169 * L'ALLER-RETOUR EST CE QUI SE MESURE. Un modèle de document se juge sur ce
170 * qui survit à un tour complet, pas sur ce qu'il sait représenter. La garde
171 * rejoue `markdown → document → markdown → document` et exige que les DEUX
172 * documents soient identiques : c'est la seule preuve que rien ne se dégrade
173 * à chaque passage. */
174function documentVersMarkdown(doc) {
175  const d = doc || {}
176  const out = []
177  if (d.titre) out.push('# ' + d.titre)
178  ;
178(d.blocs || []).forEach(function (b) {
179    if (b.type === 'section') out.push('#'.repeat(Math.max(2, b.niveau || 2)) + ' ' + b.texte)
180    else if (b.type === 'paragraphe') out.push(b.texte)
181    else if (b.type === 'liste') out.push((b.items || []).map(function (it, n) {
182      return (b.ordonnee ? (n + 1) + '. ' : '- ') + it
183    }).join('\n'))
184    else if (b.type === 'tableau') {
185      const e = b.entetes || []
186      out.push('| ' + e.join(' | ') + ' |\n|' + e.map(function () { return ' --- ' }).join('|') + '|\n' +
187        (b.lignes || []).map(function (r) { return '| ' + r.join(' | ') + ' |' }).join('\n'))
188    }
189    else if (b.type === 'image') out.push('![' + (b.alt || '') + '](' + (b.source || '') + ')')
190    else if (b.type === 'encadre') out.push('> ' + (b.intitule ? '**' + b.intitule + '** — ' : '') + b.texte)
191    else if (b.type === 'kpi') out.push('> **' + b.libelle + '** — ' + b.valeur + (b.unite ? ' ' + b.unite : '') +
192      (b.cible ? ' (cible ' + b.cible + ')' : ''))
193  })
194  return out.join('\n\n')
195}
196
197/* ── FORME TEXTE ──────────────────────────────────────────────────────────
198 * Le document tel qu'on le lit et qu'on l'imprime. */
199function documentVersHtml(doc) {
200  const d = doc || {}
201  const e = function (s) {
202    return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
203      return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]
204    })
205  }
206  /* ── L'EMPHASE EN LIGNE ────────────────────────────────────────────────
207   * MESURÉ À L'ÉCRAN, sur un vrai plan d'investissement : « Le point mort est
208   * atteint au **mois 19** » s'affichait avec ses quatre astérisques. Toutes
209   * les formes d'emphase d'un document — un chiffre qu'on souligne, un nom de
210   * mois, un terme défini — sortaient en clair. Seul l'encadré s'en tirait,
211   * parce que son intitulé est extrait par la STRUCTURE et non par le style.
212   *
213   * ÉCHAPPER D'ABORD, BALISER ENSUITE, ET DANS CET ORDRE : `e()` transforme le
214   * `<` de l'usager en `&lt;` ; les balises posées après ne peuvent donc plus
215   * être confondues avec du texte, et rien de ce qu'on tape ne devient du
216   * balisage. L'inverse — baliser puis échapper — rendrait les `<strong>` en
217   * clair et laisserait passer le reste.
218   *
219   * TROIS FORMES, PAS PLUS : le gras, l'italique et le code. Ce ne sont pas
220   * trois choix de style — ce sont les trois que le Markdown des contrats de
221   * sortie produit. Une quatrième serait une syntaxe que rien n'écrit. */
222  const enLigne = function (s) {
223    return e(s)
224      .replace(/`([^`]+)`/g, '<code>$1</code>')
225      /* PAS D'ESPACE CONTRE LE DÉLIMITEUR — c'est la règle du Markdown, et
226         ici elle protège un cas qui arrive tout le temps dans ces documents :
227         « 2 340 h * 42 € » est une MULTIPLICATION. Sans cette contrainte, tout
228         ce qui sépare deux astérisques dans un calcul passait en italique, et
229         les astérisques disparaissaient — le lecteur ne voyait plus l'opération.
230         Mesuré sur « 2 * 3 = 6 et 4 * 5 = 20 ». */
231      .replace(/\*\*([^\s*][^*\n]*?[^\s*]|[^\s*])\*\*/g, '<strong>$1</strong>')
232      .replace(/(^|[^*\w])\*([^\s*][^*\n]*?[^\s*]|[^\s*])\*(?!\*)/g, '$1<em>$2</em>')
233  }
234  const out = []
235  if (d.titre) out.push('<h1>' + enLigne(d.titre) + '</h1>')
236  ;(d.blocs || []).forEach(function (b) {
237    if (b.type === 'section') out.push('<h' + Math.min(6, b.niveau || 2) + '>' + enLigne(b.texte) + '</h' + Math.min(6, b.niveau || 2) + '>')
238    else if (b.type === 'paragraphe') out.push('<p>' + enLigne(b.texte) + '</p>')
239    else if (b.type === 'liste') {
240      const t = b.ordonnee ? 'ol' : 'ul'
241      out.push('<' + t + '>' + (b.items || []).map(function (i) { return '<li>' + enLigne(i) + '</li>' }).join('') + '</' + t + '>')
242    }
243    else if (b.type === 'tableau') out.push('<table><thead><tr>' +
244      (b.entetes || []).map(function (h) { return '<th>' + e(h) + '</th>' }).join('') + '</tr></thead><tbody>' +
245      (b.lignes || []).map(function (r) { return '<tr>' + r.map(function (c) { return '<td>' + enLigne(c) + '</td>' }).join('') + '</tr>' }).join('') +
246      '</tbody></table>')
247    else if (b.type === 'image') out.push('<img src="' + e(b.source) + '" alt="' + e(b.alt) + '">')
248    else if (b.type === 'encadre') out.push('<blockquote>' + (b.intitule ? '<strong>' + enLigne(b.intitule) + '</strong> — ' : '') + enLigne(b.texte) + '</blockquote>')
249    else if (b.type === 'kpi') out.push('<p class="crea-kpi"><strong>' + e(b.libelle) + '</strong> ' + e(b.valeur) +
250      (b.unite ? ' ' + e(b.unite) : '') + (b.cible ? ' <span>cible ' + e(b.cible) + '</span>' : '') + '</p>')
251  })
252  return out.join('\n')
253}
254
255/* ── FORME DIAPO ──────────────────────────────────────────────────────────
256 *
257 * UNE SECTION FAIT UNE DIAPO. C'est la règle la plus simple qui donne un
258 * découpage juste : l'auteur a déjà décidé où se coupent les idées en posant
259 * ses parties, et redécouper autrement reviendrait à le contredire.
260 *
261 * Rend le contrat que `detectStudioArtifact` relit déjà — `{title, theme,
262 * slides:[…]}`. Les notes d'orateur sont DÉRIVÉES des paragraphes de la
263 * section : elles n'existent pas dans le modèle, parce qu'une seule forme les
264 * rendrait. */
265function documentVersDiapos(doc) {
266  const d = doc || {}
267  const slides = []
268  let n = 1
269  slides.push({ id: n++, layout: 'title', title: d.titre || 'Document', subtitle: '', notes: '' })
270
271  let courante = null
272  const pousser = function () { if (courante) { slides.push(courante); courante = null } }
273
274  ;
274(d.blocs || []).forEach(function (b) {
275    if (b.type === 'section') {
276      pousser()
277      courante = { id: n++, layout: 'content', title: b.texte, bullets: [], notes: '' }
278      return
279    }
280    if (!courante) courante = { id: n++, layout: 'content', title: d.titre || '', bullets: [], notes: '' }
281
282    if (b.type === 'liste') (b.items || []).forEach(function (i) { courante.bullets.push(i) })
283    else if (b.type === 'encadre') courante.bullets.push((b.intitule ? b.intitule + ' — ' : '') + b.texte)
284    else if (b.type === 'kpi') courante.bullets.push(b.libelle + ' : ' + b.valeur + (b.unite ? ' ' + b.unite : ''))
285    else if (b.type === 'tableau') (b.lignes || []).forEach(function (r) { courante.bullets.push(r.join(' · ')) })
286    else if (b.type === 'image') courante.image = b.source
287    // LE PARAGRAPHE DESCEND AUX NOTES, PAS AUX PUCES. Un paragraphe projeté
288    // en puce donne la diapo qu'on lit à voix haute pendant que la salle la
289    // lit aussi — le défaut le plus commun d'un diaporama.
290    else if (b.type === 'paragraphe') courante.notes = (courante.notes ? courante.notes + ' ' : '') + b.texte
291  })
292  pousser()
293  return { title: d.titre || 'Document', theme: 'dark', slides: slides }
294}
295
296/* ── FORME TABLEUR ────────────────────────────────────────────────────────
297 *
298 * UN TABLEAU FAIT UNE FEUILLE, nommée par la section qui le précède. Les KPI
299 * se rassemblent à part et ressortent dans le contrat `{kpis:[…]}` que
300 * `detectStudioArtifact` relit déjà.
301 *
302 * Le reste est laissé, sciemment : un paragraphe n'a pas de cellule, et le
303 * loger en A1 ferait un tableur qu'on ne peut ni calculer ni relire. */
304function documentVersTableur(doc) {
305  const d = doc || {}
306  const feuilles = []
307  const kpis = []
308  let nom = ''
309  let rang = 1
310
311  ;(d.blocs || []).forEach(function (b) {
312    if (b.type === 'section') { nom = b.texte; return }
313    if (b.type === 'kpi') {
314      kpis.push({ label: b.libelle, value: b.valeur, target: b.cible || '', unit: b.unite || '', trend: 'flat' })
315      return
316    }
317    if (b.type === 'tableau') {
318      feuilles.push({ nom: nom || ('Feuille ' + rang++), entetes: b.entetes || [], lignes: b.lignes || [] })
319    }
320  })
321
322  return { title: d.titre || 'Document', feuilles: feuilles, kpis: kpis }
323}
324
325/* ── LA DÉRIVATION : UN DOCUMENT DEVIENT UN MODÈLE ────────────────────────
326 *
327 * « Un document produit peut-il devenir un modèle ? » — c'était la question qui
328 * séparait un outil qui apprend de son usage d'un outil qu'il faut nourrir à la
329 * main. Voici la réponse mécanique.
330 *
331 * CE N'EST NI UNE FORME NI UN MODE. On ne rend pas le sujet autrement : on en
332 * RETIRE ce qui appartient à ce cas-là, et on garde ce qui servira au suivant.
333 * La structure et la prose survivent, les chiffres deviennent des blancs.
334 *
335 * Le contrat de sortie du Studio Documents décrit déjà ce qu'est un modèle :
336 * « les variables entre crochets — [NOM], [DATE], [SOCIÉTÉ] — laissées telles
337 * quelles ». Cette fonction produit exactement cette forme-là.
338 *
339 * ── CE QU'ELLE REMPLACE, ET C'EST TOUT ────────────────────────────────────
340 *
341 * Ce qui se détecte SANS DEVINER : les montants, les pourcentages, les années
342 * et les dates. Un nombre est un nombre — on ne se trompe pas.
343 *
344 * ── CE QU'ELLE NE DÉTECTE PAS, ET IL FAUT LE DIRE ─────────────────────────
345 *
346 * LES NOMS. Ni la société, ni la personne, ni le lieu, ni le client. Aucune
347 * règle mécanique ne distingue « Atelier Marin » d'un mot ordinaire, et
348 * prétendre le contraire serait pire que de ne rien faire : on croirait le
349 * modèle anonyme.
350 *
351 * Ça compte parce qu'un modèle se PUBLIE — sur la place de marché, pour
352 * quelqu'un d'autre. Un modèle dérivé d'un vrai document peut encore porter le
353 * nom d'un client. Celui qui le publie doit le relire, et l'interface doit le
354 * lui dire avant. `tests/document-crea-smoke.mjs` tient cette limite : elle est
355 * vérifiée, pas seulement écrite ici. */
356const VARIABLES_CREA = [
357  // Un montant : le symbole ou le code décide, jamais le nombre seul.
358  { quoi: /\d[\d  .,]*\s*(?:€|\$|£|EUR|USD|k€|K€|M€)/g, par: '[MONTANT]' },
359  // Un pourcentage.
360  { quoi: /\d+(?:[.,]\d+)?\s*%/g, par: '[POURCENTAGE]' },
361  // Une date écrite en chiffres.
362  { quoi: /\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b/g, par: '[DATE]' },
363  // Une année seule, dans la fourchette où c'en est une.
364  { quoi: /\b(?:19|20)\d{2}\b/g, par: '[ANNÉE]' },
365]
366
367function texteEnModele(t) {
368  let out = String(t == null ? '' : t)
369  VARIABLES_CREA.forEach(function (v) { out = out.replace(v.quoi, v.par) })
370  return out
371}
372
373function documentVersModele(doc) {
374  const d = doc || {}
375  return {
376    titre: texteEnModele(d.titre),
377    blocs: (d.blocs || []).map(function (b) {
378      const n = Object.assign({}, b)
379      if (typeof n.texte === 'string') n.texte = texteEnModele(n.texte)
380      if (typeof n.intitule === 'string') n.intitule = texteEnModele(n.intitule)
381      if (Array.isArray(n.items)) n.items = n.items.map(texteEnModele)
382      if (Array.isArray(n.entetes)) n.entetes = n.entetes.map(texteEnModele)
383      if (Array.isArray(n.lignes)) n.lignes = n.lignes.map(function (r) { return r.map(texteEnModele) })
384      if (typeof n.valeur === 'string') n.valeur = texteEnModele(n.valeur)
385      if (typeof n.cible === 'string') n.cible = texteEnModele(n.cible)
386      return n
387    }),
388  }
389}
390
391/* ══ LES PONTS VERS LES VRAIS FORMATS ══════════════════════════════════════
392 *
393 * CE QUI S'EST PASSÉ, ET IL FAUT L'ÉCRIRE : ce poste a été construit à la main
394 * — huit types de blocs, un modèle, trois rendus — pendant qu'un pont vers les
395 * VRAIES bibliothèques dormait à côté, sur le serveur. `server/crea-lm-docs.js`
396 * utilise `docx`, `exceljs`, `pdfkit` et `pptxgenjs` ;
396 elles sont installées,
397 * elles fabriquent de vrais fichiers Office, et ce modèle ne leur parlait pas.
398 * Mesuré : zéro référence dans les deux sens.
399 *
400 * C'est exactement ce que le dépôt appelle un canal parallèle, et il est né de
401 * n'avoir pas cherché ce qui existait avant d'écrire.
402 *
403 * CES TROIS FONCTIONS NE FABRIQUENT RIEN. Elles traduisent le document dans la
404 * forme que chaque fabricant attend, et s'arrêtent là. Le `.docx` est fait par
405 * `docx`, le `.xlsx` par `exceljs`, le `.pptx` par `pptxgenjs` — pas ici. Si un
406 * jour l'une d'elles se met à écrire du XML, c'est que le canal parallèle est
407 * revenu.
408 *
409 * CE QU'ELLES PERDENT, ET LE DIRE VAUT MIEUX QUE LE DÉCOUVRIR : le pont est
410 * plus pauvre que le modèle. `creerDocx` ne connaît que `{titre, contenu}` —
411 * donc un tableau y descend en texte, et un encadré perd son filet. Ce n'est
412 * pas une limite du document, c'est une limite du pont, et c'est le pont qu'il
413 * faudra enrichir — pas ce fichier.
414 */
415
416/* Le texte d'un bloc, aplati pour un pont qui ne connaît que des lignes. */
417function blocEnTexte(b) {
418  if (b.type === 'paragraphe') return b.texte || ''
419  if (b.type === 'liste') return (b.items || []).map(function (i) { return '• ' + i }).join('\n')
420  if (b.type === 'encadre') return (b.intitule ? b.intitule + ' — ' : '') + (b.texte || '')
421  if (b.type === 'kpi') return b.libelle + ' : ' + b.valeur + (b.unite ? ' ' + b.unite : '') +
422    (b.cible ? ' (cible ' + b.cible + ')' : '')
423  if (b.type === 'tableau') {
424    /* UN TABLEAU DESCEND EN LIGNES DE TEXTE, et c'est une PERTE. Le pont Word
425       n'a pas de tableau ; le taire donnerait un document où des chiffres ont
426       disparu sans trace. La forme tableur, elle, le garde entier. */
427    const l = [(b.entetes || []).join(' | ')]
428    for (const r of (b.lignes || [])) l.push(r.join(' | '))
429    return l.join('\n')
430  }
431  return b.texte || ''
432}
433
434/* Vers `creerDocx` et `creerPdf` — tous deux attendent `{titre, sections}`,
435 * une section étant `{titre, contenu}`. Les blocs se regroupent sous leur
436 * section ; ce qui précède la première va dans une section sans titre. */
437function documentVersSections(doc) {
438  const d = doc || {}
439  const sections = []
440  let cour = { titre: '', contenu: [] }
441  for (const b of (d.blocs || [])) {
442    if (b.type === 'section') {
443      if (cour.titre || cour.contenu.length) sections.push(cour)
444      cour = { titre: b.texte || '', contenu: [] }
445    } else {
446      const t = blocEnTexte(b)
447      if (t) cour.contenu.push(t)
448    }
449  }
450  if (cour.titre || cour.contenu.length) sections.push(cour)
451  return {
452    titre: d.titre || 'Document',
453    sections: sections.map(function (s) { return { titre: s.titre, contenu: s.contenu.join('\n\n') } }),
454  }
455}
456
457/* Vers `creerXlsx`. Il attend `{nom, entetes, donnees}` ; `versTableur` produit
458 * déjà `{nom, entetes, lignes}` — on ne recalcule donc rien, on renomme le
459 * champ. Les indicateurs partent dans leur propre feuille : les mêler aux
460 * tableaux ferait une feuille qui ne se lit ni comme l'un ni comme l'autre. */
461function documentVersXlsx(doc) {
462  const t = documentVersTableur(doc)
463  const feuilles = t.feuilles.map(function (f) {
464    return { nom: f.nom.slice(0, 31), entetes: f.entetes, donnees: f.lignes }
465  })
466  if (t.kpis.length) {
467    feuilles.unshift({
468      nom: 'Indicateurs',
469      entetes: ['Indicateur', 'Valeur', 'Unité', 'Cible'],
470      donnees: t.kpis.map(function (k) { return [k.label, k.value, k.unit || '', k.target || ''] }),
471    })
472  }
473  return { titre: (doc || {}).titre || 'Classeur', feuilles: feuilles }
474}
475
476/* Vers `creerPptx`. Il attend `{titre, puces, notes}` ; `versDiapos` produit
477 * `{title, bullets, notes}` — on renomme, on ne recalcule pas.
478 *
479 * LE PONT A ÉTÉ ENRICHI POUR ÇA, ET C'EST LE BON SENS DE LA CORRECTION : il
480 * jetait les notes d'orateur, faute de champ. La tentation était de les faire
481 * descendre dans `contenu` — c'est-à-dire SUR la diapo. C'eût été défaire
482 * exactement ce que `versDiapos` protège : la diapo qu'on lit à voix haute
483 * pendant que la salle la lit aussi. `pptxgenjs` porte `addNotes` nativement ;
484 * c'est le pont qui manquait d'une ligne, pas le modèle qui devait plier. */
485function documentVersPptx(doc) {
486  const d = documentVersDiapos(doc)
487  const out = []
488  for (const s of (d.slides || [])) {
489    const puces = s.bullets || []
490    const notes = s.notes || ''
491    /* LA DIAPO DE TITRE NE PASSE PAS : `creerPptx` fabrique la sienne, et la
492       laisser passer en ferait deux à la suite. On la reconnaît à ce qu'elle
493       est — le titre du document, sans puce ni note. */
494    if (!puces.length && !notes) continue
495    out.push({ titre: s.title || '', puces: puces, notes: notes })
496  }
497  return { titre: d.title || 'Présentation', diapositives: out }
498}
499
500/* Le poste s'expose au navigateur sans se supposer dedans : la garde
501 * l'exécute dans Node, où `window` n'existe pas. */
502const DOCUMENT_CREA = {
503  BLOCS: BLOCS_CREA,
504  RENDUS: RENDUS_CREA,
505  depuisMarkdown: documentDepuisMarkdown,
506  versMarkdown: documentVersMarkdown,
507  versHtml: documentVersHtml,
508  versDiapos: documentVersDiapos,
509  versTableur: documentVersTableur,
510  versModele: documentVersModele,
511  /* LES PONTS. Ils traduisent, ils ne fabriquent pas : `docx`, `exceljs` et
512     `pptxgenjs` font les fichiers, sur le serveur. */
513  versSections: documentVersSections,
514  versXlsx: documentVersXlsx,
515  versPptx: documentVersPptx,
516  VARIABLES: VARIABLES_CREA,
517}
518if (typeof window !== 'undefined') window.DOCUMENT_CREA = DOCUMENT_CREA

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.