react-bootstrap-front 3.10.2 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG +9 -0
  2. package/dist/react-bootstrap-front.js +14 -14
  3. package/doc/editor/feuille-de-route.md +36 -0
  4. package/package.json +42 -8
  5. package/src/features/advanced/DropdownWrapper.js +12 -2
  6. package/src/features/basic/Dropdown.js +112 -31
  7. package/src/features/basic/computePlacement.js +66 -0
  8. package/src/features/editor/HtmlEditor.js +681 -0
  9. package/src/features/editor/HtmlEditor.scss +189 -0
  10. package/src/features/editor/ImageDialog.js +195 -0
  11. package/src/features/editor/LinkDialog.js +148 -0
  12. package/src/features/editor/MergeMenu.js +120 -0
  13. package/src/features/editor/SourceView.js +61 -0
  14. package/src/features/editor/Toolbar.js +355 -0
  15. package/src/features/editor/Toolbar.scss +130 -0
  16. package/src/features/editor/ToolbarButton.js +59 -0
  17. package/src/features/editor/ToolbarDropdown.js +46 -0
  18. package/src/features/editor/ToolbarGroup.js +27 -0
  19. package/src/features/editor/buildExtensions.js +77 -0
  20. package/src/features/editor/defaultIcons.js +45 -0
  21. package/src/features/editor/extensions/CleanPaste.js +44 -0
  22. package/src/features/editor/extensions/FontSize.js +60 -0
  23. package/src/features/editor/extensions/LegacyImageAttrs.js +43 -0
  24. package/src/features/editor/extensions/MergeVariable.js +104 -0
  25. package/src/features/editor/extensions/index.js +4 -0
  26. package/src/features/editor/html/cleanHtml.constants.js +279 -0
  27. package/src/features/editor/html/cleanHtml.js +691 -0
  28. package/src/features/editor/html/index.js +4 -0
  29. package/src/features/editor/html/normalizeLegacyHtml.js +196 -0
  30. package/src/features/editor/html/serialize.js +130 -0
  31. package/src/features/editor/html/sourceHelpers.js +86 -0
  32. package/src/features/editor/index.js +20 -0
  33. package/src/features/editor/style.scss +2 -0
  34. package/src/features/editor/toolbarPresets.js +74 -0
  35. package/src/features/page/DefaultHeader.jsx +1 -1
  36. package/src/index.js +8 -0
  37. package/src/styles/index.scss +1 -0
@@ -0,0 +1,691 @@
1
+ import { CLEAN_PASTE, CLEAN_PROFILES } from './cleanHtml.constants';
2
+
3
+ /**
4
+ *******************************************************************************
5
+ * Pré-nettoyage textuel
6
+ *******************************************************************************
7
+ */
8
+
9
+ const PRE_CLEAN_PATTERNS = [
10
+ /* Commentaires conditionnels Word : <!--[if gte mso 9]> ... <![endif]--> */
11
+ [/<!--\[if[\s\S]*?<!\[endif\]-->/gi, ''],
12
+ /* Commentaires (StartFragment, EndFragment, tout le reste) */
13
+ [/<!--[\s\S]*?-->/g, ''],
14
+ /* Déclarations */
15
+ [/<!DOCTYPE[^>]*>/gi, ''],
16
+ /* Blocs entiers à jeter avec leur contenu : Word injecte des kilo-octets de <style> */
17
+ [/<(script|style|xml|head|title|noscript)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, ''],
18
+ /* Les mêmes, non refermés */
19
+ [/<(script|style|xml|head|title|noscript)\b[^>]*>/gi, ''],
20
+ /* Balises à namespace Office : <o:p>, <v:shape>, <w:sdt> */
21
+ [/<\/?[a-z][a-z0-9]*:[^>]*>/gi, ''],
22
+ /* Marques de position invisibles */
23
+ [/[\u200b\u200c\u200d\ufeff]/g, ''],
24
+ ];
25
+
26
+ function preClean(html) {
27
+ let out = String(html);
28
+ for (let i = 0; i < PRE_CLEAN_PATTERNS.length; i++) {
29
+ out = out.replace(PRE_CLEAN_PATTERNS[i][0], PRE_CLEAN_PATTERNS[i][1]);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ /**
35
+ *******************************************************************************
36
+ * Styles : analyse et sérialisation
37
+ *******************************************************************************
38
+ */
39
+
40
+ /**
41
+ * Découpe un attribut style en couples [propriété, valeur].
42
+ * Le découpage respecte les parenthèses et les guillemets, sans quoi un
43
+ * `url(data:image/png;base64,...)` ou un `font-family: "a;b"` casserait tout.
44
+ */
45
+ export function parseStyle(value) {
46
+ const pairs = [];
47
+ const str = String(value || '');
48
+ let depth = 0;
49
+ let quote = null;
50
+ let current = '';
51
+ for (let i = 0; i < str.length; i++) {
52
+ const c = str.charAt(i);
53
+ if (quote) {
54
+ current += c;
55
+ if (c === quote && str.charAt(i - 1) !== '\\') {
56
+ quote = null;
57
+ }
58
+ continue;
59
+ }
60
+ if (c === '"' || c === "'") {
61
+ quote = c;
62
+ current += c;
63
+ continue;
64
+ }
65
+ if (c === '(') {
66
+ depth++;
67
+ } else if (c === ')') {
68
+ depth = Math.max(0, depth - 1);
69
+ }
70
+ if (c === ';' && depth === 0) {
71
+ pushPair(pairs, current);
72
+ current = '';
73
+ continue;
74
+ }
75
+ current += c;
76
+ }
77
+ pushPair(pairs, current);
78
+ return pairs;
79
+ }
80
+
81
+ function pushPair(pairs, chunk) {
82
+ const trimmed = chunk.trim();
83
+ if (trimmed === '') {
84
+ return;
85
+ }
86
+ const idx = trimmed.indexOf(':');
87
+ if (idx <= 0) {
88
+ return;
89
+ }
90
+ const prop = trimmed
91
+ .slice(0, idx)
92
+ .trim()
93
+ .toLowerCase();
94
+ const val = trimmed.slice(idx + 1).trim();
95
+ if (prop === '' || val === '') {
96
+ return;
97
+ }
98
+ pairs.push([prop, val]);
99
+ }
100
+
101
+ export function serializeStyle(pairs) {
102
+ const parts = [];
103
+ for (let i = 0; i < pairs.length; i++) {
104
+ parts.push(pairs[i][0] + ': ' + pairs[i][1]);
105
+ }
106
+ return parts.join('; ');
107
+ }
108
+
109
+ /**
110
+ * Normalise une valeur CSS pour la comparer : minuscules, guillemets retirés,
111
+ * espaces compactés puis supprimés autour des virgules.
112
+ * `"Times New Roman", serif` et `'times new roman',serif` donnent le même résultat.
113
+ */
114
+ function normalizeCssValue(value) {
115
+ return String(value)
116
+ .toLowerCase()
117
+ .replace(/["']/g, '')
118
+ .replace(/\s+/g, ' ')
119
+ .replace(/\s*,\s*/g, ',')
120
+ .replace(/!\s*important$/, '')
121
+ .trim();
122
+ }
123
+
124
+ /**
125
+ * Normalise une longueur CSS pour la comparer : `11.0pt` et `11pt` désignent la
126
+ * même chose, et Word écrit systématiquement la première forme.
127
+ */
128
+ function normalizeCssLength(value) {
129
+ const match = /^([0-9]*\.?[0-9]+)\s*([a-z%]*)$/.exec(value);
130
+ if (!match) {
131
+ return value;
132
+ }
133
+ return parseFloat(match[1]) + match[2];
134
+ }
135
+
136
+ /**
137
+ *******************************************************************************
138
+ * Aides DOM
139
+ *******************************************************************************
140
+ */
141
+
142
+ const BLOCK_TAGS = [
143
+ 'p',
144
+ 'div',
145
+ 'h1',
146
+ 'h2',
147
+ 'h3',
148
+ 'h4',
149
+ 'h5',
150
+ 'h6',
151
+ 'ul',
152
+ 'ol',
153
+ 'li',
154
+ 'blockquote',
155
+ 'hr',
156
+ 'table',
157
+ 'thead',
158
+ 'tbody',
159
+ 'tfoot',
160
+ 'tr',
161
+ 'td',
162
+ 'th',
163
+ 'pre',
164
+ ];
165
+
166
+ function isBlockTag(tag) {
167
+ return BLOCK_TAGS.indexOf(tag) !== -1;
168
+ }
169
+
170
+ function remove(node) {
171
+ if (node && node.parentNode) {
172
+ node.parentNode.removeChild(node);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Remplace un élément par ses enfants. Le texte n'est jamais perdu.
178
+ */
179
+ function unwrap(el) {
180
+ const parent = el.parentNode;
181
+ if (!parent) {
182
+ return;
183
+ }
184
+ while (el.firstChild) {
185
+ parent.insertBefore(el.firstChild, el);
186
+ }
187
+ parent.removeChild(el);
188
+ }
189
+
190
+ /**
191
+ * Renomme un élément en conservant attributs et enfants.
192
+ * Retourne le nouvel élément.
193
+ */
194
+ function rename(el, tagName) {
195
+ const doc = el.ownerDocument;
196
+ const next = doc.createElement(tagName);
197
+ const attrs = el.attributes;
198
+ for (let i = 0; i < attrs.length; i++) {
199
+ try {
200
+ next.setAttribute(attrs[i].name, attrs[i].value);
201
+ } catch (e) {
202
+ /* nom d'attribut invalide : on le laisse tomber */
203
+ }
204
+ }
205
+ while (el.firstChild) {
206
+ next.appendChild(el.firstChild);
207
+ }
208
+ if (el.parentNode) {
209
+ el.parentNode.replaceChild(next, el);
210
+ }
211
+ return next;
212
+ }
213
+
214
+ function getScheme(url) {
215
+ const match = /^\s*([a-z][a-z0-9+.-]*:)/i.exec(String(url || ''));
216
+ return match ? match[1].toLowerCase() : null;
217
+ }
218
+
219
+ function isDataImage(url) {
220
+ return /^\s*data:image\//i.test(String(url || ''));
221
+ }
222
+
223
+ function hasContent(el) {
224
+ /* Le nbsp compte comme du contenu : c'est une ligne blanche voulue. */
225
+ return el.textContent.replace(/[^\S\u00a0]+/g, '') !== '';
226
+ }
227
+
228
+ /**
229
+ *******************************************************************************
230
+ * Traitement d'un élément
231
+ *******************************************************************************
232
+ */
233
+
234
+ function cleanAttributes(el, tag, opts) {
235
+ const attrs = Array.prototype.slice.call(el.attributes);
236
+ for (let i = 0; i < attrs.length; i++) {
237
+ const name = attrs[i].name.toLowerCase();
238
+ if (name === 'style' || name === 'class') {
239
+ continue;
240
+ }
241
+ /* Gestionnaires d'événements et bruit Office */
242
+ if (name.indexOf('on') === 0 || name.indexOf('mso-') === 0 || name.indexOf(':') !== -1) {
243
+ el.removeAttribute(attrs[i].name);
244
+ continue;
245
+ }
246
+ if (opts.allowedAttributes) {
247
+ const allowed = (opts.allowedAttributes['*'] || []).concat(opts.allowedAttributes[tag] || []);
248
+ if (allowed.indexOf(name) === -1) {
249
+ el.removeAttribute(attrs[i].name);
250
+ }
251
+ }
252
+ }
253
+ /* Liens : un schéma exotique perd son href, l'élément reste (le texte survit) */
254
+ if (el.hasAttribute('href')) {
255
+ const scheme = getScheme(el.getAttribute('href'));
256
+ if (scheme !== null && opts.allowedHrefSchemes.indexOf(scheme) === -1) {
257
+ el.removeAttribute('href');
258
+ }
259
+ }
260
+ /* Images : un src inutilisable fait disparaître l'élément, une image inerte n'a pas de sens */
261
+ if (el.hasAttribute('src')) {
262
+ const src = el.getAttribute('src');
263
+ const scheme = getScheme(src);
264
+ const ok =
265
+ scheme === null ||
266
+ opts.allowedSrcSchemes.indexOf(scheme) !== -1 ||
267
+ (opts.allowDataImages && isDataImage(src));
268
+ if (!ok) {
269
+ return false;
270
+ }
271
+ }
272
+ return true;
273
+ }
274
+
275
+ function cleanClasses(el, opts) {
276
+ if (!el.hasAttribute('class')) {
277
+ return;
278
+ }
279
+ if (opts.keepClasses === null) {
280
+ return;
281
+ }
282
+ const keep = [];
283
+ const classes = el.getAttribute('class').split(/\s+/);
284
+ for (let i = 0; i < classes.length; i++) {
285
+ if (classes[i] !== '' && opts.keepClasses.indexOf(classes[i]) !== -1) {
286
+ keep.push(classes[i]);
287
+ }
288
+ }
289
+ if (keep.length > 0) {
290
+ el.setAttribute('class', keep.join(' '));
291
+ } else {
292
+ el.removeAttribute('class');
293
+ }
294
+ }
295
+
296
+ function cleanStyle(el, opts) {
297
+ if (!el.hasAttribute('style')) {
298
+ return;
299
+ }
300
+ const pairs = parseStyle(el.getAttribute('style'));
301
+ const kept = [];
302
+ for (let i = 0; i < pairs.length; i++) {
303
+ const prop = pairs[i][0];
304
+ const raw = pairs[i][1];
305
+ const value = normalizeCssValue(raw);
306
+
307
+ if (prop.indexOf('mso-') === 0 || prop.indexOf('-webkit-') === 0 || prop.indexOf('-moz-') === 0) {
308
+ continue;
309
+ }
310
+ if (opts.allowedStyles && opts.allowedStyles.indexOf(prop) === -1) {
311
+ continue;
312
+ }
313
+ if (opts.dropStyles && opts.dropStyles.indexOf(prop) !== -1) {
314
+ continue;
315
+ }
316
+ if (opts.neutralStyleValues) {
317
+ const neutral = opts.neutralStyleValues[prop];
318
+ if (neutral && neutral.indexOf(value) !== -1) {
319
+ continue;
320
+ }
321
+ }
322
+ if (prop === 'font-size' && opts.defaultFontSizes.indexOf(normalizeCssLength(value)) !== -1) {
323
+ continue;
324
+ }
325
+ if (prop === 'font-family' && opts.defaultFontFamilies.indexOf(value) !== -1) {
326
+ continue;
327
+ }
328
+ kept.push([prop, raw]);
329
+ }
330
+ if (kept.length > 0) {
331
+ el.setAttribute('style', serializeStyle(kept));
332
+ } else {
333
+ el.removeAttribute('style');
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Traduit les attributs de présentation de <font> en style.
339
+ */
340
+ const FONT_SIZE_MAP = { 1: '8pt', 2: '10pt', 3: '12pt', 4: '14pt', 5: '18pt', 6: '24pt', 7: '36pt' };
341
+
342
+ function fontToStyle(el) {
343
+ const pairs = parseStyle(el.getAttribute('style') || '');
344
+ const color = el.getAttribute('color');
345
+ const face = el.getAttribute('face');
346
+ const size = el.getAttribute('size');
347
+ if (color) {
348
+ pairs.push(['color', color]);
349
+ }
350
+ if (face) {
351
+ pairs.push(['font-family', face]);
352
+ }
353
+ if (size && FONT_SIZE_MAP[size]) {
354
+ pairs.push(['font-size', FONT_SIZE_MAP[size]]);
355
+ }
356
+ el.removeAttribute('color');
357
+ el.removeAttribute('face');
358
+ el.removeAttribute('size');
359
+ if (pairs.length > 0) {
360
+ el.setAttribute('style', serializeStyle(pairs));
361
+ }
362
+ }
363
+
364
+ function processElement(el, opts) {
365
+ let tag = el.tagName.toLowerCase();
366
+
367
+ /* 1. Balises jetées avec leur contenu */
368
+ if (opts.dropTags.indexOf(tag) !== -1) {
369
+ remove(el);
370
+ return;
371
+ }
372
+
373
+ /* 2. Signature Google Docs : <b style="font-weight:normal"> enveloppe tout le collage */
374
+ if (tag === 'b' || tag === 'strong') {
375
+ const style = normalizeCssValue(el.getAttribute('style') || '');
376
+ if (/font-weight:\s*(normal|400)/.test(style)) {
377
+ unwrap(el);
378
+ return;
379
+ }
380
+ }
381
+
382
+ /* 3. <font> : les attributs de présentation deviennent du style */
383
+ if (tag === 'font') {
384
+ fontToStyle(el);
385
+ el = rename(el, 'span');
386
+ tag = 'span';
387
+ }
388
+
389
+ /* 4. Balises historiques */
390
+ if (opts.tagAliases && opts.tagAliases[tag]) {
391
+ el = rename(el, opts.tagAliases[tag]);
392
+ tag = el.tagName.toLowerCase();
393
+ }
394
+
395
+ /* 5. Attributs, classes, styles */
396
+ if (!cleanAttributes(el, tag, opts)) {
397
+ remove(el);
398
+ return;
399
+ }
400
+ cleanClasses(el, opts);
401
+ cleanStyle(el, opts);
402
+
403
+ /* 6. <div> : bloc s'il contient des blocs, sinon paragraphe */
404
+ if (tag === 'div' && opts.convertDivToP) {
405
+ if (containsBlock(el)) {
406
+ unwrap(el);
407
+ } else {
408
+ el = rename(el, 'p');
409
+ tag = 'p';
410
+ }
411
+ return;
412
+ }
413
+
414
+ /* 7. Balises inconnues : déballées, jamais supprimées */
415
+ if (opts.allowedTags && opts.allowedTags.indexOf(tag) === -1) {
416
+ unwrap(el);
417
+ return;
418
+ }
419
+
420
+ /* 8. <span> devenu inutile : c'est le cas le plus fréquent d'un collage Word */
421
+ if (tag === 'span' && opts.unwrapEmptySpans && el.attributes.length === 0) {
422
+ unwrap(el);
423
+ return;
424
+ }
425
+
426
+ /* 9. Marques identiques imbriquées */
427
+ if (opts.mergeNestedMarks && el.parentNode && el.parentNode.nodeType === 1) {
428
+ const parent = el.parentNode;
429
+ if (
430
+ parent.tagName.toLowerCase() === tag &&
431
+ parent.childNodes.length === 1 &&
432
+ (el.getAttribute('style') || '') === (parent.getAttribute('style') || '')
433
+ ) {
434
+ unwrap(el);
435
+ }
436
+ }
437
+ }
438
+
439
+ function containsBlock(el) {
440
+ const children = el.children || [];
441
+ for (let i = 0; i < children.length; i++) {
442
+ if (isBlockTag(children[i].tagName.toLowerCase())) {
443
+ return true;
444
+ }
445
+ }
446
+ return false;
447
+ }
448
+
449
+ /**
450
+ * Parcours profondeur d'abord, post-ordre : les enfants sont traités avant leur
451
+ * parent, ce qui permet au parent de constater qu'il est devenu inutile.
452
+ */
453
+ function walk(node, opts) {
454
+ const children = Array.prototype.slice.call(node.childNodes);
455
+ for (let i = 0; i < children.length; i++) {
456
+ const child = children[i];
457
+ if (child.nodeType === 1) {
458
+ walk(child, opts);
459
+ if (child.parentNode) {
460
+ processElement(child, opts);
461
+ }
462
+ } else if (child.nodeType === 8) {
463
+ remove(child);
464
+ }
465
+ }
466
+ }
467
+
468
+ /**
469
+ *******************************************************************************
470
+ * Passes de finition
471
+ *******************************************************************************
472
+ */
473
+
474
+ /**
475
+ * Supprime les éléments devenus vides, en itérant jusqu'au point fixe : sans
476
+ * cela une cascade `<p><span><span></span></span></p>` ne serait purgée que
477
+ * d'un niveau par passe.
478
+ */
479
+ function removeEmptyElements(root, opts) {
480
+ for (let pass = 0; pass < opts.maxPasses; pass++) {
481
+ const empties = [];
482
+ const all = root.getElementsByTagName('*');
483
+ for (let i = 0; i < all.length; i++) {
484
+ const el = all[i];
485
+ const tag = el.tagName.toLowerCase();
486
+ if (opts.keepEmptyTags.indexOf(tag) !== -1) {
487
+ continue;
488
+ }
489
+ if (el.getElementsByTagName('*').length === 0 && !hasContent(el)) {
490
+ empties.push(el);
491
+ }
492
+ }
493
+ if (empties.length === 0) {
494
+ return;
495
+ }
496
+ for (let i = 0; i < empties.length; i++) {
497
+ remove(empties[i]);
498
+ }
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Vrai si le texte ne contient que des blancs ordinaires. L'insécable n'en est
504
+ * pas un ici : c'est une espace ou une ligne voulue par le rédacteur.
505
+ * String.trim() ne convient pas, il retire aussi les insécables.
506
+ */
507
+ function isBlankText(value) {
508
+ return /^[^\S\u00a0]*$/.test(value);
509
+ }
510
+
511
+ /**
512
+ * Un nœud texte est « entre blocs » si ses deux voisins significatifs sont
513
+ * absents ou de niveau bloc. Le test préserve l'espace de `a <em>b</em>`.
514
+ */
515
+ function isBlockBoundary(node) {
516
+ if (!node) {
517
+ return true;
518
+ }
519
+ return node.nodeType === 1 && isBlockTag(node.tagName.toLowerCase());
520
+ }
521
+
522
+ function isBetweenBlocks(node) {
523
+ return isBlockBoundary(previousMeaningful(node)) && isBlockBoundary(nextMeaningful(node));
524
+ }
525
+
526
+ function normalizeWhitespace(root) {
527
+ /* Les déballages ont laissé des nœuds texte adjacents : les fusionner
528
+ d'abord, sans quoi un double blanc resterait invisible d'une passe sur
529
+ l'autre et le nettoyage ne serait pas idempotent. */
530
+ root.normalize();
531
+
532
+ const nodes = [];
533
+ collectTextNodes(root, nodes);
534
+ for (let i = 0; i < nodes.length; i++) {
535
+ const node = nodes[i];
536
+
537
+ /* Blanc d'indentation entre deux blocs : il n'apporte rien et son sort
538
+ varie d'une passe à l'autre. */
539
+ if (isBlankText(node.nodeValue) && isBetweenBlocks(node)) {
540
+ remove(node);
541
+ continue;
542
+ }
543
+
544
+ let text = node.nodeValue;
545
+ /* Une suite d'insécables sert d'indentation : un espace suffit */
546
+ text = text.replace(/\u00a0{2,}/g, ' ');
547
+ /* Blancs multiples : le HTML les rend de toute façon comme un seul */
548
+ text = text.replace(/[^\S\u00a0]{2,}/g, ' ');
549
+ if (text !== node.nodeValue) {
550
+ node.nodeValue = text;
551
+ }
552
+ }
553
+ }
554
+
555
+ function collectTextNodes(node, out) {
556
+ const children = node.childNodes;
557
+ for (let i = 0; i < children.length; i++) {
558
+ const child = children[i];
559
+ if (child.nodeType === 3) {
560
+ out.push(child);
561
+ } else if (child.nodeType === 1) {
562
+ collectTextNodes(child, out);
563
+ }
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Normalisation des blocs : paragraphes imbriqués remontés, <br> superflus.
569
+ */
570
+ function normalizeBlocks(root) {
571
+ /* <p> dans <p> : on remonte l'intérieur */
572
+ let nested = root.querySelectorAll('p p, li p > p');
573
+ for (let i = nested.length - 1; i >= 0; i--) {
574
+ unwrap(nested[i]);
575
+ }
576
+
577
+ /* <br> en fin de bloc : invisible, mais il ajoute une ligne à l'impression */
578
+ const blocks = root.querySelectorAll('p, li, h1, h2, h3, h4, h5, h6, blockquote');
579
+ for (let i = 0; i < blocks.length; i++) {
580
+ let last = blocks[i].lastChild;
581
+ /* isBlankText et non trim() : une insécable finale est une ligne blanche
582
+ voulue, la retirer viderait le paragraphe qui serait ensuite purgé. */
583
+ while (last && last.nodeType === 3 && isBlankText(last.nodeValue)) {
584
+ const previous = last.previousSibling;
585
+ remove(last);
586
+ last = previous;
587
+ }
588
+ if (last && last.nodeType === 1 && last.tagName.toLowerCase() === 'br') {
589
+ remove(last);
590
+ }
591
+ }
592
+
593
+ /* Trois <br> ou plus à la suite : deux suffisent */
594
+ const brs = Array.prototype.slice.call(root.getElementsByTagName('br'));
595
+ let run = 0;
596
+ for (let i = 0; i < brs.length; i++) {
597
+ const br = brs[i];
598
+ const previous = previousMeaningful(br);
599
+ if (previous && previous.nodeType === 1 && previous.tagName.toLowerCase() === 'br') {
600
+ run++;
601
+ if (run >= 2) {
602
+ remove(br);
603
+ }
604
+ } else {
605
+ run = 0;
606
+ }
607
+ }
608
+ }
609
+
610
+ function previousMeaningful(node) {
611
+ let previous = node.previousSibling;
612
+ while (previous && previous.nodeType === 3 && isBlankText(previous.nodeValue)) {
613
+ previous = previous.previousSibling;
614
+ }
615
+ return previous;
616
+ }
617
+
618
+ function nextMeaningful(node) {
619
+ let next = node.nextSibling;
620
+ while (next && next.nodeType === 3 && isBlankText(next.nodeValue)) {
621
+ next = next.nextSibling;
622
+ }
623
+ return next;
624
+ }
625
+
626
+ /**
627
+ *******************************************************************************
628
+ * Point d'entrée
629
+ *******************************************************************************
630
+ */
631
+
632
+ function resolveOptions(options) {
633
+ if (!options) {
634
+ return CLEAN_PASTE;
635
+ }
636
+ if (typeof options === 'string') {
637
+ return CLEAN_PROFILES[options] || CLEAN_PASTE;
638
+ }
639
+ if (options.profile) {
640
+ const base = CLEAN_PROFILES[options.profile] || CLEAN_PASTE;
641
+ return { ...base, ...options };
642
+ }
643
+ return { ...CLEAN_PASTE, ...options };
644
+ }
645
+
646
+ function createRoot(html) {
647
+ if (typeof DOMParser === 'undefined') {
648
+ return null;
649
+ }
650
+ /* DOMParser n'exécute aucun script et ne déclenche aucune requête réseau. */
651
+ const doc = new DOMParser().parseFromString('<div id="rbf-clean-root">' + html + '</div>', 'text/html');
652
+ return doc.getElementById('rbf-clean-root');
653
+ }
654
+
655
+ /**
656
+ * Nettoie un fragment de HTML.
657
+ *
658
+ * @param {string} html le fragment à nettoyer
659
+ * @param {string|object} options 'load' | 'paste' | 'strict', ou un objet
660
+ * d'options fusionné sur le profil PASTE
661
+ * @returns {string} le HTML nettoyé
662
+ */
663
+ export function cleanHtml(html, options) {
664
+ if (html === null || html === undefined || html === '') {
665
+ return '';
666
+ }
667
+ const opts = resolveOptions(options);
668
+ const pre = preClean(html);
669
+ const root = createRoot(pre);
670
+ if (!root) {
671
+ /* Pas de DOM disponible : on rend au moins le pré-nettoyage textuel. */
672
+ return pre;
673
+ }
674
+ walk(root, opts);
675
+ /* L'ordre compte : blancs puis blocs, la purge des éléments vides en dernier.
676
+ Les deux premières passes peuvent vider un élément, et c'est la purge qui
677
+ doit en décider — sinon un contenu propre changerait encore au nettoyage
678
+ suivant et la fonction ne serait pas idempotente. */
679
+ if (opts.normalizeWhitespace) {
680
+ normalizeWhitespace(root);
681
+ }
682
+ if (opts.normalizeBlocks) {
683
+ normalizeBlocks(root);
684
+ }
685
+ if (opts.removeEmptyElements) {
686
+ removeEmptyElements(root, opts);
687
+ }
688
+ return root.innerHTML;
689
+ }
690
+
691
+ export default cleanHtml;
@@ -0,0 +1,4 @@
1
+ export { cleanHtml, parseStyle, serializeStyle } from './cleanHtml';
2
+ export { normalizeLegacyHtml } from './normalizeLegacyHtml';
3
+ export { serialize, serializeHtml, isEmptyHtml } from './serialize';
4
+ export * from './cleanHtml.constants';