@lkaopremier/html-to-docx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@lkaopremier/html-to-docx",
3
+ "version": "0.0.1",
4
+ "main": "src/index.js",
5
+ "type": "module",
6
+ "keywords": [],
7
+ "author": "lkapremier",
8
+ "license": "ISC",
9
+ "description": "",
10
+ "dependencies": {
11
+ "docx": "^9.0.3",
12
+ "html-entities": "^2.5.2",
13
+ "html-minifier": "^4.0.0",
14
+ "lodash": "^4.17.21",
15
+ "node-html-parser": "^6.1.13",
16
+ "sharp": "^0.33.5"
17
+ },
18
+ "bin": {
19
+ "html-to-docx": "bin/cli.js"
20
+ },
21
+ "devDependencies": {},
22
+ "scripts": {
23
+ "test": "echo \"Error: no test specified\" && exit 1"
24
+ },
25
+ "types": "./src/index.d.ts"
26
+ }
package/src/helper.js ADDED
@@ -0,0 +1,544 @@
1
+ import { parse as htmlParser } from 'node-html-parser'
2
+ import { minify } from 'html-minifier'
3
+ import sharp from 'sharp'
4
+
5
+ export function normalizeHtml(content) {
6
+ const elm = htmlParser(content)
7
+
8
+ const head =
9
+ elm.querySelector('head')?.outerHTML ??
10
+ `<head><meta charset="UTF-8"><title>Document</title></head>`
11
+
12
+ const body = cleanHtmlContent(
13
+ elm.querySelector('body')?.outerHTML?.trim() ??
14
+ `<body>${cleanHtmlContent(elm.outerHTML).trim()}</body>`,
15
+ )
16
+
17
+ const doc = minify(`<!DOCTYPE html><html>${head}${body}</html>`, {
18
+ removeAttributeQuotes: true,
19
+ removeEmptyAttributes: true,
20
+ removeComments: true,
21
+ })
22
+
23
+ return cleanHtmlContent(doc)
24
+ }
25
+
26
+ export function cleanHtmlContent(content) {
27
+ return content.replace(/(\s{2,}|\n)/g, match => {
28
+ if (/<(pre|code)>/.test(match)) {
29
+ return match
30
+ }
31
+
32
+ return ' '
33
+ })
34
+ }
35
+
36
+ export function pageNodes(bodyElm) {
37
+ const pages = {}
38
+ let children = []
39
+ bodyElm.childNodes.forEach(child => {
40
+ if (child && child.classList?.contains('page-break')) {
41
+ pages[Object.keys(pages).length + 1] = arrayToNodeHTMLElement(children)
42
+ children = []
43
+ } else {
44
+ children.push(child)
45
+ }
46
+ })
47
+
48
+ if (children.length > 0) {
49
+ pages[Object.keys(pages).length + 1] = arrayToNodeHTMLElement(children)
50
+ }
51
+
52
+ return Object.values(pages)
53
+ }
54
+
55
+ export function arrayToNodeHTMLElement(array) {
56
+ const fragment = htmlParser('')
57
+ array.forEach(item => fragment.appendChild(item))
58
+ return fragment
59
+ }
60
+
61
+ export function trim(str, ch) {
62
+ var start = 0,
63
+ end = str.length
64
+
65
+ while (start < end && str[start] === ch) ++start
66
+
67
+ while (end > start && str[end - 1] === ch) --end
68
+
69
+ return start > 0 || end < str.length ? str.substring(start, end) : str
70
+ }
71
+
72
+ export function splitMeasure(value) {
73
+ const match = value?.toString().match(/^(\d+(\.\d+)?)(px|pt|cm|in|mm|pc|pi)$/)
74
+
75
+ if (!match) {
76
+ return []
77
+ }
78
+
79
+ return [parseFloat(match[1]), match[3]]
80
+ }
81
+
82
+ export function normalizeMeasure(value) {
83
+ const [numericValue, unit] = splitMeasure(value)
84
+
85
+ if (!numericValue || !unit) {
86
+ return ''
87
+ }
88
+
89
+ if (unit === 'px') {
90
+ return `${numericValue * 0.75}pt`
91
+ } else if (['pt', 'in', 'cm', 'mm', 'pc', 'pi'].includes(unit)) {
92
+ return `${numericValue}${unit}`
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Capitalizes first letters of words in string.
98
+ * @param {string} str String to be modified
99
+ * @param {boolean=false} lower Whether all other letters should be lowercased
100
+ * @return {string}
101
+ * @usage
102
+ * capitalize('fix this string'); // -> 'Fix This String'
103
+ * capitalize('javaSCrIPT'); // -> 'JavaSCrIPT'
104
+ * capitalize('javaSCrIPT', true); // -> 'Javascript'
105
+ */
106
+ export const capitalize = (str, lower = false) => {
107
+ return (lower ? str.toLowerCase() : str).replace(
108
+ /(?:^|\s|["'([{])+\S/g,
109
+ match => match.toUpperCase(),
110
+ )
111
+ }
112
+
113
+ export function colorToHex(color) {
114
+ const namedColors = {
115
+ red: '#FF0000',
116
+ blue: '#0000FF',
117
+ green: '#008000',
118
+ black: '#000000',
119
+ white: '#FFFFFF',
120
+ yellow: '#FFFF00',
121
+ orange: '#FFA500',
122
+ purple: '#800080',
123
+ pink: '#FFC0CB',
124
+ gray: '#808080',
125
+ silver: '#C0C0C0',
126
+ maroon: '#800000',
127
+ olive: '#808000',
128
+ lime: '#00FF00',
129
+ teal: '#008080',
130
+ navy: '#000080',
131
+ aqua: '#00FFFF',
132
+ fuchsia: '#FF00FF',
133
+ cyan: '#00FFFF',
134
+ brown: '#A52A2A',
135
+ gold: '#FFD700',
136
+ coral: '#FF7F50',
137
+ violet: '#EE82EE',
138
+ indigo: '#4B0082',
139
+ khaki: '#F0E68C',
140
+ salmon: '#FA8072',
141
+ chocolate: '#D2691E',
142
+ tan: '#D2B48C',
143
+ azure: '#F0FFFF',
144
+ beige: '#F5F5DC',
145
+ lavender: '#E6E6FA',
146
+ crimson: '#DC143C',
147
+ turquoise: '#40E0D0',
148
+ ivory: '#FFFFF0',
149
+ orchid: '#DA70D6',
150
+ plum: '#DDA0DD',
151
+ sienna: '#A0522D',
152
+ midnightblue: '#191970',
153
+ seashell: '#FFF5EE',
154
+ tomato: '#FF6347',
155
+ snow: '#FFFAFA',
156
+ mintcream: '#F5FFFA',
157
+ wheat: '#F5DEB3',
158
+ moccasin: '#FFE4B5',
159
+ hotpink: '#FF69B4',
160
+ skyblue: '#87CEEB',
161
+ slategray: '#708090',
162
+ darkblue: '#00008B',
163
+ darkgreen: '#006400',
164
+ darkred: '#8B0000',
165
+ lightblue: '#ADD8E6',
166
+ lightgreen: '#90EE90',
167
+ lightpink: '#FFB6C1',
168
+ lightgray: '#D3D3D3',
169
+ }
170
+
171
+ if (namedColors[color.toLowerCase()]) {
172
+ return namedColors[color.toLowerCase()]
173
+ }
174
+
175
+ const hexRegex = /^#([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$/i
176
+ if (hexRegex.test(color)) {
177
+ return color.toUpperCase()
178
+ }
179
+
180
+ const rgbRegex =
181
+ /^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d?\.?\d+))?\)$/i
182
+ const rgbMatch = color.match(rgbRegex)
183
+ if (rgbMatch) {
184
+ const r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0')
185
+ const g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0')
186
+ const b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0')
187
+ const a =
188
+ rgbMatch[4] !== undefined
189
+ ? Math.round(parseFloat(rgbMatch[4]) * 255)
190
+ .toString(16)
191
+ .padStart(2, '0')
192
+ : null
193
+
194
+ return `#${r}${g}${b}${a || ''}`.toUpperCase()
195
+ }
196
+
197
+ const hslRegex =
198
+ /^hsla?\((\d{1,3}),\s*([\d.]+%)?,\s*([\d.]+%)?(?:,\s*(\d?\.?\d+))?\)$/i
199
+ const hslMatch = color.match(hslRegex)
200
+ if (hslMatch) {
201
+ const h = parseInt(hslMatch[1]) % 360
202
+ const s = parseFloat(hslMatch[2]) / 100
203
+ const l = parseFloat(hslMatch[3]) / 100
204
+ const a = hslMatch[4] !== undefined ? parseFloat(hslMatch[4]) : 1
205
+
206
+ const rgb = hslToRgb(h, s, l)
207
+ const r = rgb.r.toString(16).padStart(2, '0')
208
+ const g = rgb.g.toString(16).padStart(2, '0')
209
+ const b = rgb.b.toString(16).padStart(2, '0')
210
+ const alpha =
211
+ a < 1
212
+ ? Math.round(a * 255)
213
+ .toString(16)
214
+ .padStart(2, '0')
215
+ : null
216
+
217
+ return `#${r}${g}${b}${alpha || ''}`.toUpperCase()
218
+ }
219
+
220
+ return '#00000'
221
+ }
222
+
223
+ export function hslToRgb(h, s, l) {
224
+ const c = (1 - Math.abs(2 * l - 1)) * s
225
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
226
+ const m = l - c / 2
227
+
228
+ let r = 0,
229
+ g = 0,
230
+ b = 0
231
+ if (h < 60) {
232
+ r = c
233
+ g = x
234
+ b = 0
235
+ } else if (h < 120) {
236
+ r = x
237
+ g = c
238
+ b = 0
239
+ } else if (h < 180) {
240
+ r = 0
241
+ g = c
242
+ b = x
243
+ } else if (h < 240) {
244
+ r = 0
245
+ g = x
246
+ b = c
247
+ } else if (h < 300) {
248
+ r = x
249
+ g = 0
250
+ b = c
251
+ } else {
252
+ r = c
253
+ g = 0
254
+ b = x
255
+ }
256
+
257
+ return {
258
+ r: Math.round((r + m) * 255),
259
+ g: Math.round((g + m) * 255),
260
+ b: Math.round((b + m) * 255),
261
+ }
262
+ }
263
+
264
+ export function calculateRatio(w, h) {
265
+ if (typeof w !== 'number' || typeof h !== 'number' || h === 0) {
266
+ throw new Error(
267
+ 'Les paramètres doivent être des nombres, et h ne doit pas être 0',
268
+ )
269
+ }
270
+
271
+ return (w / h).toFixed(2)
272
+ }
273
+
274
+ export async function imageBase64ToBuffer(base64Content) {
275
+ if (!base64Content.startsWith('data:')) {
276
+ return undefined
277
+ }
278
+
279
+ const mimeTypeMatch = base64Content.match(/^data:(.+);base64,/)
280
+ if (!mimeTypeMatch) {
281
+ return undefined
282
+ }
283
+
284
+ const mimeType = mimeTypeMatch[1]
285
+
286
+ const base64Data = base64Content.replace(/^data:.+;base64,/, '')
287
+
288
+ const buffer = Buffer.from(base64Data, 'base64')
289
+
290
+ const mimeToExtension = {
291
+ 'image/png': 'png',
292
+ 'image/jpeg': 'jpg',
293
+ 'image/jpg': 'jpg',
294
+ 'image/gif': 'gif',
295
+ 'image/webp': 'webp',
296
+ 'image/svg+xml': 'svg',
297
+ }
298
+
299
+ const extension = mimeToExtension[mimeType]
300
+ if (!extension) {
301
+ return undefined
302
+ }
303
+
304
+ const metadata = await sharp(buffer).metadata()
305
+ const { width, height } = metadata
306
+
307
+ if (!width || !height) {
308
+ return undefined
309
+ }
310
+
311
+ const ratio = calculateRatio(width, height)
312
+
313
+ return { mimeType, extension, buffer, width, height, ratio }
314
+ }
315
+
316
+ export function random(length) {
317
+ return Math.random().toString(36).substring(length)
318
+ }
319
+
320
+ export function convertCssToDocxMeasurement(cssValue) {
321
+ const auto = { size: 0, type: 'nil' }
322
+
323
+ if (typeof cssValue !== 'string') {
324
+ return auto
325
+ }
326
+
327
+ if (cssValue.endsWith('%')) {
328
+ const percentage = parseFloat(cssValue)
329
+
330
+ if (isNaN(percentage)) {
331
+ return auto
332
+ }
333
+
334
+ return { size: percentage, type: 'pct' }
335
+ }
336
+
337
+ if (cssValue === 'auto' || cssValue === '0' || cssValue === 'none') {
338
+ return auto
339
+ }
340
+
341
+ if (cssValue.endsWith('px')) {
342
+ const pixels = parseFloat(cssValue)
343
+ if (isNaN(pixels)) {
344
+ return auto
345
+ }
346
+
347
+ return { size: Math.round(pixels * 15), type: 'dxa' }
348
+ }
349
+
350
+ if (cssValue.endsWith('pt')) {
351
+ const points = parseFloat(cssValue)
352
+ if (isNaN(points)) {
353
+ return auto
354
+ }
355
+
356
+ return { size: Math.round(points * 20), type: 'dxa' }
357
+ }
358
+
359
+ return auto
360
+ }
361
+
362
+ /**
363
+ * Convertit une valeur CSS de marge (ex: "15px", "10px 20px", "5px 10px 15px 20px")
364
+ * en un objet avec `top`, `left`, `bottom`, et `right` (espacement en twips).
365
+ *
366
+ * @param cssMargin - Une chaîne représentant les marges CSS.
367
+ * @returns Un objet contenant les marges `top`, `left`, `bottom`, et `right` en twips.
368
+ */
369
+ export function parseCssMargin(cssMargin) {
370
+ const toTwips = value => {
371
+ if (value.endsWith('px')) {
372
+ return Math.round(parseFloat(value) * 15)
373
+ }
374
+ if (value.endsWith('pt')) {
375
+ return Math.round(parseFloat(value) * 20)
376
+ }
377
+ return 0
378
+ }
379
+
380
+ const parts = cssMargin.split(' ').map(part => part.trim())
381
+
382
+ let top, right, bottom, left
383
+
384
+ if (parts.length === 1) {
385
+ top = right = bottom = left = toTwips(parts[0])
386
+ } else if (parts.length === 2) {
387
+ top = bottom = toTwips(parts[0])
388
+ right = left = toTwips(parts[1])
389
+ } else if (parts.length === 3) {
390
+ top = toTwips(parts[0])
391
+ right = left = toTwips(parts[1])
392
+ bottom = toTwips(parts[2])
393
+ } else if (parts.length === 4) {
394
+ top = toTwips(parts[0])
395
+ right = toTwips(parts[1])
396
+ bottom = toTwips(parts[2])
397
+ left = toTwips(parts[3])
398
+ } else {
399
+ top = right = bottom = left = 0
400
+ }
401
+
402
+ return { top, right, bottom, left }
403
+ }
404
+
405
+ export function getListItemNumber(styleType, start) {
406
+ function toRoman(num, isUpper = false) {
407
+ const romanNumerals = [
408
+ { value: 1000, numeral: 'M' },
409
+ { value: 900, numeral: 'CM' },
410
+ { value: 500, numeral: 'D' },
411
+ { value: 400, numeral: 'CD' },
412
+ { value: 100, numeral: 'C' },
413
+ { value: 90, numeral: 'XC' },
414
+ { value: 50, numeral: 'L' },
415
+ { value: 40, numeral: 'XL' },
416
+ { value: 10, numeral: 'X' },
417
+ { value: 9, numeral: 'IX' },
418
+ { value: 5, numeral: 'V' },
419
+ { value: 4, numeral: 'IV' },
420
+ { value: 1, numeral: 'I' },
421
+ ]
422
+
423
+ let result = ''
424
+ let tempNum = num
425
+
426
+ for (let i = 0; i < romanNumerals.length; i++) {
427
+ while (tempNum >= romanNumerals[i].value) {
428
+ result += romanNumerals[i].numeral
429
+ tempNum -= romanNumerals[i].value
430
+ }
431
+ }
432
+
433
+ return isUpper ? result.toUpperCase() : result.toLowerCase()
434
+ }
435
+
436
+ function toAlpha(num, isUpper = false) {
437
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz'
438
+ let result = ''
439
+ while (num > 0) {
440
+ num--
441
+ result = alphabet[num % 26] + result
442
+ num = Math.floor(num / 26)
443
+ }
444
+ return isUpper ? result.toUpperCase() : result.toLowerCase()
445
+ }
446
+
447
+ function toGreek(num, isUpper = false) {
448
+ const greekAlphabet = [
449
+ 'α',
450
+ 'β',
451
+ 'γ',
452
+ 'δ',
453
+ 'ε',
454
+ 'ζ',
455
+ 'η',
456
+ 'θ',
457
+ 'ι',
458
+ 'κ',
459
+ 'λ',
460
+ 'μ',
461
+ 'ν',
462
+ 'ξ',
463
+ 'ο',
464
+ 'π',
465
+ 'ρ',
466
+ 'σ',
467
+ 'τ',
468
+ 'υ',
469
+ 'φ',
470
+ 'χ',
471
+ 'ψ',
472
+ 'ω',
473
+ ]
474
+ let result = greekAlphabet[(num - 1) % 24]
475
+ return isUpper ? result.toUpperCase() : result.toLowerCase()
476
+ }
477
+
478
+ function toLetter(num, isUpper = false) {
479
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz'
480
+ let letter = alphabet[(num - 1) % 26]
481
+ return isUpper ? letter.toUpperCase() : letter.toLowerCase()
482
+ }
483
+
484
+ switch (styleType) {
485
+ case 'decimal':
486
+ return start
487
+ case 'upper-roman':
488
+ return toRoman(start, true)
489
+ case 'lower-roman':
490
+ return toRoman(start, false)
491
+ case 'upper-alpha':
492
+ return toAlpha(start, true)
493
+ case 'lower-alpha':
494
+ return toAlpha(start, false)
495
+ case 'upper-letter':
496
+ return toLetter(start, true)
497
+ case 'lower-letter':
498
+ return toLetter(start, false)
499
+ case 'upper-greek':
500
+ return toGreek(start, true)
501
+ case 'lower-greek':
502
+ return toGreek(start, false)
503
+ case 'circle':
504
+ return '○'
505
+ case 'disc':
506
+ return '•'
507
+ case 'square':
508
+ return '▪'
509
+ case 'none':
510
+ return ''
511
+ default:
512
+ return start
513
+ }
514
+ }
515
+
516
+ export function getWordIndent(level) {
517
+ const baseIndent = 720 // 720 TWIP = 0.5 pouce
518
+ const hangingIndent = 360 // 360 TWIP = 0.25 pouce
519
+
520
+ const left = level * baseIndent
521
+
522
+ return {
523
+ left: left,
524
+ hanging: hangingIndent,
525
+ }
526
+ }
527
+
528
+ export default {
529
+ normalizeHtml,
530
+ pageNodes,
531
+ trim,
532
+ colorToHex,
533
+ hslToRgb,
534
+ normalizeMeasure,
535
+ splitMeasure,
536
+ capitalize,
537
+ calculateRatio,
538
+ arrayToNodeHTMLElement,
539
+ imageBase64ToBuffer,
540
+ convertCssToDocxMeasurement,
541
+ random,
542
+ parseCssMargin,
543
+ getListItemNumber,
544
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ // index.d.ts
2
+
3
+ declare module "html-to-docx" {
4
+ /**
5
+ * Options pour la conversion HTML en DOCX.
6
+ */
7
+ interface HtmlToDocxOptions {
8
+ /**
9
+ * Définit le nom du fichier DOCX généré.
10
+ */
11
+ fileName?: string;
12
+
13
+ /**
14
+ * Définit les marges du document.
15
+ * Exemple : { top: 1, bottom: 1, left: 1, right: 1 }
16
+ */
17
+ margins?: {
18
+ top?: string;
19
+ bottom?: string;
20
+ left?: string;
21
+ right?: string;
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Convertit un contenu HTML en un fichier DOCX.
27
+ *
28
+ * @param html - Le contenu HTML à convertir.
29
+ * @param options - Les options de configuration pour la conversion.
30
+ * @returns Une promesse qui se résout en un `ArrayBuffer` contenant les données du fichier DOCX.
31
+ */
32
+ function htmlToDocx(
33
+ html: string,
34
+ options?: HtmlToDocxOptions
35
+ ): Promise<ArrayBuffer>;
36
+
37
+ export default htmlToDocx;
38
+ }
39
+