@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/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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @license MIT
3
+ * @copyright Copyright 2026 Modus Operandi Inc. All Rights Reserved.
4
+ */
5
+ export declare function parseFrameMakerHTM5Zip(this: void, file: File, updateSrc: (src: File) => Promise<string>): Promise<Element[]>;