@modusoperandi/licit-import-utils 0.1.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.
- package/LICENSE +21 -0
- package/capco.util.d.ts +38 -0
- package/capco.util.js +195 -0
- package/index.d.ts +8 -0
- package/index.js +8 -0
- package/licit-elements.d.ts +878 -0
- package/licit-elements.js +2588 -0
- package/licit-transform.d.ts +360 -0
- package/licit-transform.js +2197 -0
- package/package.json +52 -0
- package/transform.docx.d.ts +16 -0
- package/transform.docx.js +154 -0
- package/transform.utils.d.ts +17 -0
- package/transform.utils.js +155 -0
- package/transform.zip.d.ts +5 -0
- package/transform.zip.js +296 -0
- package/types.d.ts +9 -0
- package/types.js +5 -0
- package/zip.utils.d.ts +6 -0
- package/zip.utils.js +23 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@modusoperandi/licit-import-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"subversion": "1",
|
|
7
|
+
"description": "A utility package for importing files like json or docx into Licit compatible documents",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"types": "index.d.ts",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/MO-Movia/licit-import-utils.git"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:unit": "jest",
|
|
17
|
+
"test:coverage": "jest --env=jsdom --coverage",
|
|
18
|
+
"build:clean": "rm -rf dist/ && rm -f modusoperandi-*.*.*.tgz",
|
|
19
|
+
"lint": "eslint src",
|
|
20
|
+
"ci:build": "tsc -b tsconfig.prod.json --clean && tsc -b tsconfig.prod.json && npx copyfiles@2.4.1 package.json LICENSE dist",
|
|
21
|
+
"ci:bom": "npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --short-PURLs --output-format XML --output-file dist/bom.xml",
|
|
22
|
+
"verify": "npm run lint -- --fix && npm run ci:build && npm run test:coverage && echo 'All Tests Passed!'"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@modusoperandi/mammoth": "^1.7.0-6",
|
|
26
|
+
"jszip": "^3.10.1"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"@modusoperandi/mammoth": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"jszip": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"uuid": "^13.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@modusoperandi/mammoth": "^1.7.0-6",
|
|
41
|
+
"@modusoperandi/eslint-config": "^3.0.3",
|
|
42
|
+
"@types/jest": "^30.0.0",
|
|
43
|
+
"jszip": "^3.10.1",
|
|
44
|
+
"eslint": "^9.39.2",
|
|
45
|
+
"jest": "^30.2.0",
|
|
46
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
47
|
+
"jest-junit": "^16.0.0",
|
|
48
|
+
"ts-jest": "^29.4.6",
|
|
49
|
+
"ts-node": "^10.9.2",
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2026 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
import type { MessageSink } from './types';
|
|
6
|
+
export declare class DocxTransformer {
|
|
7
|
+
private readonly docType;
|
|
8
|
+
readonly messagesSink?: MessageSink;
|
|
9
|
+
constructor(docType: string, messagesSink?: MessageSink);
|
|
10
|
+
transform(arrayBuffer: ArrayBuffer): Promise<Document>;
|
|
11
|
+
private transformElement;
|
|
12
|
+
private transformBullets;
|
|
13
|
+
private transformNonAFDPBullets;
|
|
14
|
+
private transSubBullets;
|
|
15
|
+
private getElement;
|
|
16
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2026 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
const SpecialCharacters = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/gu;
|
|
6
|
+
export class DocxTransformer {
|
|
7
|
+
docType;
|
|
8
|
+
messagesSink;
|
|
9
|
+
constructor(docType, messagesSink) {
|
|
10
|
+
this.docType = docType;
|
|
11
|
+
this.messagesSink = messagesSink;
|
|
12
|
+
}
|
|
13
|
+
async transform(arrayBuffer) {
|
|
14
|
+
const styleMapArray = [
|
|
15
|
+
'u => u',
|
|
16
|
+
"p[style-name='toc 1'] => !",
|
|
17
|
+
"p[style-name='toc 2'] => !",
|
|
18
|
+
"p[style-name='TOC 1'] => !",
|
|
19
|
+
"p[style-name='TOC 2'] => !",
|
|
20
|
+
"p[style-name='TOC 3'] => !",
|
|
21
|
+
"p[style-name='TOC Heading'] => !",
|
|
22
|
+
"p[style-name='Default'] => p.Normal:fresh",
|
|
23
|
+
"p[style-name='List Paragraph'] => p.List-style:fresh",
|
|
24
|
+
];
|
|
25
|
+
if ('Non Specific' !== this.docType) {
|
|
26
|
+
styleMapArray.push("p[style-name='List Paragraph'] => p.List-style:fresh");
|
|
27
|
+
}
|
|
28
|
+
const options = {
|
|
29
|
+
styleMap: styleMapArray,
|
|
30
|
+
transformDocument: this.transformElement.bind(this),
|
|
31
|
+
ignoreEmptyParagraphs: true,
|
|
32
|
+
};
|
|
33
|
+
// Mamoth is big, so we load it only when needed
|
|
34
|
+
const mammoth = await import('@modusoperandi/mammoth/mammoth.browser');
|
|
35
|
+
const result = (await mammoth.default.convertToHtml({ arrayBuffer }, options));
|
|
36
|
+
result.messages?.forEach((m) => {
|
|
37
|
+
this.messagesSink?.(m.type, m.message);
|
|
38
|
+
});
|
|
39
|
+
return new DOMParser().parseFromString(result.value, 'text/html');
|
|
40
|
+
}
|
|
41
|
+
transformElement(element) {
|
|
42
|
+
if (element.numbering &&
|
|
43
|
+
'isOrdered' in element.numbering &&
|
|
44
|
+
!element.numbering.isOrdered) {
|
|
45
|
+
if ('Non Specific' === this.docType) {
|
|
46
|
+
element = this.transformNonAFDPBullets(element);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
element = this.transformBullets(element);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (element.type === 'paragraph') {
|
|
53
|
+
if ('Non Specific' !== this.docType) {
|
|
54
|
+
element = this.transSubBullets(element);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (element?.children?.length > 0) {
|
|
58
|
+
const docElements = element.children.filter((c) => 'type' in c);
|
|
59
|
+
if (docElements.length > 0) {
|
|
60
|
+
const children = docElements.map((c) => this.transformElement(c));
|
|
61
|
+
element = { ...element, children: children };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return element;
|
|
65
|
+
}
|
|
66
|
+
/* Method to transform bullets */
|
|
67
|
+
transformBullets(element) {
|
|
68
|
+
const undefinedCharacter = '?';
|
|
69
|
+
element = {
|
|
70
|
+
...element,
|
|
71
|
+
indent: null,
|
|
72
|
+
numbering: null,
|
|
73
|
+
styleId: 'AFDP Bullet',
|
|
74
|
+
styleName: 'AFDP Bullet',
|
|
75
|
+
};
|
|
76
|
+
if (element.children?.length > 0 &&
|
|
77
|
+
'type' in element.children[0] &&
|
|
78
|
+
element.children[0].children?.length > 0) {
|
|
79
|
+
const childCharcter = element.children[0].children[0];
|
|
80
|
+
if ('value' in childCharcter &&
|
|
81
|
+
childCharcter.value &&
|
|
82
|
+
(SpecialCharacters.test(childCharcter.value) ||
|
|
83
|
+
undefinedCharacter === childCharcter.value.trim())) {
|
|
84
|
+
element.children.splice(0, 1);
|
|
85
|
+
element = {
|
|
86
|
+
...element,
|
|
87
|
+
indent: null,
|
|
88
|
+
numbering: null,
|
|
89
|
+
styleId: 'AFDP Sub-bullet',
|
|
90
|
+
styleName: 'AFDP Sub-bullet',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return element;
|
|
95
|
+
}
|
|
96
|
+
/* Method to transform Non AFDP bullets */
|
|
97
|
+
transformNonAFDPBullets(element) {
|
|
98
|
+
const undefinedCharacter = '?';
|
|
99
|
+
element = {
|
|
100
|
+
...element,
|
|
101
|
+
numbering: { ...element.numbering, symbol: '' },
|
|
102
|
+
};
|
|
103
|
+
if (element?.children?.[0]?.children?.[0]) {
|
|
104
|
+
const childCharcter = element.children[0].children[0];
|
|
105
|
+
if (childCharcter?.value &&
|
|
106
|
+
(SpecialCharacters.test(childCharcter.value) ||
|
|
107
|
+
undefinedCharacter === childCharcter.value.trim())) {
|
|
108
|
+
element.children.splice(0, 1);
|
|
109
|
+
element = {
|
|
110
|
+
...element,
|
|
111
|
+
numbering: { ...element.numbering, symbol: '' },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return element;
|
|
116
|
+
}
|
|
117
|
+
/* Method to transform Sub bullets */
|
|
118
|
+
transSubBullets(element) {
|
|
119
|
+
element.children ??= [];
|
|
120
|
+
for (const child of element.children) {
|
|
121
|
+
if ('type' in child && child.type === 'run') {
|
|
122
|
+
child.children ??= [];
|
|
123
|
+
for (const textChild of child.children) {
|
|
124
|
+
if ('value' in textChild) {
|
|
125
|
+
// Ensure it's a TextElement
|
|
126
|
+
this.getElement(element, textChild);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return element;
|
|
132
|
+
}
|
|
133
|
+
getElement(element, textChild) {
|
|
134
|
+
const undefinedCharacters = ['✪', '', '', '✪✪'];
|
|
135
|
+
const bulletLimit = element.numbering ? 1 : 2;
|
|
136
|
+
let bulletsCount = 0;
|
|
137
|
+
const trimmedValue = textChild.value.trim();
|
|
138
|
+
if (undefinedCharacters.includes(trimmedValue)) {
|
|
139
|
+
bulletsCount += trimmedValue.length;
|
|
140
|
+
element.styleId =
|
|
141
|
+
element.styleId === 'AFDP Bullet' ||
|
|
142
|
+
element.styleId === 'AFDP Sub-bullet' ||
|
|
143
|
+
trimmedValue.length === 2
|
|
144
|
+
? 'AFDP Sub-bullet'
|
|
145
|
+
: 'AFDP Bullet';
|
|
146
|
+
element.styleName = element.styleId;
|
|
147
|
+
textChild.value = '';
|
|
148
|
+
if (bulletsCount > bulletLimit) {
|
|
149
|
+
element.styleId = 'Normal';
|
|
150
|
+
this.messagesSink?.('Warning', 'More than 2 bullet format detected.');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2026 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
import type { LicitDocumentJSON, LicitElementJSON, LicitImageAttrsJSON, LicitTableJSON } from './licit-elements';
|
|
6
|
+
export declare function base64ToFile(imgUrl64: string, fileName: string | number): File;
|
|
7
|
+
export declare function getImageSizeFromBase64(this: void, base64: string, timeout?: number): Promise<{
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function applyImageSizes(this: void, dom: Document | Element): Promise<void>;
|
|
12
|
+
export declare function applyImageSize(this: void, img: HTMLImageElement): Promise<void>;
|
|
13
|
+
export declare function removeEmptyParagraphFromJSON(this: void, json: LicitDocumentJSON): LicitDocumentJSON;
|
|
14
|
+
export declare function processAllTableWidths(this: void, node?: LicitDocumentJSON | LicitElementJSON | LicitImageAttrsJSON | LicitTableJSON): void;
|
|
15
|
+
export declare function processTableWidths(this: void, node: LicitDocumentJSON | LicitElementJSON | LicitImageAttrsJSON | LicitTableJSON): void;
|
|
16
|
+
export declare function updateImageSrc(file: File, img: HTMLImageElement, updateSrc: (src: File) => Promise<string>, fallback: string | Promise<string>): Promise<void>;
|
|
17
|
+
export declare function updateSource(this: void, img: HTMLImageElement, name: string | number, updateSrc: (src: File) => Promise<string>): Promise<void>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* @copyright Copyright 2026 Modus Operandi Inc. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
//Base 64 Images converted to png for Upload to GUI
|
|
6
|
+
export function base64ToFile(imgUrl64, fileName) {
|
|
7
|
+
const base64 = imgUrl64.split(',')[1];
|
|
8
|
+
const byteCharacters = atob(base64);
|
|
9
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
10
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
11
|
+
byteNumbers[i] = byteCharacters.codePointAt(i);
|
|
12
|
+
}
|
|
13
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
14
|
+
const type = imgUrl64.substring(imgUrl64.indexOf(':') + 1, imgUrl64.indexOf(';'));
|
|
15
|
+
const blob = new Blob([byteArray], { type });
|
|
16
|
+
const name = fileName + '.' + type.substring(type.indexOf('/') + 1);
|
|
17
|
+
const file = new File([blob], name, {
|
|
18
|
+
type,
|
|
19
|
+
});
|
|
20
|
+
return file;
|
|
21
|
+
}
|
|
22
|
+
export function getImageSizeFromBase64(base64, timeout = 5000) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const img = new Image();
|
|
25
|
+
img.onload = () => resolve({ width: img.width, height: img.height });
|
|
26
|
+
img.onerror = reject;
|
|
27
|
+
img.src = base64;
|
|
28
|
+
setInterval(() => reject(new Error('Image load timeout')), timeout);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function applyImageSizes(dom) {
|
|
32
|
+
const imgTags = dom.querySelectorAll('img');
|
|
33
|
+
await Promise.all(Array.from(imgTags).map(applyImageSize));
|
|
34
|
+
}
|
|
35
|
+
export async function applyImageSize(img) {
|
|
36
|
+
const imgUrl64 = img?.getAttribute('src');
|
|
37
|
+
if (!imgUrl64) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { width, height } = await getImageSizeFromBase64(imgUrl64);
|
|
41
|
+
const maxWidth = 624;
|
|
42
|
+
let finalWidth = width;
|
|
43
|
+
let finalHeight = height;
|
|
44
|
+
if (finalWidth > maxWidth) {
|
|
45
|
+
const scale = maxWidth / finalWidth;
|
|
46
|
+
finalWidth = maxWidth;
|
|
47
|
+
finalHeight = Math.round(finalHeight * scale);
|
|
48
|
+
}
|
|
49
|
+
img.setAttribute('width', finalWidth.toString());
|
|
50
|
+
img.setAttribute('height', finalHeight.toString());
|
|
51
|
+
}
|
|
52
|
+
export function removeEmptyParagraphFromJSON(json) {
|
|
53
|
+
removeEmptyNodes(json);
|
|
54
|
+
return json;
|
|
55
|
+
}
|
|
56
|
+
function removeEmptyNodes(json) {
|
|
57
|
+
if (Array.isArray(json?.content)) {
|
|
58
|
+
json.content = json.content
|
|
59
|
+
.map((item) => removeEmptyNodes(item))
|
|
60
|
+
.filter((x) => x !== false);
|
|
61
|
+
}
|
|
62
|
+
if (json?.type === 'text' && json?.text === '') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (json?.type === 'paragraph') {
|
|
66
|
+
const attrs = json.attrs;
|
|
67
|
+
if (attrs?.id === 'chspace') {
|
|
68
|
+
return json;
|
|
69
|
+
}
|
|
70
|
+
const titleRegex = /(TableTitle|FigureTitle)$/;
|
|
71
|
+
if (attrs?.styleName && titleRegex.exec(attrs.styleName)) {
|
|
72
|
+
return json;
|
|
73
|
+
}
|
|
74
|
+
if (!json.content || json.content.length === 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (json.content.every((item) => item.type === 'text' && item?.text?.trim?.() === '')) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return json;
|
|
82
|
+
}
|
|
83
|
+
export function processAllTableWidths(node) {
|
|
84
|
+
if (node?.type === 'table') {
|
|
85
|
+
processTableWidths(node);
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(node?.content)) {
|
|
88
|
+
node.content.forEach(processAllTableWidths);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function processTableWidths(node) {
|
|
92
|
+
const tableWidths = [];
|
|
93
|
+
const firstRow = node.content?.[0];
|
|
94
|
+
if (firstRow?.content) {
|
|
95
|
+
for (const cell of firstRow.content) {
|
|
96
|
+
if (Array.isArray(cell.attrs?.colwidth)) {
|
|
97
|
+
tableWidths.push(...cell.attrs.colwidth.map(Number));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const scaledWidths = scaleWidthArray(tableWidths, 619);
|
|
101
|
+
for (const row of node.content) {
|
|
102
|
+
for (let i = 0; i < row?.content?.length; i++) {
|
|
103
|
+
const cell = row.content[i];
|
|
104
|
+
if (scaledWidths[i] != null) {
|
|
105
|
+
cell.attrs.colwidth = [scaledWidths[i]];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function scaleWidthArray(rawWidths, target) {
|
|
112
|
+
const total = rawWidths.reduce((sum, w) => sum + w, 0);
|
|
113
|
+
const scaled = rawWidths.map((w) => Math.floor((w / total) * target));
|
|
114
|
+
let diff = target - scaled.reduce((sum, w) => sum + w, 0);
|
|
115
|
+
// Distribute remaining pixels starting from the largest original width
|
|
116
|
+
const indices = rawWidths
|
|
117
|
+
.map((value, index) => ({ value, index }))
|
|
118
|
+
.sort((a, b) => b.value - a.value)
|
|
119
|
+
.map((item) => item.index);
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (diff > 0) {
|
|
122
|
+
scaled[indices[i % indices.length]]++;
|
|
123
|
+
diff--;
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
return scaled;
|
|
127
|
+
}
|
|
128
|
+
export async function updateImageSrc(file, img, updateSrc, fallback) {
|
|
129
|
+
const readerPromise = updateSrc(file)
|
|
130
|
+
.catch((error) => {
|
|
131
|
+
console.error('Error uploading image:', error);
|
|
132
|
+
return fallback;
|
|
133
|
+
})
|
|
134
|
+
.then((src) => {
|
|
135
|
+
// Update the image node's src property with the new source
|
|
136
|
+
// (New source will be URL from GUI)
|
|
137
|
+
img.src = src;
|
|
138
|
+
img.setAttribute('srcRelative', src); // This is needed for the image inside table to work
|
|
139
|
+
});
|
|
140
|
+
return readerPromise;
|
|
141
|
+
}
|
|
142
|
+
export async function updateSource(img, name, updateSrc) {
|
|
143
|
+
try {
|
|
144
|
+
await applyImageSize(img);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.warn('Could not extract image size', err);
|
|
148
|
+
}
|
|
149
|
+
const imgUrl64 = img?.getAttribute('src');
|
|
150
|
+
if (imgUrl64) {
|
|
151
|
+
// Convert to File and upload/update src
|
|
152
|
+
const file = base64ToFile(imgUrl64, name);
|
|
153
|
+
await updateImageSrc(file, img, (f) => updateSrc(f), imgUrl64);
|
|
154
|
+
}
|
|
155
|
+
}
|