@aster-editor/word 0.0.0-stage → 0.4.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.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +359 -2
  3. package/dist/docx/import-docx.d.ts +11 -0
  4. package/dist/docx/import-docx.d.ts.map +1 -0
  5. package/dist/docx/import-docx.js +199 -0
  6. package/dist/docx/import-docx.js.map +1 -0
  7. package/dist/docx/postprocess/dom-utils.d.ts +18 -0
  8. package/dist/docx/postprocess/dom-utils.d.ts.map +1 -0
  9. package/dist/docx/postprocess/dom-utils.js +64 -0
  10. package/dist/docx/postprocess/dom-utils.js.map +1 -0
  11. package/dist/docx/postprocess/footnotes.d.ts +10 -0
  12. package/dist/docx/postprocess/footnotes.d.ts.map +1 -0
  13. package/dist/docx/postprocess/footnotes.js +86 -0
  14. package/dist/docx/postprocess/footnotes.js.map +1 -0
  15. package/dist/docx/postprocess/normalize-images.d.ts +8 -0
  16. package/dist/docx/postprocess/normalize-images.d.ts.map +1 -0
  17. package/dist/docx/postprocess/normalize-images.js +23 -0
  18. package/dist/docx/postprocess/normalize-images.js.map +1 -0
  19. package/dist/docx/postprocess/normalize-lists.d.ts +8 -0
  20. package/dist/docx/postprocess/normalize-lists.d.ts.map +1 -0
  21. package/dist/docx/postprocess/normalize-lists.js +54 -0
  22. package/dist/docx/postprocess/normalize-lists.js.map +1 -0
  23. package/dist/docx/postprocess/normalize-tables.d.ts +10 -0
  24. package/dist/docx/postprocess/normalize-tables.d.ts.map +1 -0
  25. package/dist/docx/postprocess/normalize-tables.js +92 -0
  26. package/dist/docx/postprocess/normalize-tables.js.map +1 -0
  27. package/dist/docx/postprocess/run-postprocess.d.ts +15 -0
  28. package/dist/docx/postprocess/run-postprocess.d.ts.map +1 -0
  29. package/dist/docx/postprocess/run-postprocess.js +32 -0
  30. package/dist/docx/postprocess/run-postprocess.js.map +1 -0
  31. package/dist/docx/postprocess/strip-empty-paragraphs.d.ts +7 -0
  32. package/dist/docx/postprocess/strip-empty-paragraphs.d.ts.map +1 -0
  33. package/dist/docx/postprocess/strip-empty-paragraphs.js +32 -0
  34. package/dist/docx/postprocess/strip-empty-paragraphs.js.map +1 -0
  35. package/dist/docx/postprocess/types.d.ts +14 -0
  36. package/dist/docx/postprocess/types.d.ts.map +1 -0
  37. package/dist/docx/postprocess/types.js +2 -0
  38. package/dist/docx/postprocess/types.js.map +1 -0
  39. package/dist/docx/postprocess/unwrap-cell-paragraphs.d.ts +8 -0
  40. package/dist/docx/postprocess/unwrap-cell-paragraphs.d.ts.map +1 -0
  41. package/dist/docx/postprocess/unwrap-cell-paragraphs.js +37 -0
  42. package/dist/docx/postprocess/unwrap-cell-paragraphs.js.map +1 -0
  43. package/dist/docx/postprocess/variables.d.ts +40 -0
  44. package/dist/docx/postprocess/variables.d.ts.map +1 -0
  45. package/dist/docx/postprocess/variables.js +74 -0
  46. package/dist/docx/postprocess/variables.js.map +1 -0
  47. package/dist/docx/preprocess/content-controls.d.ts +10 -0
  48. package/dist/docx/preprocess/content-controls.d.ts.map +1 -0
  49. package/dist/docx/preprocess/content-controls.js +137 -0
  50. package/dist/docx/preprocess/content-controls.js.map +1 -0
  51. package/dist/docx/preprocess/fields.d.ts +12 -0
  52. package/dist/docx/preprocess/fields.d.ts.map +1 -0
  53. package/dist/docx/preprocess/fields.js +169 -0
  54. package/dist/docx/preprocess/fields.js.map +1 -0
  55. package/dist/docx/preprocess/minimal-docx.d.ts +18 -0
  56. package/dist/docx/preprocess/minimal-docx.d.ts.map +1 -0
  57. package/dist/docx/preprocess/minimal-docx.js +45 -0
  58. package/dist/docx/preprocess/minimal-docx.js.map +1 -0
  59. package/dist/docx/preprocess/names.d.ts +23 -0
  60. package/dist/docx/preprocess/names.d.ts.map +1 -0
  61. package/dist/docx/preprocess/names.js +38 -0
  62. package/dist/docx/preprocess/names.js.map +1 -0
  63. package/dist/docx/preprocess/variables.d.ts +63 -0
  64. package/dist/docx/preprocess/variables.d.ts.map +1 -0
  65. package/dist/docx/preprocess/variables.js +255 -0
  66. package/dist/docx/preprocess/variables.js.map +1 -0
  67. package/dist/docx/preprocess/xml-utils.d.ts +59 -0
  68. package/dist/docx/preprocess/xml-utils.d.ts.map +1 -0
  69. package/dist/docx/preprocess/xml-utils.js +130 -0
  70. package/dist/docx/preprocess/xml-utils.js.map +1 -0
  71. package/dist/docx/style-map.d.ts +22 -0
  72. package/dist/docx/style-map.d.ts.map +1 -0
  73. package/dist/docx/style-map.js +78 -0
  74. package/dist/docx/style-map.js.map +1 -0
  75. package/dist/docx/types.d.ts +82 -0
  76. package/dist/docx/types.d.ts.map +1 -0
  77. package/dist/docx/types.js +2 -0
  78. package/dist/docx/types.js.map +1 -0
  79. package/dist/export/blocks.d.ts +39 -0
  80. package/dist/export/blocks.d.ts.map +1 -0
  81. package/dist/export/blocks.js +256 -0
  82. package/dist/export/blocks.js.map +1 -0
  83. package/dist/export/context.d.ts +65 -0
  84. package/dist/export/context.d.ts.map +1 -0
  85. package/dist/export/context.js +11 -0
  86. package/dist/export/context.js.map +1 -0
  87. package/dist/export/css.d.ts +28 -0
  88. package/dist/export/css.d.ts.map +1 -0
  89. package/dist/export/css.js +191 -0
  90. package/dist/export/css.js.map +1 -0
  91. package/dist/export/docx-module.d.ts +15 -0
  92. package/dist/export/docx-module.d.ts.map +1 -0
  93. package/dist/export/docx-module.js +10 -0
  94. package/dist/export/docx-module.js.map +1 -0
  95. package/dist/export/dom.d.ts +34 -0
  96. package/dist/export/dom.d.ts.map +1 -0
  97. package/dist/export/dom.js +109 -0
  98. package/dist/export/dom.js.map +1 -0
  99. package/dist/export/download-docx.d.ts +10 -0
  100. package/dist/export/download-docx.d.ts.map +1 -0
  101. package/dist/export/download-docx.js +39 -0
  102. package/dist/export/download-docx.js.map +1 -0
  103. package/dist/export/export-docx.d.ts +11 -0
  104. package/dist/export/export-docx.d.ts.map +1 -0
  105. package/dist/export/export-docx.js +35 -0
  106. package/dist/export/export-docx.js.map +1 -0
  107. package/dist/export/footnotes.d.ts +19 -0
  108. package/dist/export/footnotes.d.ts.map +1 -0
  109. package/dist/export/footnotes.js +75 -0
  110. package/dist/export/footnotes.js.map +1 -0
  111. package/dist/export/header-footer.d.ts +9 -0
  112. package/dist/export/header-footer.d.ts.map +1 -0
  113. package/dist/export/header-footer.js +64 -0
  114. package/dist/export/header-footer.js.map +1 -0
  115. package/dist/export/html-to-docx.d.ts +17 -0
  116. package/dist/export/html-to-docx.d.ts.map +1 -0
  117. package/dist/export/html-to-docx.js +74 -0
  118. package/dist/export/html-to-docx.js.map +1 -0
  119. package/dist/export/images.d.ts +28 -0
  120. package/dist/export/images.d.ts.map +1 -0
  121. package/dist/export/images.js +170 -0
  122. package/dist/export/images.js.map +1 -0
  123. package/dist/export/inline.d.ts +39 -0
  124. package/dist/export/inline.d.ts.map +1 -0
  125. package/dist/export/inline.js +392 -0
  126. package/dist/export/inline.js.map +1 -0
  127. package/dist/export/json-to-html.d.ts +7 -0
  128. package/dist/export/json-to-html.d.ts.map +1 -0
  129. package/dist/export/json-to-html.js +153 -0
  130. package/dist/export/json-to-html.js.map +1 -0
  131. package/dist/export/sanitize.d.ts +14 -0
  132. package/dist/export/sanitize.d.ts.map +1 -0
  133. package/dist/export/sanitize.js +32 -0
  134. package/dist/export/sanitize.js.map +1 -0
  135. package/dist/export/styles.d.ts +109 -0
  136. package/dist/export/styles.d.ts.map +1 -0
  137. package/dist/export/styles.js +315 -0
  138. package/dist/export/styles.js.map +1 -0
  139. package/dist/export/tables.d.ts +17 -0
  140. package/dist/export/tables.d.ts.map +1 -0
  141. package/dist/export/tables.js +228 -0
  142. package/dist/export/tables.js.map +1 -0
  143. package/dist/export/toc.d.ts +12 -0
  144. package/dist/export/toc.d.ts.map +1 -0
  145. package/dist/export/toc.js +32 -0
  146. package/dist/export/toc.js.map +1 -0
  147. package/dist/export/types.d.ts +179 -0
  148. package/dist/export/types.d.ts.map +1 -0
  149. package/dist/export/types.js +2 -0
  150. package/dist/export/types.js.map +1 -0
  151. package/dist/export/variables.d.ts +57 -0
  152. package/dist/export/variables.d.ts.map +1 -0
  153. package/dist/export/variables.js +121 -0
  154. package/dist/export/variables.js.map +1 -0
  155. package/dist/extension/word-paste.d.ts +10 -0
  156. package/dist/extension/word-paste.d.ts.map +1 -0
  157. package/dist/extension/word-paste.js +27 -0
  158. package/dist/extension/word-paste.js.map +1 -0
  159. package/dist/google/normalize-google-docs-html.d.ts +12 -0
  160. package/dist/google/normalize-google-docs-html.d.ts.map +1 -0
  161. package/dist/google/normalize-google-docs-html.js +116 -0
  162. package/dist/google/normalize-google-docs-html.js.map +1 -0
  163. package/dist/index.d.ts +25 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +9 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/paste/detectors.d.ts +16 -0
  168. package/dist/paste/detectors.d.ts.map +1 -0
  169. package/dist/paste/detectors.js +39 -0
  170. package/dist/paste/detectors.js.map +1 -0
  171. package/dist/paste/dom-utils.d.ts +39 -0
  172. package/dist/paste/dom-utils.d.ts.map +1 -0
  173. package/dist/paste/dom-utils.js +154 -0
  174. package/dist/paste/dom-utils.js.map +1 -0
  175. package/dist/paste/normalize-word-html.d.ts +29 -0
  176. package/dist/paste/normalize-word-html.d.ts.map +1 -0
  177. package/dist/paste/normalize-word-html.js +75 -0
  178. package/dist/paste/normalize-word-html.js.map +1 -0
  179. package/dist/paste/passes/clean-inline-styles.d.ts +18 -0
  180. package/dist/paste/passes/clean-inline-styles.d.ts.map +1 -0
  181. package/dist/paste/passes/clean-inline-styles.js +213 -0
  182. package/dist/paste/passes/clean-inline-styles.js.map +1 -0
  183. package/dist/paste/passes/collapse-empty-paragraphs.d.ts +9 -0
  184. package/dist/paste/passes/collapse-empty-paragraphs.d.ts.map +1 -0
  185. package/dist/paste/passes/collapse-empty-paragraphs.js +55 -0
  186. package/dist/paste/passes/collapse-empty-paragraphs.js.map +1 -0
  187. package/dist/paste/passes/normalize-headings.d.ts +12 -0
  188. package/dist/paste/passes/normalize-headings.d.ts.map +1 -0
  189. package/dist/paste/passes/normalize-headings.js +51 -0
  190. package/dist/paste/passes/normalize-headings.js.map +1 -0
  191. package/dist/paste/passes/normalize-tables.d.ts +10 -0
  192. package/dist/paste/passes/normalize-tables.d.ts.map +1 -0
  193. package/dist/paste/passes/normalize-tables.js +105 -0
  194. package/dist/paste/passes/normalize-tables.js.map +1 -0
  195. package/dist/paste/passes/normalize-whitespace.d.ts +11 -0
  196. package/dist/paste/passes/normalize-whitespace.d.ts.map +1 -0
  197. package/dist/paste/passes/normalize-whitespace.js +124 -0
  198. package/dist/paste/passes/normalize-whitespace.js.map +1 -0
  199. package/dist/paste/passes/rebuild-lists.d.ts +20 -0
  200. package/dist/paste/passes/rebuild-lists.d.ts.map +1 -0
  201. package/dist/paste/passes/rebuild-lists.js +350 -0
  202. package/dist/paste/passes/rebuild-lists.js.map +1 -0
  203. package/dist/paste/passes/strip-conditional-comments.d.ts +15 -0
  204. package/dist/paste/passes/strip-conditional-comments.d.ts.map +1 -0
  205. package/dist/paste/passes/strip-conditional-comments.js +35 -0
  206. package/dist/paste/passes/strip-conditional-comments.js.map +1 -0
  207. package/dist/paste/passes/strip-office-namespaces.d.ts +10 -0
  208. package/dist/paste/passes/strip-office-namespaces.d.ts.map +1 -0
  209. package/dist/paste/passes/strip-office-namespaces.js +45 -0
  210. package/dist/paste/passes/strip-office-namespaces.js.map +1 -0
  211. package/dist/paste/passes/variables.d.ts +38 -0
  212. package/dist/paste/passes/variables.d.ts.map +1 -0
  213. package/dist/paste/passes/variables.js +217 -0
  214. package/dist/paste/passes/variables.js.map +1 -0
  215. package/dist/paste/types.d.ts +64 -0
  216. package/dist/paste/types.d.ts.map +1 -0
  217. package/dist/paste/types.js +18 -0
  218. package/dist/paste/types.js.map +1 -0
  219. package/package.json +58 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joris Tremoulet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,360 @@
1
- # Temporary Holding Version
1
+ # 📄 @aster-editor/word
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Import et export `.docx`, nettoyage du collage depuis Word et Google Docs pour Aster Editor. Produit du HTML sémantique que le schéma ProseMirror accepte tel quel, et un fichier Word depuis ce même HTML.
4
+
5
+ ## 📦 Installation
6
+
7
+ ```bash
8
+ npm install @aster-editor/word
9
+ ```
10
+
11
+ Peer dependency : `@aster-editor/core`. Dépendance embarquée : `mammoth` (BSD-2), chargé en **import dynamique** uniquement quand `importDocx` est appelé.
12
+
13
+ ## 📥 `importDocx`
14
+
15
+ ```ts
16
+ type DocxInput = File | ArrayBuffer | ArrayBufferView;
17
+ function importDocx(input: DocxInput, options?: DocxImportOptions): Promise<DocxImportResult>;
18
+ ```
19
+
20
+ Convertit un `.docx` en HTML prêt pour `editor.setHTML` ou `editor.insertHTML`. L'entrée (`DocxInput`) est un `File`, un `ArrayBuffer` ou une vue typée (`Uint8Array`, `Buffer` Node) dont seule la fenêtre `byteOffset`/`byteLength` est lue. Ne lève jamais pour un document Word valide : ce qui n'est pas converti fidèlement remonte dans `warnings`. Rejette une erreur nommée `DocxImportError` (message préfixé `invalid-docx:`) si l'entrée n'est pas de l'un de ces types (`null`, chaîne, objet quelconque), n'est pas une archive zip, ne peut pas être lue (`arrayBuffer()` qui rejette) ou si mammoth ne peut pas la décoder.
21
+
22
+ ### Options (`DocxImportOptions`)
23
+
24
+ | Champ | Type | Défaut | Rôle |
25
+ | ----------- | --------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | `upload` | `UploadHandler` | — | Hook `(file: File) => Promise<string>` appelé pour chaque image embarquée. Sans hook : data URL base64. |
27
+ | `footnotes` | `boolean` | `true` | Conserver les notes de bas de page et de fin en liste finale. `false` les supprime avec un avertissement. |
28
+ | `styleMap` | `readonly string[]` | `[]` | Règles mammoth additionnelles (`p[style-name='X'] => h2:fresh`), ajoutées **après** celles d'Aster, qui restent prioritaires. Lignes vides et `#` ignorées. |
29
+ | `parse` | `HtmlParser` | `new DOMParser()` | Parseur HTML injectable (`{ parseFromString(html, 'text/html'): Document }`) pour SSR ou tests. |
30
+ | `variables` | `DocxVariableOptions` | tout actif | `{ detect?, pattern? }` : sources de variables reconnues et motif alternatif à `{{nom}}`. Voir [Variables](#variables). |
31
+
32
+ ### Résultat (`DocxImportResult`)
33
+
34
+ | Champ | Type | Rôle |
35
+ | ----------- | ------------------------------ | -------------------------------------------------------------------------------------------------- |
36
+ | `html` | `string` | HTML sémantique, `body.innerHTML`. |
37
+ | `warnings` | `readonly DocxImportWarning[]` | `{ code, message }`, vide si tout est passé. |
38
+ | `variables` | `readonly AsterVariable[]` | Variables trouvées (`name`, `label?`, `value?`), ordre d'apparition. Voir [Variables](#variables). |
39
+
40
+ ### Codes d'avertissement (`DocxImportWarningCode`)
41
+
42
+ | Code | Sens |
43
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `mammoth` | Message mammoth remonté tel quel (`warning: Unrecognised paragraph style…`, image illisible). |
45
+ | `image-upload-failed` | Le hook `upload` a rejeté ou résolu autre chose qu'une chaîne non vide ; l'image est repliée en base64. |
46
+ | `footnotes-dropped` | Des notes existaient et `footnotes: false` les a supprimées. |
47
+ | `unsupported-style` | Réservé aux post-traitements : élément non converti fidèlement. |
48
+ | `variables-preprocess-failed` | Le prétraitement des champs de fusion et contrôles de contenu a échoué (XML illisible, pas de `DOMParser` XML) ; le document est importé sans, les `{{nom}}` saisis restent détectés. |
49
+
50
+ ### Ce que l'import produit
51
+
52
+ - **Titres** : `Title`/`Titre` et `Heading 1..6`/`Titre 1..6` (noms localisés et identifiants internes `Heading1`, `Titre1`) → `h1..h6`.
53
+ - **Listes** : numérotation native Word (`w:numPr`) → `ul`/`ol` imbriqués ; le texte de chaque `li` est enveloppé dans un `<p>` (schéma Tiptap `listItem = paragraph block*`), les sous-listes orphelines sont rattachées au `li` précédent.
54
+ - **Marks** : gras, italique, `Strong`/`Fort` → `strong`, `Emphasis`/`Accentuation` → `em`, souligné → `u`, barré → `s`, surlignage → `mark`, style `Code` → `code`.
55
+ - **Citations** : `Quote`, `Intense Quote`, `Citation`, `Citation intense` → `blockquote`, fusionnées quand elles se suivent.
56
+ - **Code** : styles `Code`, `HTML Preformatted` → `pre`, lignes consécutives dans un seul bloc.
57
+ - **Tables** : `thead` + `th` si Word a marqué la ligne d'en-tête ou si toute la première ligne est en gras, `tbody` pour le reste, `colspan`/`rowspan` conservés, `<p>` unique des cellules déballé.
58
+ - **Images** : `src` = URL du hook ou data URL, `alt` toujours présent (vide si absent), `width`/`height` nuls retirés.
59
+ - **Notes** : liste `<ol data-footnotes>` en fin de document, appels `[1]` en texte simple, identifiants dans `data-footnote`. Note : le schéma Tiptap ne conserve pas `data-footnote` après `setHTML`.
60
+ - **Liens** : `<a href>` natifs mammoth, puis filtrés par la politique de liens du core à l'insertion.
61
+ - **Variables** : `<span data-aster-variable="nom" data-label="…">{{nom}}</span>`, voir ci-dessous.
62
+ - Paragraphes vides supprimés.
63
+
64
+ ### Variables
65
+
66
+ Trois sources sont reconnues et ramenées à la même puce `<span data-aster-variable="nom">{{nom}}</span>`, que le core transforme en variable à l'insertion :
67
+
68
+ | Source (`detect`) | Dans Word | Ce qu'on garde |
69
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | `braces` | `{{nom}}` tapé dans le texte, espaces intérieurs tolérés (`{{ nom }}`), y compris dans un titre, une liste, une cellule, une marque, ou coupé sur deux runs. | Le nom. Accolades seules et `{{` incomplet restent du texte ; `<pre>` et `<code>` ne sont jamais analysés. |
71
+ | `mergeFields` | Champs de fusion `MERGEFIELD nom`, forme simple (`w:fldSimple`) ou complexe (`w:fldChar begin … end`, instruction répartie sur plusieurs runs). Un nom, entre guillemets ou non, est gardé tel quel s'il respecte la règle des noms (`"Prénom"` → `Prénom`) ; sinon il est normalisé : minuscules, accents retirés, espaces → `_` (`"Nom complet"` → `nom_complet`). | Le nom, la mise en forme du run et **le résultat affiché** comme valeur connue (`value`), sauf s'il est vide ou n'est que le placeholder `«nom»` / `{{nom}}` : un export `fields` avec `values` se réimporte donc avec ses valeurs. Les autres champs (`HYPERLINK`, `PAGE`, `TOC`, `IF`…) sont intacts. |
72
+ | `contentControls` | Contrôles de contenu `w:sdt` avec `w:tag` (nom) et `w:alias` (libellé) ; sans tag, l'alias normalisé sert de nom. Un contrôle de ligne (dans un paragraphe) devient un run ; un contrôle de bloc fait de paragraphes devient un paragraphe. Sans tag ni alias, ou pour une table des matières, un groupe, une case à cocher, ou un bloc qui contient autre chose que des paragraphes (table, ligne, cellule), le contrôle est laissé tel quel. Liste déroulante, zone de liste et date donnent la valeur affichée. | Le nom, le libellé (`data-label`, `label`) et **la valeur saisie** (`value`), sauf si le contrôle affiche encore son texte de remplissage (`w:showingPlcHdr`). Un tag présent plusieurs fois garde le libellé et la valeur de sa première apparition. Une variable imbriquée dans un contrôle de bloc remplacé n'est pas listée : elle n'est plus dans le document. |
73
+
74
+ Les champs et contrôles sont réécrits dans le XML du `.docx` **avant** mammoth, car mammoth perd les premiers et traverse les seconds. Les parties traitées sont localisées par les relations du paquet : le corps désigné par `_rels/.rels` (`word/document.xml` en général), puis ses en-têtes, pieds de page et notes d'après `word/_rels/document.xml.rels` ; sans ce fichier, repli sur les noms usuels (`header1.xml`, `footer1.xml`, `footnotes.xml`, `endnotes.xml`). Une partie sans champ ni contrôle n'est pas parsée. Le prétraitement ne lève jamais : en cas d'échec, le document est importé sans et un avertissement `variables-preprocess-failed` est émis. `result.variables` liste les variables du HTML dans l'ordre d'apparition, puis celles trouvées dans les en-têtes et pieds de page (que mammoth ne rend pas) ; donne-la au core avec `editor.variables.define(result.variables)`.
75
+
76
+ Les `{{nom}}` tapés dans un en-tête ou un pied de page sont eux aussi remontés dans `result.variables` (sans libellé ni valeur) quand `braces` est actif, avec `pattern` s'il est fourni. Un `{{nom}}` dont les accolades et le nom sont répartis sur plusieurs marques d'un même paragraphe (`<strong>{{</strong>nom}}`, après une correction dans Word) est reconnu : la puce est posée à la place du premier fragment, dans ses marques, et les fragments suivants sont retirés. Il n'est jamais recollé à travers un saut de ligne, une puce ou un bloc.
77
+
78
+ ```ts
79
+ const { html, variables } = await importDocx(file, {
80
+ variables: { detect: ['braces', 'contentControls'], pattern: /\[([a-z_]+)\]/g },
81
+ });
82
+ editor.setHTML(html);
83
+ editor.variables.define(variables); // libellés et valeurs connues
84
+ ```
85
+
86
+ `pattern` remplace la syntaxe `{{nom}}` du texte libre par un motif dont le **groupe 1 est le nom** (validé par la règle du core : lettres, chiffres, `_`, `.`, `-`). Les runs `{{nom}}` produits par le prétraitement restent reconnus, mais seulement pour les noms qu'il a collectés. Un motif à backtracking exponentiel est à la charge de l'intégrateur. `detect: []` désactive tout.
87
+
88
+ ### Exemple avec `<input type="file">`
89
+
90
+ ```ts
91
+ import { createAster } from '@aster-editor/ui';
92
+ import { importDocx } from '@aster-editor/word';
93
+
94
+ const { editor } = createAster(document.querySelector<HTMLElement>('#editor')!);
95
+ const input = document.querySelector<HTMLInputElement>('#docx')!;
96
+
97
+ input.addEventListener('change', async () => {
98
+ const file = input.files?.[0];
99
+ if (!file) return;
100
+ try {
101
+ const { html, warnings } = await importDocx(file, {
102
+ upload: async (image) => {
103
+ const body = new FormData();
104
+ body.append('file', image);
105
+ const response = await fetch('/api/uploads', { method: 'POST', body });
106
+ const { url } = (await response.json()) as { url: string };
107
+ return url;
108
+ },
109
+ });
110
+ editor.setHTML(html);
111
+ for (const warning of warnings) console.warn(`[${warning.code}] ${warning.message}`);
112
+ } catch (error) {
113
+ console.error('Fichier .docx illisible', error);
114
+ }
115
+ });
116
+ ```
117
+
118
+ Avec un `ArrayBuffer` (fetch, test) : `await importDocx(await response.arrayBuffer())`. Côté Node ou SSR, un `Buffer` passe tel quel : `await importDocx(await readFile('rapport.docx'))`.
119
+
120
+ ## 🧹 `normalizeWordHtml` et `normalizeGoogleDocsHtml`
121
+
122
+ ```ts
123
+ function normalizeWordHtml(html: string, options?: WordPasteOptions): string;
124
+ function normalizeGoogleDocsHtml(html: string, options?: WordPasteOptions): string;
125
+ function isWordHtml(html: string): boolean;
126
+ function isGoogleDocsHtml(html: string): boolean;
127
+ ```
128
+
129
+ Fonctions **pures** : elles parsent le HTML dans un `Document` neuf, ne touchent jamais au `document` global et renvoient `body.innerHTML` nettoyé. Chaîne vide → chaîne vide. Le fragment réellement copié est isolé entre `<!--StartFragment-->` et `<!--EndFragment-->` quand les marqueurs existent ; si `EndFragment` manque (presse-papier tronqué), la balise ouverte et jamais fermée en fin de chaîne est retirée plutôt que rendue en texte.
130
+
131
+ Les deux normaliseurs sont aussi une barrière : balises actives supprimées avec leur contenu (`script`, `iframe`, `object`, `embed`, `form`, `template`, `noscript`, `frame`, `frameset`, `svg`, `math`, `meta`, `style`), tout attribut `on*` jeté, liens limités aux schémas `http`, `https`, `mailto`, `tel` (les autres sont déballés en texte), images limitées à `http(s)`, `data`, `blob`, `file` et relatif, valeur de `color` validée avant conservation. Le core applique ensuite sa propre politique de liens et d'images à l'insertion (`file:` et `data:` non-image y sont rejetés). Cette barrière est figée par une suite de tests dédiée (`paste/security.spec.ts`) qui couvre aussi les deux regex quadratiques corrigées en 0.3.0 (`isWordHtml`, `parseListFormats` : un collage de 2 Mo se détecte en moins d'une milliseconde) et les fragments tronqués au milieu d'un vecteur.
132
+
133
+ ### Options (`WordPasteOptions`)
134
+
135
+ | Champ | Type | Défaut | Rôle |
136
+ | --------------- | ---------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
137
+ | `keepColors` | `boolean` | `false` | Conserver `color` explicite. Jamais le noir / `windowtext` par défaut, jamais dans un lien. |
138
+ | `keepAlignment` | `boolean` | `true` | Conserver `text-align` (`center`, `right`, `justify` ; `left` est le défaut et tombe). |
139
+ | `parse` | `(html: string) => Document` | `DOMParser` | Parseur injectable pour tests ou environnement sans DOM. |
140
+ | `variables` | `boolean \| { pattern? }` | `true` | `{{nom}}` collé → puce `<span data-aster-variable>`. `false` désactive, `{ pattern }` remplace la syntaxe (groupe 1 = nom). |
141
+
142
+ ### Les 9 passes Word, dans l'ordre
143
+
144
+ | # | Passe | Ce qu'elle fait |
145
+ | --- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
146
+ | 1 | `stripConditionalComments` | Avant parsing : jette `<!--[if gte mso 9]>…<![endif]-->` (XML de réglages, VML) et les marqueurs `<![if !supportLists]>`. Après parsing : supprime tous les nœuds commentaire. |
147
+ | 2 | `stripOfficeNamespaces` | Supprime avec leur contenu `<xml>`, `<style>`, `<script>`, `<meta>`, `<link>`, `<title>`, `<head>` et les balises actives `<template>`, `<iframe>`, `<object>`, `<embed>`, `<form>`, `<noscript>`, `<frame>`, `<frameset>`, `<svg>`, `<math>` ; déballe les balises préfixées `<o:p>`, `<w:*>`, `<v:*>`, `<m:*>` en gardant leur texte. |
148
+ | 3 | `rebuildLists` | Lit `mso-list:lX levelN lfoM` sur les paragraphes et, dans le `<style>`, `@list lX:levelN { mso-level-number-format; mso-level-start-at }` puis les overrides d'instance `@list lX:levelN lfoM {…}` qui priment ; reconstruit `<ul>`/`<ol>` imbriqués, pose `type` (`a`, `A`, `i`, `I`) et `start` (lu dans le marqueur, sinon `mso-level-start-at`). Sans feuille de style, décide via le texte du marqueur : `1.`, `1.1.` décimal ; `c.`, `d.`, `l.`, `m.` isolés alpha ; `i.`, `v.`, `x.` isolés romains sauf si le marqueur suivant est `j.`, `w.`, `y.` ; deux lettres romaines ou plus (`ii.`, `iv.`) romain. Chaque instance `lfo` est une liste distincte. |
149
+ | 4 | `normalizeHeadings` | `MsoTitle` → `h1`, `MsoHeading1..6` → `h1..h6` (plafonné à 6), `MsoQuote` / `MsoIntenseQuote` → `blockquote` fusionnés. |
150
+ | 5 | `cleanInlineStyles` | Jette `mso-*`, polices, tailles, interlignes, marges, fonds, `class`, `lang` ; traduit `font-weight:bold` → `strong`, `font-style:italic` → `em`, `underline` → `u`, `line-through` → `s`, `vertical-align` → `sub`/`sup`, `mso-highlight` → `mark` ; renomme `b`/`i`/`strike`/`del`/`ins` ; ne garde que `href`/`title`/`target`/`rel` sur `a`, `src`/`alt`/`title`/`width`/`height` sur `img`, `start`/`type` sur `ol` ; déballe les `<a>` sans `href` ou d'un schéma autre que `http`, `https`, `mailto`, `tel`, supprime les `<img>` à `src` actif ; avec `keepColors`, ne garde `color` que si la valeur est un hexa, un nom CSS ou un `rgb()`/`hsl()` ; déballe les `span` et `font` vides. |
151
+ | 6 | `normalizeTables` | Retire `border`, `cellspacing`, `width`, `valign`, styles ; garde `colspan`/`rowspan` > 1 ; première ligne toute en gras → `<thead><th>` (le `mso-yfti-firstrow:yes` de Word n'est pas un indice : il marque la première ligne de toute table) ; déballe le `<p>` unique des cellules sauf s'il porte encore un attribut (alignement). Une table à une ligne n'a pas d'en-tête. |
152
+ | 7 | `collapseEmptyParagraphs` | Supprime `<p>&nbsp;</p>`, `<p><br></p>`, les `<br>` en fin de bloc et les `<br>` orphelins. Garde un `<p></p>` si tout se vide. |
153
+ | 8 | `normalizeWhitespace` | `&nbsp;` entre mots → espace, suites d'espaces réduites, trim en début et fin de bloc (seulement pour un texte réellement en bord de bloc : l'espace entre un texte et une image inline reste), espaces de bord sortis des marks (`<em>a</em><u> b</u>` → `<em>a</em> <u>b</u>`), blancs d'indentation entre blocs supprimés. `<pre>` intact. |
154
+ | 9 | `wrapVariables` | `{{nom}}` dans les nœuds texte (hors `<pre>`, `<code>`) → `<span data-aster-variable="nom">{{nom}}</span>`, après fusion des textes adjacents pour recoller un `{{` coupé sur deux spans. Espaces intérieurs tolérés, accolades seules ignorées, nom validé par la règle du core. Option `variables` (`false`, ou `{ pattern }`). |
155
+
156
+ Le pipeline Google Docs est plus court : passes 1, 2, puis déballage du `<b style="font-weight:normal" id="docs-internal-guid-…">`, réimbrication des sous-listes (`<ul><li>a</li><ul>…</ul></ul>` → dans le `li`), remontée du `list-style-type` des `<li>` (`lower-alpha`, `upper-alpha`, `lower-roman`, `upper-roman`) en attribut `type` du `<ol>`, passe 5, déballage du `<div>` nu que Google pose autour de chaque table, puis passes 6 à 9.
157
+
158
+ ```ts
159
+ import { isWordHtml, normalizeWordHtml } from '@aster-editor/word';
160
+
161
+ const html = event.clipboardData?.getData('text/html') ?? '';
162
+ if (isWordHtml(html)) {
163
+ const clean = normalizeWordHtml(html, { keepColors: true });
164
+ editor.insertHTML(clean);
165
+ }
166
+ ```
167
+
168
+ Détection : `isWordHtml` cherche `urn:schemas-microsoft-com:office:word`, `class="Mso…"`, `mso-*:`, `<o:p>` ou le `ProgId Word.Document` ; `isGoogleDocsHtml` cherche `id="docs-internal-guid-`. Travail sur la chaîne, sans parser.
169
+
170
+ ## 🔌 `wordPasteTransformer`
171
+
172
+ ```ts
173
+ function wordPasteTransformer(options?: WordPasteOptions): PasteTransformer;
174
+ ```
175
+
176
+ Fabrique le transformateur à passer dans `pasteTransformers` : détecte Word puis Google Docs, applique le normaliseur adapté, laisse tout autre HTML intact. En cas d'erreur interne, journalise un `console.warn` et rend le HTML d'origine : un collage ne casse jamais l'éditeur.
177
+
178
+ ```ts
179
+ import { createEditor } from '@aster-editor/core';
180
+ import { wordPasteTransformer } from '@aster-editor/word';
181
+
182
+ const editor = createEditor({
183
+ element,
184
+ pasteTransformers: [wordPasteTransformer({ keepAlignment: false })],
185
+ });
186
+ ```
187
+
188
+ Avec `createAster` de `@aster-editor/ui`, même option : `createAster(host, { pasteTransformers: [wordPasteTransformer()] })`.
189
+
190
+ ## 📤 Export .docx
191
+
192
+ ```ts
193
+ function exportDocx(input: string | AsterDocument, options?: DocxExportOptions): Promise<Blob>;
194
+ function downloadDocx(blob: Blob, filename?: string): void;
195
+ function jsonToHtml(document: AsterDocument): string;
196
+ ```
197
+
198
+ Convertit le HTML d'Aster (`editor.getHTML()`) ou un document JSON ProseMirror (`editor.getJSON()`) en fichier Word. Rend un `Blob` de type `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, prêt pour `downloadDocx` ou un upload. Ne lève jamais pour du HTML valide : ce qui n'a pas d'équivalent Word remonte via `options.onWarning`. La lib `docx` (MIT, ~1,3 MB non minifié) est chargée en **import dynamique** au premier appel, comme mammoth pour l'import : elle n'entre dans le bundle initial que si ton bundler ne fait pas de code-splitting.
199
+
200
+ Le JSON passe par `jsonToHtml`, sérialiseur minimal limité au schéma d'Aster (StarterKit, tables, images, alignement, surlignage, indice/exposant, `textStyle`, tâches). Pour un schéma étendu par tes propres extensions, donne plutôt `editor.getHTML()` à `exportDocx`.
201
+
202
+ ### Options (`DocxExportOptions`)
203
+
204
+ | Champ | Type | Défaut | Rôle |
205
+ | ------------------ | --------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
206
+ | `parse` | `HtmlParser` | `new DOMParser()` | Parseur HTML injectable, jamais le `document` global (SSR, tests). |
207
+ | `fetchImage` | `(src: string) => Promise<ArrayBuffer\|null>` | — | Récupère les octets d'une image `http(s)://`. Sans hook, seules les data URL sont embarquées, les autres sont ignorées avec avertissement. |
208
+ | `title` | `string` | — | Titre inscrit dans les propriétés du document. |
209
+ | `creator` | `string` | — | Auteur inscrit dans les propriétés du document. |
210
+ | `pageSize` | `'A4' \| 'LETTER'` | `'A4'` | Format de page, marges de 2,54 cm. `page.size` prime. |
211
+ | `page` | `DocxPageOptions` | — | `size`, `orientation` (`portrait`/`landscape`), `margins` en mm (`top`, `right`, `bottom`, `left`, 25,4 mm par défaut). |
212
+ | `lang` | `string` | — | Langue du document (`w:lang`, ex. `fr-FR`) pour le correcteur de Word. |
213
+ | `defaultFont` | `{ family?, sizePt? }` | — | Police et taille du texte courant (`docDefaults`). |
214
+ | `variables` | `DocxExportVariableOptions` | `placeholders` | Rendu des variables : `mode` (`placeholders`/`resolve`/`fields`), `values`, `pattern`. Voir [Variables à l'export](#variables-à-lexport). |
215
+ | `header` | `string \| DocxHeaderOptions` | — | En-tête : `text` brut ou `html` inline (gras, liens, variables), `align`. |
216
+ | `footer` | `string \| DocxFooterOptions` | — | Pied de page : idem, plus `pageNumbers: true` (`PAGE`) ou `'x-of-y'` (`PAGE / NUMPAGES`). |
217
+ | `toc` | `boolean \| { title?, levels? }` | — | Table des matières en tête du corps, 3 niveaux par défaut. Word propose de la mettre à jour à l'ouverture. |
218
+ | `numberedHeadings` | `boolean \| { levels?, format? }` | — | Numérotation `1.`, `1.1.`… (`decimal`) ou `1`, `1.1` (`legal`) liée aux styles `Heading 1..n`, 3 niveaux par défaut. |
219
+ | `headingStyles` | `{ h1?: { color?, sizePt?, bold?, italics?, font? }, … }` | — | Surcharges des styles de titres ; sans option, les titres restent ceux de Word. |
220
+ | `onWarning` | `(warning: DocxExportWarning) => void` | — | Reçoit chaque avertissement au fil de la conversion. Une exception levée par le hook n'est pas rattrapée : `exportDocx` rejette. |
221
+
222
+ ### Codes d'avertissement (`DocxExportWarningCode`)
223
+
224
+ | Code | Sens |
225
+ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------- |
226
+ | `image-skipped` | Image ignorée : URL distante sans `fetchImage`, hook en échec ou `null`, data URL illisible, format non raster (SVG, WebP). |
227
+ | `unsupported-element` | `iframe`, `video`, `audio`, `canvas`, `svg`, `form`… sans équivalent Word, ignorés. |
228
+ | `variable-unresolved` | Mode `resolve` : variable sans valeur dans `values`, laissée en `{{nom}}`. |
229
+ | `footnote-missing` | `<sup data-footnote-ref="n">` sans note `n` dans la liste `data-footnotes`, laissé en exposant. |
230
+ | `footnote-not-allowed` | Appel de note dans un en-tête, un pied de page ou une autre note : Word l'y refuse, laissé en exposant `[n]`. |
231
+ | `footnote-duplicate` | Seconde note déclarée sous une clé déjà prise (ignorée), ou second appel vers une note déjà référencée (exposant `[n]`). |
232
+ | `table-span-clamped` | `colspan` ou `<col span>` borné à 63 colonnes, `rowspan` à 65 534 lignes : plafonds de Word. |
233
+ | `page-margins-clamped` | Marge négative ou non finie (défaut 25,4 mm appliqué), ou somme des marges qui ne laisse pas 1 cm utile (marges réduites). |
234
+
235
+ `DocxWarning<Code>` est le type générique commun : `DocxImportWarning` et `DocxExportWarning` en sont deux spécialisations structurellement compatibles.
236
+
237
+ ### Mapping HTML → Word
238
+
239
+ | HTML | Word |
240
+ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
241
+ | `h1`..`h6` | Styles `Heading 1`..`Heading 6` |
242
+ | `p`, `text-align` | Paragraphe, alignement `center`/`right`/`justify` (`left` est le défaut) |
243
+ | `strong`/`b`, `em`/`i`, `u`, `s` | Gras, italique, souligné, barré (aussi lus depuis `font-weight`, `font-style`, `text-decoration`) |
244
+ | `sub`, `sup` | Indice, exposant |
245
+ | `code`, `kbd`, `samp` | Style de caractère `Code`, police Consolas |
246
+ | `mark`, `background-color` | Surlignage jaune |
247
+ | `span style="color / font-family / font-size"` | Couleur (`#hex`, `rgb()` en nombres ou `%`, `hsl()`, noms courants), police, taille (`px`, `pt`, `em`, `rem`, `%` et mots-clés `small`, `large`… relatifs à 11 pt) |
248
+ | `a href` | Lien externe (`http`, `https`, `mailto`, `tel`, `ftp` ; autres schémas → texte simple). Espaces, `\|` et non-ASCII sont percent-encodés dans la cible, les `%xx` existants conservés. |
249
+ | `ul`, `ol`, `ol type="a/A/i/I"`, `ol start` | Numérotation Word sur 9 niveaux : puces, décimal, lettres, romains. Chaque liste (racine ou imbriquée) a sa propre numérotation et repart à 1, ou au `start` de l'`<ol>`. Le premier bloc d'un `li` porte la puce. |
250
+ | `blockquote` | Style de paragraphe `Quote` (italique, indenté) |
251
+ | `pre` | Une ligne = un paragraphe de style `HTML Preformatted` (Consolas) |
252
+ | `hr` | Paragraphe vide à bordure basse |
253
+ | `br` | Saut de ligne. Les `<br>` terminaux d'un paragraphe qui a d'autre contenu sont ignorés, comme en HTML (`<p><br></p>` garde le sien). |
254
+ | `img` | Image inline PNG/JPEG/GIF/BMP, dimensions lues dans `width`/`height` ou l'en-tête du fichier, plafonnées à la largeur de page |
255
+ | `table`, `thead`, `th`, `colspan`, `rowspan` | Table 100 % avec bordures, lignes d'en-tête marquées (`thead` ou ligne entièrement en `th`) et en gras, fusions horizontales et verticales. `colspan`/`<col span>` bornés à 63 et `rowspan` à 65 534 (plafonds de Word, avertissement `table-span-clamped`) |
256
+ | `div`, `section`, `article`… | Traversés ; les inline nus deviennent des paragraphes |
257
+ | `td colwidth`, `style="width"`, `col` | Largeurs de colonnes en DXA (1 px = 15 twips) : grille `w:tblGrid` + `w:tcW` par cellule et disposition fixe quand toutes les colonnes sont connues (réduites à l'échelle si la table déborde de la page) ; sinon table à 100 % et largeur sur les seules cellules connues |
258
+ | `div data-page-break`, `hr data-page-break` | Saut de page (`w:br w:type="page"`) |
259
+ | `p style="page-break-before: always"` / `break-before: page` / `p data-page-break` | Paragraphe avec `w:pageBreakBefore` (titres compris) |
260
+ | `span data-aster-variable`, `{{nom}}` | Variable : texte, valeur ou champ `MERGEFIELD` selon `options.variables.mode`. Voir [Variables à l'export](#variables-à-lexport) |
261
+ | `sup data-footnote-ref="n"`, `sup>[n]` + `ol data-footnotes > li data-footnote` | Vraie note de bas de page Word (`footnotes.xml`, style `Footnote Text`), appel `w:footnoteReference`, liste retirée du corps |
262
+
263
+ Les styles `Quote`, `HTML Preformatted` et `Code` portent les noms que la style map d'`importDocx` reconnaît : un aller-retour `exportDocx` → `importDocx` restitue titres, marques, lien, listes imbriquées, citation, bloc de code, image et table. Les alignements, couleurs et polices ne survivent pas à l'import (mammoth les jette volontairement).
264
+
265
+ Le texte est assaini avant d'entrer dans le XML : les caractères de contrôle que XML 1.0 interdit (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F) et les non-caractères U+FFFE/U+FFFF sont retirés des runs, des `alt`/`title` d'image, des `href` et des propriétés `title`/`creator`. Sans ça, Word refuse d'ouvrir le fichier. Tabulations, sauts de ligne, espaces insécables et Unicode étendu sont conservés tels quels ; seuls les blancs HTML (espace, tabulation, LF, CR, FF) sont réduits selon les règles du HTML.
266
+
267
+ ### Variables à l'export
268
+
269
+ Convention du core : `<span data-aster-variable="nom" data-label="Nom">{{nom}}</span>`. Un `{{nom}}` en texte brut (sans span) est aussi reconnu en modes `fields` et `resolve`, via `variables.pattern` (groupe 1 = nom ; par défaut `{{nom}}`, nom en lettres, chiffres, `_`, `.`, `-`, blancs tolérés). Les blocs `<pre>` et marques `<code>` ne sont jamais analysés. Un motif personnalisé est recompilé avec `g` (et sans `y`) puis appliqué tel quel à tout le texte du document : sa complexité n'est pas bornée, un motif pathologique (ReDoS) relève de l'intégrateur.
270
+
271
+ Une valeur est de la **donnée**, pas du HTML : ses blancs sont écrits tels quels et ses sauts de ligne (`\n`, `\r\n`) deviennent des `<w:br/>` en `resolve`, des espaces dans le résultat d'un champ en `fields`. Une valeur non chaîne (`42`, `true`) est convertie par `String()` ; `null`/`undefined` valent « sans valeur ».
272
+
273
+ | `variables.mode` | Rendu dans Word |
274
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
275
+ | `placeholders` (défaut) | Texte `{{nom}}` tel quel, marques conservées. |
276
+ | `resolve` | Valeur de `values[nom]`, marques du span et de son contexte conservées. Sans valeur : `{{nom}}` + avertissement `variable-unresolved`. |
277
+ | `fields` | Champ de fusion `w:fldSimple w:instr=" MERGEFIELD nom "`. Résultat du champ : la valeur connue, sinon `«nom»`. Un nom avec blanc, guillemet ou backslash est cité (`MERGEFIELD "Nom du client"`, `\` et `"` échappés). `importDocx` relit ces champs en variables. `fldSimple` ne porte pas de propriétés de run : les marques du span ne survivent pas en ce mode. |
278
+
279
+ ```ts
280
+ await exportDocx(editor.getHTML(), {
281
+ variables: { mode: 'fields', values: { nom: 'ACME' } },
282
+ });
283
+ ```
284
+
285
+ ### En-têtes, pieds de page, table des matières, notes
286
+
287
+ - **En-tête / pied** : `header: 'Confidentiel'` ou `{ html: '<strong>Nebula</strong> {{client}}', align: 'right' }`. Le HTML passe par le convertisseur du corps (marques, liens, variables dans les trois modes, plusieurs paragraphes) ; `align` s'applique aux paragraphes sans `text-align` propre. `footer.pageNumbers: 'x-of-y'` ajoute un dernier paragraphe `PAGE / NUMPAGES`, centré sans `align`, sinon aligné comme le reste (`'left'` compris). Une option vide n'émet rien. Word refuse les appels de note hors du corps : un `<sup data-footnote-ref>` ou `<sup>[n]</sup>` dans `header.html`/`footer.html` reste un exposant `[n]` (avertissement `footnote-not-allowed`), et une liste `<ol data-footnotes>` placée dans `header.html`/`footer.html` n'est pas collectée comme notes — elle est rendue comme une liste ordinaire de l'en-tête ou du pied. Seule la liste du corps alimente `footnotes.xml`.
288
+ - **Table des matières** : `toc: { title: 'Sommaire', levels: 2 }` insère le titre (style `TOC Heading`, hors des niveaux de titre) et le champ `TOC \h \o "1-2"`, **prérempli** avec les titres du document (entrées cliquables). Les numéros de page sont calculés par Word : le fichier porte `w:updateFields`, Word demande à l'ouverture « Ce document contient des champs… mettre à jour ? » — répondre Oui. LibreOffice met le champ à jour sans demander.
289
+ - **Notes de bas de page** : le format produit par `importDocx` (`<sup>[1]</sup>` + `<ol data-footnotes><li data-footnote="footnote-1">`) redevient de vraies notes Word ; `<sup data-footnote-ref="1">` est aussi accepté, et un `<li>` sans `data-footnote` est désigné par sa position (ou la première clé libre si cette position est déjà prise par un `data-footnote` explicite). Word n'accepte qu'un appel par note et aucun appel dans une note : un second `<sup>` vers la même note, ou un appel depuis le texte d'une note, reste un exposant `[n]` avec avertissement `footnote-duplicate` / `footnote-not-allowed` ; une note déclarée deux fois sous la même clé garde la première. L'aller-retour `exportDocx` → `importDocx` restitue le même HTML.
290
+ - **Numérotation des titres** : `numberedHeadings: true` lie une numérotation à `Heading 1..3` (`w:pStyle` dans `numbering.xml`, `w:numPr` dans le style) : Word numérote les titres existants et ceux ajoutés ensuite, la table des matières reprend les numéros. `format: 'legal'` supprime le point final (`1`, `1.1`).
291
+ - **Mise en page** : `page: { orientation: 'landscape', margins: { top: 20, bottom: 20, left: 15, right: 15 } }`. Les images et les tables sont plafonnées à la largeur utile résultante. Une marge négative reprend le défaut, et deux marges opposées dont la somme ne laisse pas 1 cm de zone utile sont réduites à proportion ; les deux cas remontent `page-margins-clamped`.
292
+
293
+ #### Limites connues de l'export
294
+
295
+ - **Liste dans une citation** : exportée fidèlement (paragraphes de style `Quote` numérotés, lisibles dans Word), mais **dégradée à la réimportation** par mammoth, qui suit le style et jette la numérotation : `<blockquote><ul><li>a</li></ul></blockquote>` revient en `<blockquote><p>a</p></blockquote>`.
296
+ - **`li` multi-paragraphes avec sous-liste** : export Word standard (seul le premier paragraphe porte la puce, les suivants sont indentés). À la **réimportation**, mammoth sort les paragraphes non numérotés de la liste et la coupe en listes sœurs : `<ul><li><p>a</p><p>b</p><ul><li><p>c</p></li></ul></li></ul>` revient en `<ul><li><p>a</p></li></ul><p>b</p><ul><li><p></p><ul><li><p>c</p></li></ul></li></ul>`.
297
+ - **`<ol start>`** : écrit dans `numbering.xml` (`w:start` et `startOverride`), honoré par Word, mais **non relu par mammoth** : l'aller-retour rend un `<ol>` sans `start`.
298
+ - **Bloc dans un inline** (`<span><div>…</div></span>`) : le bloc ne peut plus devenir un paragraphe, son texte est isolé par des sauts de ligne dans le paragraphe courant.
299
+ - **Champs de fusion sans marques** : en mode `fields`, `w:fldSimple` n'accepte pas de propriétés de run, un `<strong><span data-aster-variable>` perd son gras. Utilise `resolve` quand la mise en forme compte plus que le champ.
300
+ - **Table des matières à mettre à jour** : les numéros de page ne sont jamais écrits par l'export, Word les calcule à l'ouverture après confirmation.
301
+ - **Numérotation des titres non relue** : `importDocx` (mammoth) jette les numéros automatiques, un aller-retour rend des titres sans `1.`.
302
+ - **En-têtes, pieds et notes à la réimportation** : mammoth ignore les en-têtes et pieds de page ; les notes reviennent via `data-footnotes` comme pour tout .docx.
303
+ - **Largeurs de colonnes** : le `colwidth` Tiptap est en pixels d'écran (1 px = 15 twips) ; une table plus large que la zone de texte est réduite proportionnellement. Les largeurs en `%` sont ignorées.
304
+ - **Tables de plus de 63 colonnes** : les fusions sont bornées à 63, mais une table qui compte réellement plus de 63 colonnes est exportée telle quelle (aucune colonne n'est jetée) ; Word, limité à 63, peut la réparer à l'ouverture.
305
+
306
+ Les trois premières dégradations sont figées par des tests d'aller-retour (`export/lists.spec.ts`, cas X-3, M-1 et M-2 des revues) et la quatrième par `export/html-to-docx.spec.ts` (M-5), pour qu'une régression soit visible.
307
+
308
+ ### Exemple avec un bouton
309
+
310
+ ```ts
311
+ import { createAster } from '@aster-editor/ui';
312
+ import { downloadDocx, exportDocx } from '@aster-editor/word';
313
+
314
+ const { editor } = createAster(document.querySelector<HTMLElement>('#editor')!);
315
+
316
+ document.querySelector('#export')?.addEventListener('click', async () => {
317
+ const blob = await exportDocx(editor.getHTML(), {
318
+ title: 'Compte rendu',
319
+ creator: 'Aster',
320
+ fetchImage: async (src) => {
321
+ const response = await fetch(src);
322
+ return response.ok ? response.arrayBuffer() : null;
323
+ },
324
+ onWarning: (warning) => console.warn(`[${warning.code}] ${warning.message}`),
325
+ });
326
+ downloadDocx(blob, 'compte-rendu');
327
+ });
328
+ ```
329
+
330
+ `downloadDocx` crée un `<a download rel="noopener">` éphémère et révoque l'URL objet juste après le clic ; le nom reçoit `.docx` s'il ne l'a pas et ses caractères interdits (`\ / : * ? " < > |`) sont remplacés par `-`, `document.docx` par défaut. À appeler depuis un gestionnaire d'événement utilisateur. Hors navigateur (SSR, Node), la fonction lève : envoie plutôt les octets, `Buffer.from(await blob.arrayBuffer())`.
331
+
332
+ Un `exportDocx` sans `DOMParser` global ni `options.parse` lève aussi une erreur explicite, et une entrée qui n'est ni chaîne ni objet est refusée avant tout chargement de `docx`.
333
+
334
+ ## ⚠️ Limites connues
335
+
336
+ - **Pas de fidélité de mise en page à l'import** : colonnes, en-têtes et pieds de page, sauts de page, polices exactes, tailles et interlignes sont volontairement jetés par mammoth. L'objectif est de préserver l'intention (structure, emphase), pas le rendu au pixel. Les en-têtes et pieds ne sont lus que pour y collecter des variables (`result.variables`). À l'export, en revanche, en-têtes, pieds, numéros de page, sauts de page, table des matières, mise en page et police par défaut sont produits (voir « En-têtes, pieds de page, table des matières, notes »).
337
+ - **Images collées depuis Word sous Windows** : le presse-papier référence des fichiers locaux (`file:///C:/Users/…/clip_image001.png`). Le navigateur ne peut pas les lire, la politique de liens du core rejette `file:` et l'image est perdue. Passe par l'import `.docx` ou par un glisser-déposer du fichier image.
338
+ - **Formes, zones de texte, SmartArt, équations** : contenu VML/DrawingML non converti. Le texte des zones de texte peut survivre, pas la mise en page.
339
+ - **Numérotation reprise** : une liste Word qui « continue » une liste précédente arrive avec `start`, mais la reprise exacte dépend de ce que Word a sérialisé.
340
+ - **Notes de bas de page** : conservées dans `html` via `data-footnotes` / `data-footnote`, mais le schéma Tiptap ne garde pas ces attributs après `setHTML` ; seule la liste finale et les appels `[1]` restent.
341
+ - **Styles custom** : un style de paragraphe inconnu remonte en avertissement `mammoth` et devient un `<p>`. Ajoute une règle dans `styleMap` pour le mapper.
342
+ - **Google Docs** : les `li` gardent leur `<p>` ; les couleurs et tailles posées par Google sont jetées comme pour Word.
343
+ - **Aller-retour export → import** : une liste dans une citation, un `li` multi-paragraphes avec sous-liste et `<ol start>` sont exportés fidèlement pour Word mais dégradés par mammoth à la réimportation. Détail et HTML obtenu dans [Limites connues de l'export](#limites-connues-de-lexport).
344
+
345
+ ## ⚖️ Poids de mammoth et de docx, import dynamique
346
+
347
+ mammoth pèse environ **660 kB non minifié**, la lib `docx` environ **1,3 MB**. `importDocx` charge mammoth via `await import('mammoth')` au premier appel et `exportDocx` fait de même pour `docx` : ni l'un ni l'autre n'entre dans le bundle initial si ton bundler suit le code-splitting (esbuild, Vite, Angular CLI le font). Pour aller plus loin, importe aussi le paquet à la demande :
348
+
349
+ ```ts
350
+ async function loadDocx(file: File): Promise<string> {
351
+ const { importDocx } = await import('@aster-editor/word');
352
+ return (await importDocx(file)).html;
353
+ }
354
+ ```
355
+
356
+ `wordPasteTransformer`, `normalizeWordHtml`, `normalizeGoogleDocsHtml`, `jsonToHtml` et `downloadDocx` n'ont **aucune dépendance** à mammoth ni à `docx` : les importer statiquement ne coûte que leur propre code. Sur `ng serve`, le tout premier `importDocx` ou `exportDocx` d'une session peut recharger la page : l'optimiseur de dépendances de Vite découvre mammoth ou `docx` à ce moment-là (voir le [guide d'intégration](../../docs/GUIDE_INTEGRATION.md)).
357
+
358
+ ## 🧾 Exports
359
+
360
+ `importDocx`, `exportDocx`, `downloadDocx`, `jsonToHtml`, `DOCX_MIME_TYPE`, `normalizeWordHtml`, `normalizeGoogleDocsHtml`, `isWordHtml`, `isGoogleDocsHtml`, `wordPasteTransformer`, et les types `DocxInput`, `DocxImportOptions`, `DocxImportResult`, `DocxImportWarning`, `DocxImportWarningCode`, `DocxVariableOptions`, `VariableDetection`, `DocxExportInput`, `DocxExportOptions`, `DocxExportWarning`, `DocxExportWarningCode`, `DocxImageFetcher`, `DocxWarning`, `DocxPageSize`, `DocxPageOrientation`, `DocxPageMargins`, `DocxPageOptions`, `DocxDefaultFont`, `DocxExportVariableOptions`, `DocxVariableMode`, `DocxHeaderAlign`, `DocxHeaderOptions`, `DocxFooterOptions`, `DocxTocOptions`, `DocxNumberedHeadingsOptions`, `DocxHeadingStyle`, `DocxHeadingStyles`, `HtmlParser`, `WordPasteOptions`, `PasteVariableOptions`, `ListLevelFormat`.
@@ -0,0 +1,11 @@
1
+ import type { DocxImportOptions, DocxImportResult, DocxInput } from './types';
2
+ /**
3
+ * Convertit un document Word (.docx) en HTML sémantique prêt pour `AsterEditor.setHTML`
4
+ * ou `insertHTML`. Accepte un `File`, un `ArrayBuffer` ou une vue typée (`Uint8Array`,
5
+ * `Buffer` Node). Ne lève jamais pour un document Word valide : les éléments non
6
+ * convertis fidèlement sont signalés dans `warnings`. Rejette avec une erreur
7
+ * `DocxImportError` (message `invalid-docx: …`) si l'entrée n'est ni l'un de ces types,
8
+ * ni une archive .docx lisible.
9
+ */
10
+ export declare function importDocx(input: DocxInput, options?: DocxImportOptions): Promise<DocxImportResult>;
11
+ //# sourceMappingURL=import-docx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-docx.d.ts","sourceRoot":"","sources":["../../src/docx/import-docx.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAEhB,SAAS,EAEV,MAAM,SAAS,CAAC;AAqKjB;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CA+C3B"}