@lkaopremier/html-to-docx 0.0.1

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.
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
+