@kimdayoun/hwpx-mcp 0.3.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/README.md +356 -0
- package/dist/HangingIndentCalculator.d.ts +69 -0
- package/dist/HangingIndentCalculator.js +348 -0
- package/dist/HwpxDocument.d.ts +1607 -0
- package/dist/HwpxDocument.js +10845 -0
- package/dist/HwpxParser.d.ts +76 -0
- package/dist/HwpxParser.js +3848 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4317 -0
- package/dist/types.d.ts +1641 -0
- package/dist/types.js +8 -0
- package/package.json +67 -0
|
@@ -0,0 +1,3848 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.HwpxParser = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
function generateId() {
|
|
20
|
+
return Math.random().toString(36).substring(2, 11);
|
|
21
|
+
}
|
|
22
|
+
class HwpxParser {
|
|
23
|
+
static async parse(zip) {
|
|
24
|
+
const content = {
|
|
25
|
+
metadata: {},
|
|
26
|
+
sections: [],
|
|
27
|
+
images: new Map(),
|
|
28
|
+
binItems: new Map(),
|
|
29
|
+
binData: new Map(),
|
|
30
|
+
footnotes: [],
|
|
31
|
+
endnotes: [],
|
|
32
|
+
};
|
|
33
|
+
this.styles = {
|
|
34
|
+
charShapes: new Map(),
|
|
35
|
+
paraShapes: new Map(),
|
|
36
|
+
fonts: new Map(),
|
|
37
|
+
fontsByLang: new Map(),
|
|
38
|
+
borderFills: new Map(),
|
|
39
|
+
tabDefs: new Map(),
|
|
40
|
+
numberings: new Map(),
|
|
41
|
+
bullets: new Map(),
|
|
42
|
+
styles: new Map(),
|
|
43
|
+
memoShapes: new Map(),
|
|
44
|
+
};
|
|
45
|
+
const headerXml = await this.readXmlFile(zip, 'Contents/header.xml');
|
|
46
|
+
if (headerXml) {
|
|
47
|
+
content.metadata = this.parseMetadata(headerXml);
|
|
48
|
+
content.docSetting = this.parseDocSetting(headerXml);
|
|
49
|
+
this.parseStyles(headerXml);
|
|
50
|
+
this.parseMemoShapes(headerXml);
|
|
51
|
+
content.compatibleDocument = this.parseCompatibleDocument(headerXml);
|
|
52
|
+
// Assign parsed styles to content
|
|
53
|
+
content.styles = this.styles;
|
|
54
|
+
}
|
|
55
|
+
await this.parseImages(zip, content);
|
|
56
|
+
await this.parseBinDataStorage(zip, content);
|
|
57
|
+
let sectionIndex = 0;
|
|
58
|
+
while (true) {
|
|
59
|
+
const sectionPath = `Contents/section${sectionIndex}.xml`;
|
|
60
|
+
const sectionXml = await this.readXmlFile(zip, sectionPath);
|
|
61
|
+
if (!sectionXml)
|
|
62
|
+
break;
|
|
63
|
+
const section = this.parseSection(sectionXml, content, sectionIndex);
|
|
64
|
+
content.sections.push(section);
|
|
65
|
+
sectionIndex++;
|
|
66
|
+
}
|
|
67
|
+
// Parse Scripts (optional)
|
|
68
|
+
const scriptCode = await this.parseScriptCode(zip);
|
|
69
|
+
if (scriptCode) {
|
|
70
|
+
content.scriptCode = scriptCode;
|
|
71
|
+
}
|
|
72
|
+
// Parse XMLTemplate (optional)
|
|
73
|
+
const xmlTemplate = await this.parseXmlTemplate(zip);
|
|
74
|
+
if (xmlTemplate) {
|
|
75
|
+
content.xmlTemplate = xmlTemplate;
|
|
76
|
+
}
|
|
77
|
+
return content;
|
|
78
|
+
}
|
|
79
|
+
static async parseScriptCode(zip) {
|
|
80
|
+
const scriptsFolder = zip.folder('Scripts');
|
|
81
|
+
if (!scriptsFolder)
|
|
82
|
+
return undefined;
|
|
83
|
+
const scriptCode = {};
|
|
84
|
+
// Read DefaultJScript
|
|
85
|
+
const defaultJScript = await this.readXmlFile(zip, 'Scripts/DefaultJScript');
|
|
86
|
+
if (defaultJScript) {
|
|
87
|
+
scriptCode.source = defaultJScript;
|
|
88
|
+
scriptCode.type = 'JScript';
|
|
89
|
+
}
|
|
90
|
+
// Read JScriptVersion
|
|
91
|
+
const versionFile = await this.readXmlFile(zip, 'Scripts/JScriptVersion');
|
|
92
|
+
if (versionFile) {
|
|
93
|
+
scriptCode.version = versionFile.trim();
|
|
94
|
+
}
|
|
95
|
+
// Read Header script if exists
|
|
96
|
+
const headerScript = await this.readXmlFile(zip, 'Scripts/Header');
|
|
97
|
+
if (headerScript) {
|
|
98
|
+
scriptCode.header = headerScript;
|
|
99
|
+
}
|
|
100
|
+
// Read PreScript and PostScript if they exist as separate files
|
|
101
|
+
const preScriptNames = Object.keys(zip.files).filter(f => f.startsWith('Scripts/PreScript'));
|
|
102
|
+
if (preScriptNames.length > 0) {
|
|
103
|
+
scriptCode.preScript = [];
|
|
104
|
+
for (const name of preScriptNames) {
|
|
105
|
+
const code = await this.readXmlFile(zip, name);
|
|
106
|
+
if (code) {
|
|
107
|
+
scriptCode.preScript.push({ name: name.replace('Scripts/', ''), code });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const postScriptNames = Object.keys(zip.files).filter(f => f.startsWith('Scripts/PostScript'));
|
|
112
|
+
if (postScriptNames.length > 0) {
|
|
113
|
+
scriptCode.postScript = [];
|
|
114
|
+
for (const name of postScriptNames) {
|
|
115
|
+
const code = await this.readXmlFile(zip, name);
|
|
116
|
+
if (code) {
|
|
117
|
+
scriptCode.postScript.push({ name: name.replace('Scripts/', ''), code });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return Object.keys(scriptCode).length > 0 ? scriptCode : undefined;
|
|
122
|
+
}
|
|
123
|
+
static async parseXmlTemplate(zip) {
|
|
124
|
+
const xmlTemplate = {};
|
|
125
|
+
// Read Schema
|
|
126
|
+
const schema = await this.readXmlFile(zip, 'XMLTemplate/Schema');
|
|
127
|
+
if (schema) {
|
|
128
|
+
xmlTemplate.schema = schema;
|
|
129
|
+
}
|
|
130
|
+
// Read Instance
|
|
131
|
+
const instance = await this.readXmlFile(zip, 'XMLTemplate/Instance');
|
|
132
|
+
if (instance) {
|
|
133
|
+
xmlTemplate.instance = instance;
|
|
134
|
+
}
|
|
135
|
+
return (xmlTemplate.schema || xmlTemplate.instance) ? xmlTemplate : undefined;
|
|
136
|
+
}
|
|
137
|
+
static async readXmlFile(zip, path) {
|
|
138
|
+
const file = zip.file(path);
|
|
139
|
+
if (!file)
|
|
140
|
+
return null;
|
|
141
|
+
return await file.async('string');
|
|
142
|
+
}
|
|
143
|
+
static parseMetadata(xml) {
|
|
144
|
+
const metadata = {};
|
|
145
|
+
const extract = (tag) => {
|
|
146
|
+
const regex = new RegExp(`<(?:hh:)?${tag}[^>]*>([^<]*)</(?:hh:)?${tag}>`);
|
|
147
|
+
const match = xml.match(regex);
|
|
148
|
+
return match?.[1];
|
|
149
|
+
};
|
|
150
|
+
metadata.title = extract('title');
|
|
151
|
+
metadata.creator = extract('creator');
|
|
152
|
+
metadata.createdDate = extract('createdDate');
|
|
153
|
+
metadata.modifiedDate = extract('modifiedDate');
|
|
154
|
+
metadata.description = extract('description');
|
|
155
|
+
metadata.subject = extract('subject');
|
|
156
|
+
// Parse keywords
|
|
157
|
+
const keywordsMatch = xml.match(/<(?:hh:)?keywords[^>]*>([^<]*)<\/(?:hh:)?keywords>/i);
|
|
158
|
+
if (keywordsMatch) {
|
|
159
|
+
metadata.keywords = keywordsMatch[1].split(',').map(k => k.trim()).filter(k => k);
|
|
160
|
+
}
|
|
161
|
+
// Parse comments
|
|
162
|
+
const commentsMatch = xml.match(/<(?:hh:)?comments[^>]*>([^<]*)<\/(?:hh:)?comments>/i);
|
|
163
|
+
if (commentsMatch) {
|
|
164
|
+
metadata.comments = commentsMatch[1];
|
|
165
|
+
}
|
|
166
|
+
// Parse forbidden strings
|
|
167
|
+
const forbiddenRegex = /<(?:hh:)?forbidden[^>]*>([^<]*)<\/(?:hh:)?forbidden>/gi;
|
|
168
|
+
const forbiddenStrings = [];
|
|
169
|
+
let forbiddenMatch;
|
|
170
|
+
while ((forbiddenMatch = forbiddenRegex.exec(xml)) !== null) {
|
|
171
|
+
forbiddenStrings.push(forbiddenMatch[1]);
|
|
172
|
+
}
|
|
173
|
+
if (forbiddenStrings.length > 0) {
|
|
174
|
+
metadata.forbiddenStrings = forbiddenStrings;
|
|
175
|
+
}
|
|
176
|
+
return metadata;
|
|
177
|
+
}
|
|
178
|
+
static parseDocSetting(xml) {
|
|
179
|
+
const docSetting = {};
|
|
180
|
+
// Parse beginNumber
|
|
181
|
+
const beginNumMatch = xml.match(/<(?:hh:)?beginNum[^>]*>([\s\S]*?)<\/(?:hh:)?beginNum>|<(?:hh:)?beginNum([^>]*)\/>/i);
|
|
182
|
+
if (beginNumMatch) {
|
|
183
|
+
const content = beginNumMatch[1] || beginNumMatch[2] || '';
|
|
184
|
+
docSetting.beginNumber = {};
|
|
185
|
+
const pageMatch = content.match(/page="(\d+)"/);
|
|
186
|
+
if (pageMatch)
|
|
187
|
+
docSetting.beginNumber.page = parseInt(pageMatch[1]);
|
|
188
|
+
const footnoteMatch = content.match(/footnote="(\d+)"/);
|
|
189
|
+
if (footnoteMatch)
|
|
190
|
+
docSetting.beginNumber.footnote = parseInt(footnoteMatch[1]);
|
|
191
|
+
const endnoteMatch = content.match(/endnote="(\d+)"/);
|
|
192
|
+
if (endnoteMatch)
|
|
193
|
+
docSetting.beginNumber.endnote = parseInt(endnoteMatch[1]);
|
|
194
|
+
const pictureMatch = content.match(/(?:picture|pic)="(\d+)"/);
|
|
195
|
+
if (pictureMatch)
|
|
196
|
+
docSetting.beginNumber.picture = parseInt(pictureMatch[1]);
|
|
197
|
+
const tableMatch = content.match(/(?:table|tbl)="(\d+)"/);
|
|
198
|
+
if (tableMatch)
|
|
199
|
+
docSetting.beginNumber.table = parseInt(tableMatch[1]);
|
|
200
|
+
const equationMatch = content.match(/equation="(\d+)"/);
|
|
201
|
+
if (equationMatch)
|
|
202
|
+
docSetting.beginNumber.equation = parseInt(equationMatch[1]);
|
|
203
|
+
const totalPageMatch = content.match(/totalPage="(\d+)"/);
|
|
204
|
+
if (totalPageMatch)
|
|
205
|
+
docSetting.beginNumber.totalPage = parseInt(totalPageMatch[1]);
|
|
206
|
+
}
|
|
207
|
+
// Parse caretPos
|
|
208
|
+
const caretPosMatch = xml.match(/<(?:hh:)?caretPos[^>]*>([\s\S]*?)<\/(?:hh:)?caretPos>|<(?:hh:)?caretPos([^>]*)\/>/i);
|
|
209
|
+
if (caretPosMatch) {
|
|
210
|
+
const content = caretPosMatch[1] || caretPosMatch[2] || '';
|
|
211
|
+
docSetting.caretPos = {};
|
|
212
|
+
const listMatch = content.match(/list="([^"]*)"/);
|
|
213
|
+
if (listMatch)
|
|
214
|
+
docSetting.caretPos.list = listMatch[1];
|
|
215
|
+
const paraMatch = content.match(/para="([^"]*)"/);
|
|
216
|
+
if (paraMatch)
|
|
217
|
+
docSetting.caretPos.para = paraMatch[1];
|
|
218
|
+
const posMatch = content.match(/pos="([^"]*)"/);
|
|
219
|
+
if (posMatch)
|
|
220
|
+
docSetting.caretPos.pos = posMatch[1];
|
|
221
|
+
}
|
|
222
|
+
return Object.keys(docSetting).length > 0 ? docSetting : undefined;
|
|
223
|
+
}
|
|
224
|
+
static parseMemoShapes(xml) {
|
|
225
|
+
const memoShapeRegex = /<(?:hh:)?memoShape[^>]*>([\s\S]*?)<\/(?:hh:)?memoShape>|<(?:hh:)?memoShape([^>]*)\/>/gi;
|
|
226
|
+
let match;
|
|
227
|
+
while ((match = memoShapeRegex.exec(xml)) !== null) {
|
|
228
|
+
const content = match[1] || match[2] || '';
|
|
229
|
+
const idMatch = content.match(/id="(\d+)"/);
|
|
230
|
+
const id = idMatch ? parseInt(idMatch[1]) : this.styles.memoShapes.size;
|
|
231
|
+
const memoShape = { id };
|
|
232
|
+
const widthMatch = content.match(/width="(\d+)"/);
|
|
233
|
+
if (widthMatch)
|
|
234
|
+
memoShape.width = parseInt(widthMatch[1]);
|
|
235
|
+
const lineTypeMatch = content.match(/lineType="([^"]*)"/);
|
|
236
|
+
if (lineTypeMatch)
|
|
237
|
+
memoShape.lineType = lineTypeMatch[1];
|
|
238
|
+
const lineColorMatch = content.match(/lineColor="([^"]*)"/);
|
|
239
|
+
if (lineColorMatch)
|
|
240
|
+
memoShape.lineColor = lineColorMatch[1];
|
|
241
|
+
const fillColorMatch = content.match(/fillColor="([^"]*)"/);
|
|
242
|
+
if (fillColorMatch)
|
|
243
|
+
memoShape.fillColor = fillColorMatch[1];
|
|
244
|
+
const activeColorMatch = content.match(/activeColor="([^"]*)"/);
|
|
245
|
+
if (activeColorMatch)
|
|
246
|
+
memoShape.activeColor = activeColorMatch[1];
|
|
247
|
+
const memoTypeMatch = content.match(/memoType="([^"]*)"/);
|
|
248
|
+
if (memoTypeMatch)
|
|
249
|
+
memoShape.memoType = memoTypeMatch[1];
|
|
250
|
+
this.styles.memoShapes.set(id, memoShape);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
static parseStyles(xml) {
|
|
254
|
+
this.parseFonts(xml);
|
|
255
|
+
this.parseCharShapes(xml);
|
|
256
|
+
this.parseParaShapes(xml);
|
|
257
|
+
this.parseBorderFills(xml);
|
|
258
|
+
this.parseTabDefs(xml);
|
|
259
|
+
this.parseNumberings(xml);
|
|
260
|
+
this.parseBullets(xml);
|
|
261
|
+
this.parseStyleDefs(xml);
|
|
262
|
+
}
|
|
263
|
+
static parseFonts(xml) {
|
|
264
|
+
// Parse fonts from all fontfaces (HANGUL, LATIN, HANJA, JAPANESE, OTHER, SYMBOL, USER)
|
|
265
|
+
const languages = ['HANGUL', 'LATIN', 'HANJA', 'JAPANESE', 'OTHER', 'SYMBOL', 'USER'];
|
|
266
|
+
for (const lang of languages) {
|
|
267
|
+
const fontFaceRegex = new RegExp(`<hh:fontface[^>]*lang="${lang}"[^>]*>([\\s\\S]*?)<\\/hh:fontface>`, 'i');
|
|
268
|
+
const fontFaceMatch = xml.match(fontFaceRegex);
|
|
269
|
+
if (fontFaceMatch) {
|
|
270
|
+
const fontRegex = /<hh:font[^>]*id="(\d+)"[^>]*face="([^"]*)"/gi;
|
|
271
|
+
let match;
|
|
272
|
+
while ((match = fontRegex.exec(fontFaceMatch[1])) !== null) {
|
|
273
|
+
const fontId = parseInt(match[1]);
|
|
274
|
+
const fontName = match[2];
|
|
275
|
+
// Store with language prefix to avoid ID conflicts between languages
|
|
276
|
+
const key = `${lang.toLowerCase()}_${fontId}`;
|
|
277
|
+
this.styles.fontsByLang.set(key, fontName);
|
|
278
|
+
// Also store in main fonts map (HANGUL takes priority for backward compatibility)
|
|
279
|
+
if (lang === 'HANGUL' || !this.styles.fonts.has(fontId)) {
|
|
280
|
+
this.styles.fonts.set(fontId, fontName);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Fallback if no fontfaces found
|
|
286
|
+
if (this.styles.fonts.size === 0) {
|
|
287
|
+
const fontRegex = /<(?:hh:)?font[^>]*face="([^"]*)"[^>]*>/gi;
|
|
288
|
+
let match;
|
|
289
|
+
let fontId = 0;
|
|
290
|
+
while ((match = fontRegex.exec(xml)) !== null) {
|
|
291
|
+
this.styles.fonts.set(fontId, match[1]);
|
|
292
|
+
fontId++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
static parseCharShapes(xml) {
|
|
297
|
+
// Capture tag name (charShape or charPr) in group 1, content in group 2
|
|
298
|
+
const charShapeRegex = /<(?:hh:)?(charShape|charPr)([^>]*)>([\s\S]*?)<\/(?:hh:)?(?:charShape|charPr)>/gi;
|
|
299
|
+
const charShapeRegexSelfClosing = /<(?:hh:)?(charShape|charPr)([^/>]*)\/>/gi;
|
|
300
|
+
let match;
|
|
301
|
+
let shapeId = 0;
|
|
302
|
+
const parseCharShape = (shapeContent, tagName) => {
|
|
303
|
+
const charShape = { id: shapeId, tagName };
|
|
304
|
+
const idMatch = shapeContent.match(/\bid="(\d+)"/);
|
|
305
|
+
if (idMatch) {
|
|
306
|
+
shapeId = parseInt(idMatch[1]);
|
|
307
|
+
charShape.id = shapeId;
|
|
308
|
+
}
|
|
309
|
+
const heightMatch = shapeContent.match(/height="(\d+)"/);
|
|
310
|
+
if (heightMatch) {
|
|
311
|
+
charShape.fontSize = parseInt(heightMatch[1]) / 100;
|
|
312
|
+
}
|
|
313
|
+
const boldTagMatch = shapeContent.match(/<(?:hh:)?bold\s*\/>/i);
|
|
314
|
+
const boldAttrMatch = shapeContent.match(/bold="([^"]*)"/);
|
|
315
|
+
charShape.bold = !!boldTagMatch || boldAttrMatch?.[1] === '1' || boldAttrMatch?.[1] === 'true';
|
|
316
|
+
const italicTagMatch = shapeContent.match(/<(?:hh:)?italic\s*\/>/i);
|
|
317
|
+
const italicAttrMatch = shapeContent.match(/italic="([^"]*)"/);
|
|
318
|
+
charShape.italic = !!italicTagMatch || italicAttrMatch?.[1] === '1' || italicAttrMatch?.[1] === 'true';
|
|
319
|
+
const underlineMatch = shapeContent.match(/<(?:hh:)?underline[^>]*type="([^"]*)"[^>]*(?:shape="([^"]*)")?[^>]*(?:color="([^"]*)")?/i);
|
|
320
|
+
if (underlineMatch && underlineMatch[1] !== 'NONE') {
|
|
321
|
+
const underlineTypeMap = {
|
|
322
|
+
'BOTTOM': 'Bottom', 'CENTER': 'Center', 'TOP': 'Top', 'NONE': 'None'
|
|
323
|
+
};
|
|
324
|
+
const shapeMap = {
|
|
325
|
+
'SOLID': 'Solid', 'DASH': 'Dash', 'DOT': 'Dot', 'DASH_DOT': 'DashDot',
|
|
326
|
+
'DASH_DOT_DOT': 'DashDotDot', 'LONG_DASH': 'LongDash', 'CIRCLE_DOT': 'CircleDot',
|
|
327
|
+
'DOUBLE_SLIM': 'DoubleSlim', 'SLIM_THICK': 'SlimThick', 'THICK_SLIM': 'ThickSlim',
|
|
328
|
+
'SLIM_THICK_SLIM': 'SlimThickSlim', 'NONE': 'None'
|
|
329
|
+
};
|
|
330
|
+
charShape.underline = {
|
|
331
|
+
type: underlineTypeMap[underlineMatch[1]?.toUpperCase()] || 'Bottom',
|
|
332
|
+
shape: shapeMap[underlineMatch[2]?.toUpperCase()] || 'Solid',
|
|
333
|
+
color: underlineMatch[3] || '#000000'
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const strikeMatch = shapeContent.match(/<(?:hh:)?strikeout[^>]*type="([^"]*)"[^>]*(?:shape="([^"]*)")?[^>]*(?:color="([^"]*)")?/i);
|
|
337
|
+
if (strikeMatch && strikeMatch[1] !== 'NONE') {
|
|
338
|
+
const strikeTypeMap = {
|
|
339
|
+
'NONE': 'None', 'CONTINUOUS': 'Continuous'
|
|
340
|
+
};
|
|
341
|
+
const shapeMap = {
|
|
342
|
+
'SOLID': 'Solid', 'DASH': 'Dash', 'DOT': 'Dot', 'DASH_DOT': 'DashDot',
|
|
343
|
+
'DASH_DOT_DOT': 'DashDotDot', 'LONG_DASH': 'LongDash', 'NONE': 'None'
|
|
344
|
+
};
|
|
345
|
+
charShape.strikeout = {
|
|
346
|
+
type: strikeTypeMap[strikeMatch[1]?.toUpperCase()] || 'Continuous',
|
|
347
|
+
shape: shapeMap[strikeMatch[2]?.toUpperCase()] || 'Solid',
|
|
348
|
+
color: strikeMatch[3] || '#000000'
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const colorMatch = shapeContent.match(/textColor="([^"]*)"/);
|
|
352
|
+
if (colorMatch && colorMatch[1] !== '#000000') {
|
|
353
|
+
charShape.color = colorMatch[1];
|
|
354
|
+
}
|
|
355
|
+
const bgColorMatch = shapeContent.match(/shadeColor="([^"]*)"/);
|
|
356
|
+
if (bgColorMatch && bgColorMatch[1] !== 'none') {
|
|
357
|
+
charShape.backgroundColor = bgColorMatch[1];
|
|
358
|
+
}
|
|
359
|
+
const fontRefMatch = shapeContent.match(/<(?:hh:)?fontRef[^>]*/i);
|
|
360
|
+
if (fontRefMatch) {
|
|
361
|
+
const fontRefContent = fontRefMatch[0];
|
|
362
|
+
charShape.fontRefs = {};
|
|
363
|
+
charShape.fontNames = {};
|
|
364
|
+
const hangulMatch = fontRefContent.match(/hangul="(\d+)"/);
|
|
365
|
+
if (hangulMatch) {
|
|
366
|
+
charShape.fontRefs.hangul = parseInt(hangulMatch[1]);
|
|
367
|
+
charShape.fontNames.hangul = this.styles.fontsByLang.get(`hangul_${hangulMatch[1]}`) || this.styles.fonts.get(parseInt(hangulMatch[1]));
|
|
368
|
+
charShape.fontName = charShape.fontNames.hangul; // Default to hangul font
|
|
369
|
+
}
|
|
370
|
+
const latinMatch = fontRefContent.match(/latin="(\d+)"/);
|
|
371
|
+
if (latinMatch) {
|
|
372
|
+
charShape.fontRefs.latin = parseInt(latinMatch[1]);
|
|
373
|
+
charShape.fontNames.latin = this.styles.fontsByLang.get(`latin_${latinMatch[1]}`) || this.styles.fonts.get(parseInt(latinMatch[1]));
|
|
374
|
+
}
|
|
375
|
+
const hanjaMatch = fontRefContent.match(/hanja="(\d+)"/);
|
|
376
|
+
if (hanjaMatch) {
|
|
377
|
+
charShape.fontRefs.hanja = parseInt(hanjaMatch[1]);
|
|
378
|
+
charShape.fontNames.hanja = this.styles.fontsByLang.get(`hanja_${hanjaMatch[1]}`) || this.styles.fonts.get(parseInt(hanjaMatch[1]));
|
|
379
|
+
}
|
|
380
|
+
const japaneseMatch = fontRefContent.match(/japanese="(\d+)"/);
|
|
381
|
+
if (japaneseMatch) {
|
|
382
|
+
charShape.fontRefs.japanese = parseInt(japaneseMatch[1]);
|
|
383
|
+
charShape.fontNames.japanese = this.styles.fontsByLang.get(`japanese_${japaneseMatch[1]}`) || this.styles.fonts.get(parseInt(japaneseMatch[1]));
|
|
384
|
+
}
|
|
385
|
+
const otherMatch = fontRefContent.match(/other="(\d+)"/);
|
|
386
|
+
if (otherMatch) {
|
|
387
|
+
charShape.fontRefs.other = parseInt(otherMatch[1]);
|
|
388
|
+
charShape.fontNames.other = this.styles.fontsByLang.get(`other_${otherMatch[1]}`) || this.styles.fonts.get(parseInt(otherMatch[1]));
|
|
389
|
+
}
|
|
390
|
+
const symbolMatch = fontRefContent.match(/symbol="(\d+)"/);
|
|
391
|
+
if (symbolMatch) {
|
|
392
|
+
charShape.fontRefs.symbol = parseInt(symbolMatch[1]);
|
|
393
|
+
charShape.fontNames.symbol = this.styles.fontsByLang.get(`symbol_${symbolMatch[1]}`) || this.styles.fonts.get(parseInt(symbolMatch[1]));
|
|
394
|
+
}
|
|
395
|
+
const userMatch = fontRefContent.match(/user="(\d+)"/);
|
|
396
|
+
if (userMatch) {
|
|
397
|
+
charShape.fontRefs.user = parseInt(userMatch[1]);
|
|
398
|
+
charShape.fontNames.user = this.styles.fontsByLang.get(`user_${userMatch[1]}`) || this.styles.fonts.get(parseInt(userMatch[1]));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Parse spacing element - extract each attribute separately (order-independent)
|
|
402
|
+
const spacingElemMatch = shapeContent.match(/<(?:hh:)?spacing([^>]*)\/?>/i);
|
|
403
|
+
if (spacingElemMatch) {
|
|
404
|
+
const spacingAttrs = spacingElemMatch[1];
|
|
405
|
+
const hangulMatch = spacingAttrs.match(/hangul="(-?\d+)"/);
|
|
406
|
+
const latinMatch = spacingAttrs.match(/latin="(-?\d+)"/);
|
|
407
|
+
const hanjaMatch = spacingAttrs.match(/hanja="(-?\d+)"/);
|
|
408
|
+
const japaneseMatch = spacingAttrs.match(/japanese="(-?\d+)"/);
|
|
409
|
+
const otherMatch = spacingAttrs.match(/other="(-?\d+)"/);
|
|
410
|
+
const symbolMatch = spacingAttrs.match(/symbol="(-?\d+)"/);
|
|
411
|
+
const userMatch = spacingAttrs.match(/user="(-?\d+)"/);
|
|
412
|
+
charShape.charSpacing = {
|
|
413
|
+
hangul: hangulMatch ? parseInt(hangulMatch[1]) : 0,
|
|
414
|
+
latin: latinMatch ? parseInt(latinMatch[1]) : 0,
|
|
415
|
+
hanja: hanjaMatch ? parseInt(hanjaMatch[1]) : 0,
|
|
416
|
+
japanese: japaneseMatch ? parseInt(japaneseMatch[1]) : 0,
|
|
417
|
+
other: otherMatch ? parseInt(otherMatch[1]) : 0,
|
|
418
|
+
symbol: symbolMatch ? parseInt(symbolMatch[1]) : 0,
|
|
419
|
+
user: userMatch ? parseInt(userMatch[1]) : 0
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
// Fallback: simple spacing attribute
|
|
424
|
+
const charSpacingAttrMatch = shapeContent.match(/spacing="(-?\d+)"/);
|
|
425
|
+
if (charSpacingAttrMatch) {
|
|
426
|
+
const spacingValue = parseInt(charSpacingAttrMatch[1]);
|
|
427
|
+
charShape.charSpacing = {
|
|
428
|
+
hangul: spacingValue, latin: spacingValue, hanja: spacingValue,
|
|
429
|
+
japanese: spacingValue, other: spacingValue, symbol: spacingValue, user: spacingValue
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Parse relSz element - extract each attribute separately (order-independent)
|
|
434
|
+
const relSzElemMatch = shapeContent.match(/<(?:hh:)?relSz([^>]*)\/?>/i);
|
|
435
|
+
if (relSzElemMatch) {
|
|
436
|
+
const relSzAttrs = relSzElemMatch[1];
|
|
437
|
+
const hangulMatch = relSzAttrs.match(/hangul="(\d+)"/);
|
|
438
|
+
const latinMatch = relSzAttrs.match(/latin="(\d+)"/);
|
|
439
|
+
const hanjaMatch = relSzAttrs.match(/hanja="(\d+)"/);
|
|
440
|
+
const japaneseMatch = relSzAttrs.match(/japanese="(\d+)"/);
|
|
441
|
+
const otherMatch = relSzAttrs.match(/other="(\d+)"/);
|
|
442
|
+
const symbolMatch = relSzAttrs.match(/symbol="(\d+)"/);
|
|
443
|
+
const userMatch = relSzAttrs.match(/user="(\d+)"/);
|
|
444
|
+
charShape.relSize = {
|
|
445
|
+
hangul: hangulMatch ? parseInt(hangulMatch[1]) : 100,
|
|
446
|
+
latin: latinMatch ? parseInt(latinMatch[1]) : 100,
|
|
447
|
+
hanja: hanjaMatch ? parseInt(hanjaMatch[1]) : 100,
|
|
448
|
+
japanese: japaneseMatch ? parseInt(japaneseMatch[1]) : 100,
|
|
449
|
+
other: otherMatch ? parseInt(otherMatch[1]) : 100,
|
|
450
|
+
symbol: symbolMatch ? parseInt(symbolMatch[1]) : 100,
|
|
451
|
+
user: userMatch ? parseInt(userMatch[1]) : 100
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// Fallback: simple relSz attribute
|
|
456
|
+
const relSzAttrMatch = shapeContent.match(/relSz="(\d+)"/);
|
|
457
|
+
if (relSzAttrMatch) {
|
|
458
|
+
const relSzValue = parseInt(relSzAttrMatch[1]);
|
|
459
|
+
charShape.relSize = {
|
|
460
|
+
hangul: relSzValue, latin: relSzValue, hanja: relSzValue,
|
|
461
|
+
japanese: relSzValue, other: relSzValue, symbol: relSzValue, user: relSzValue
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Parse offset element - extract each attribute separately (order-independent)
|
|
466
|
+
const charOffsetElemMatch = shapeContent.match(/<(?:hh:)?offset([^>]*)\/?>/i);
|
|
467
|
+
if (charOffsetElemMatch) {
|
|
468
|
+
const offsetAttrs = charOffsetElemMatch[1];
|
|
469
|
+
const hangulMatch = offsetAttrs.match(/hangul="(-?\d+)"/);
|
|
470
|
+
const latinMatch = offsetAttrs.match(/latin="(-?\d+)"/);
|
|
471
|
+
const hanjaMatch = offsetAttrs.match(/hanja="(-?\d+)"/);
|
|
472
|
+
const japaneseMatch = offsetAttrs.match(/japanese="(-?\d+)"/);
|
|
473
|
+
const otherMatch = offsetAttrs.match(/other="(-?\d+)"/);
|
|
474
|
+
const symbolMatch = offsetAttrs.match(/symbol="(-?\d+)"/);
|
|
475
|
+
const userMatch = offsetAttrs.match(/user="(-?\d+)"/);
|
|
476
|
+
charShape.charOffset = {
|
|
477
|
+
hangul: hangulMatch ? parseInt(hangulMatch[1]) : 0,
|
|
478
|
+
latin: latinMatch ? parseInt(latinMatch[1]) : 0,
|
|
479
|
+
hanja: hanjaMatch ? parseInt(hanjaMatch[1]) : 0,
|
|
480
|
+
japanese: japaneseMatch ? parseInt(japaneseMatch[1]) : 0,
|
|
481
|
+
other: otherMatch ? parseInt(otherMatch[1]) : 0,
|
|
482
|
+
symbol: symbolMatch ? parseInt(symbolMatch[1]) : 0,
|
|
483
|
+
user: userMatch ? parseInt(userMatch[1]) : 0
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
// Fallback: simple offset attribute
|
|
488
|
+
const charOffsetAttrMatch = shapeContent.match(/offset="(-?\d+)"/);
|
|
489
|
+
if (charOffsetAttrMatch) {
|
|
490
|
+
const offsetValue = parseInt(charOffsetAttrMatch[1]);
|
|
491
|
+
charShape.charOffset = {
|
|
492
|
+
hangul: offsetValue, latin: offsetValue, hanja: offsetValue,
|
|
493
|
+
japanese: offsetValue, other: offsetValue, symbol: offsetValue, user: offsetValue
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Parse ratio element - extract each attribute separately (order-independent)
|
|
498
|
+
const ratioElemMatch = shapeContent.match(/<(?:hh:)?ratio([^>]*)\/?>/i);
|
|
499
|
+
if (ratioElemMatch) {
|
|
500
|
+
const ratioAttrs = ratioElemMatch[1];
|
|
501
|
+
const hangulMatch = ratioAttrs.match(/hangul="(\d+)"/);
|
|
502
|
+
const latinMatch = ratioAttrs.match(/latin="(\d+)"/);
|
|
503
|
+
const hanjaMatch = ratioAttrs.match(/hanja="(\d+)"/);
|
|
504
|
+
const japaneseMatch = ratioAttrs.match(/japanese="(\d+)"/);
|
|
505
|
+
const otherMatch = ratioAttrs.match(/other="(\d+)"/);
|
|
506
|
+
const symbolMatch = ratioAttrs.match(/symbol="(\d+)"/);
|
|
507
|
+
const userMatch = ratioAttrs.match(/user="(\d+)"/);
|
|
508
|
+
charShape.ratio = {
|
|
509
|
+
hangul: hangulMatch ? parseInt(hangulMatch[1]) : 100,
|
|
510
|
+
latin: latinMatch ? parseInt(latinMatch[1]) : 100,
|
|
511
|
+
hanja: hanjaMatch ? parseInt(hanjaMatch[1]) : 100,
|
|
512
|
+
japanese: japaneseMatch ? parseInt(japaneseMatch[1]) : 100,
|
|
513
|
+
other: otherMatch ? parseInt(otherMatch[1]) : 100,
|
|
514
|
+
symbol: symbolMatch ? parseInt(symbolMatch[1]) : 100,
|
|
515
|
+
user: userMatch ? parseInt(userMatch[1]) : 100
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const symMarkMatch = shapeContent.match(/<(?:hh:)?symMark[^>]*symMarkType="([^"]*)"/i);
|
|
519
|
+
if (symMarkMatch && symMarkMatch[1] !== 'NONE') {
|
|
520
|
+
const symMarkMap = {
|
|
521
|
+
'DOT': 'Dot', 'CIRCLE': 'Circle', 'RING': 'Ring', 'CARON': 'Caron',
|
|
522
|
+
'UNDER_DOT': 'UnderDot', 'UNDER_LINE': 'UnderLine', 'TRIANGLE': 'Triangle', 'NONE': 'None'
|
|
523
|
+
};
|
|
524
|
+
charShape.symMark = symMarkMap[symMarkMatch[1].toUpperCase()] || 'None';
|
|
525
|
+
}
|
|
526
|
+
const useFontSpaceMatch = shapeContent.match(/useFontSpace="([^"]*)"/);
|
|
527
|
+
if (useFontSpaceMatch) {
|
|
528
|
+
charShape.useFontSpace = useFontSpaceMatch[1] === '1' || useFontSpaceMatch[1] === 'true';
|
|
529
|
+
}
|
|
530
|
+
const useKerningMatch = shapeContent.match(/useKerning="([^"]*)"/);
|
|
531
|
+
if (useKerningMatch) {
|
|
532
|
+
charShape.useKerning = useKerningMatch[1] === '1' || useKerningMatch[1] === 'true';
|
|
533
|
+
}
|
|
534
|
+
const outlineMatch = shapeContent.match(/<(?:hh:)?outline[^>]*type="([^"]*)"/i);
|
|
535
|
+
if (outlineMatch && outlineMatch[1] !== 'NONE') {
|
|
536
|
+
const outlineMap = {
|
|
537
|
+
'SOLID': 'Solid', 'DOT': 'Dot', 'DASH': 'Dash', 'DASH_DOT': 'DashDot',
|
|
538
|
+
'DASH_DOT_DOT': 'DashDotDot', 'THICK': 'Thick'
|
|
539
|
+
};
|
|
540
|
+
charShape.outline = {
|
|
541
|
+
type: outlineMap[outlineMatch[1].toUpperCase()] || 'Solid'
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const shadowMatch = shapeContent.match(/<(?:hh:)?shadow[^>]*type="([^"]*)"[^>]*(?:offsetX="(-?\d+)")?[^>]*(?:offsetY="(-?\d+)")?[^>]*(?:color="([^"]*)")?/i);
|
|
545
|
+
if (shadowMatch && shadowMatch[1] !== 'NONE') {
|
|
546
|
+
const shadowMap = {
|
|
547
|
+
'DROP': 'Drop', 'CONTINUOUS': 'Cont', 'NONE': 'None'
|
|
548
|
+
};
|
|
549
|
+
charShape.shadow = {
|
|
550
|
+
type: shadowMap[shadowMatch[1].toUpperCase()] || 'None',
|
|
551
|
+
offsetX: shadowMatch[2] ? parseInt(shadowMatch[2]) / 100 : undefined,
|
|
552
|
+
offsetY: shadowMatch[3] ? parseInt(shadowMatch[3]) / 100 : undefined,
|
|
553
|
+
color: shadowMatch[4] || undefined
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const embossMatch = shapeContent.match(/<(?:hh:)?emboss\s*\/>/i);
|
|
557
|
+
charShape.emboss = !!embossMatch;
|
|
558
|
+
const engraveMatch = shapeContent.match(/<(?:hh:)?engrave\s*\/>/i);
|
|
559
|
+
charShape.engrave = !!engraveMatch;
|
|
560
|
+
const borderFillIdMatch = shapeContent.match(/borderFillIDRef="(\d+)"/);
|
|
561
|
+
if (borderFillIdMatch) {
|
|
562
|
+
charShape.borderFillId = parseInt(borderFillIdMatch[1]);
|
|
563
|
+
}
|
|
564
|
+
this.styles.charShapes.set(charShape.id, charShape);
|
|
565
|
+
shapeId++;
|
|
566
|
+
};
|
|
567
|
+
while ((match = charShapeRegex.exec(xml)) !== null) {
|
|
568
|
+
// match[1] = tagName (charShape or charPr), match[0] = full match
|
|
569
|
+
const tagName = match[1].toLowerCase();
|
|
570
|
+
parseCharShape(match[0], tagName);
|
|
571
|
+
}
|
|
572
|
+
while ((match = charShapeRegexSelfClosing.exec(xml)) !== null) {
|
|
573
|
+
// match[1] = tagName (charShape or charPr), match[0] = full match
|
|
574
|
+
const tagName = match[1].toLowerCase();
|
|
575
|
+
parseCharShape(match[0], tagName);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
static parseParaShapes(xml) {
|
|
579
|
+
const paraShapeRegex = /<(?:hh:)?(?:paraShape|paraPr)[^>]*>([\s\S]*?)<\/(?:hh:)?(?:paraShape|paraPr)>/gi;
|
|
580
|
+
let match;
|
|
581
|
+
let shapeId = 0;
|
|
582
|
+
while ((match = paraShapeRegex.exec(xml)) !== null) {
|
|
583
|
+
const shapeContent = match[0];
|
|
584
|
+
const paraShape = { id: shapeId };
|
|
585
|
+
const idMatch = shapeContent.match(/\bid="(\d+)"/);
|
|
586
|
+
if (idMatch) {
|
|
587
|
+
shapeId = parseInt(idMatch[1]);
|
|
588
|
+
paraShape.id = shapeId;
|
|
589
|
+
}
|
|
590
|
+
const alignMatch = shapeContent.match(/<(?:hh:)?align[^>]*horizontal="([^"]*)"/i);
|
|
591
|
+
if (alignMatch) {
|
|
592
|
+
const alignVal = alignMatch[1].toUpperCase();
|
|
593
|
+
if (alignVal === 'JUSTIFY')
|
|
594
|
+
paraShape.align = 'Justify';
|
|
595
|
+
else if (alignVal === 'CENTER')
|
|
596
|
+
paraShape.align = 'Center';
|
|
597
|
+
else if (alignVal === 'RIGHT')
|
|
598
|
+
paraShape.align = 'Right';
|
|
599
|
+
else if (alignVal === 'DISTRIBUTE')
|
|
600
|
+
paraShape.align = 'Distribute';
|
|
601
|
+
else if (alignVal === 'DISTRIBUTE_SPACE')
|
|
602
|
+
paraShape.align = 'DistributeSpace';
|
|
603
|
+
else
|
|
604
|
+
paraShape.align = 'Left';
|
|
605
|
+
}
|
|
606
|
+
let lineSpaceMatch = shapeContent.match(/<(?:hh:)?lineSpacing[^>]*type="([^"]*)"[^>]*value="(\d+)"/i);
|
|
607
|
+
if (!lineSpaceMatch) {
|
|
608
|
+
lineSpaceMatch = shapeContent.match(/<(?:hh:)?lineSpacing[^>]*value="(\d+)"[^>]*type="([^"]*)"/i);
|
|
609
|
+
if (lineSpaceMatch) {
|
|
610
|
+
lineSpaceMatch = [lineSpaceMatch[0], lineSpaceMatch[2], lineSpaceMatch[1]];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (lineSpaceMatch) {
|
|
614
|
+
paraShape.lineSpacing = parseInt(lineSpaceMatch[2]);
|
|
615
|
+
const typeMap = {
|
|
616
|
+
'PERCENT': 'percent', 'FIXED': 'fixed', 'BETWEEN_LINES': 'betweenLines', 'AT_LEAST': 'atLeast'
|
|
617
|
+
};
|
|
618
|
+
paraShape.lineSpacingType = typeMap[lineSpaceMatch[1]?.toUpperCase()] || 'percent';
|
|
619
|
+
}
|
|
620
|
+
const caseMatch = shapeContent.match(/<hp:case[^>]*>([\s\S]*?)<\/hp:case>/i);
|
|
621
|
+
const marginSource = caseMatch ? caseMatch[1] : shapeContent;
|
|
622
|
+
const leftMatch = marginSource.match(/<(?:hc:)?left[^>]*value="(-?\d+)"/i);
|
|
623
|
+
if (leftMatch) {
|
|
624
|
+
paraShape.marginLeft = parseInt(leftMatch[1]) / 100;
|
|
625
|
+
}
|
|
626
|
+
const rightMatch = marginSource.match(/<(?:hc:)?right[^>]*value="(-?\d+)"/i);
|
|
627
|
+
if (rightMatch) {
|
|
628
|
+
paraShape.marginRight = parseInt(rightMatch[1]) / 100;
|
|
629
|
+
}
|
|
630
|
+
const prevMatch = marginSource.match(/<(?:hc:)?prev[^>]*value="(-?\d+)"/i);
|
|
631
|
+
if (prevMatch) {
|
|
632
|
+
paraShape.marginTop = parseInt(prevMatch[1]) / 100;
|
|
633
|
+
}
|
|
634
|
+
const nextMatch = marginSource.match(/<(?:hc:)?next[^>]*value="(-?\d+)"/i);
|
|
635
|
+
if (nextMatch) {
|
|
636
|
+
paraShape.marginBottom = parseInt(nextMatch[1]) / 100;
|
|
637
|
+
}
|
|
638
|
+
const intentMatch = marginSource.match(/<(?:hc:)?intent[^>]*value="(-?\d+)"/i);
|
|
639
|
+
if (intentMatch) {
|
|
640
|
+
paraShape.firstLineIndent = parseInt(intentMatch[1]) / 100;
|
|
641
|
+
}
|
|
642
|
+
const tabDefMatch = shapeContent.match(/tabPrIDRef="(\d+)"/);
|
|
643
|
+
if (tabDefMatch) {
|
|
644
|
+
paraShape.tabDefId = parseInt(tabDefMatch[1]);
|
|
645
|
+
}
|
|
646
|
+
const condenseMatch = shapeContent.match(/condense="(-?\d+)"/);
|
|
647
|
+
if (condenseMatch) {
|
|
648
|
+
paraShape.condense = parseInt(condenseMatch[1]);
|
|
649
|
+
}
|
|
650
|
+
const breakLatinMatch = shapeContent.match(/breakLatinWord="([^"]*)"/);
|
|
651
|
+
if (breakLatinMatch) {
|
|
652
|
+
const breakMap = {
|
|
653
|
+
'KEEP_WORD': 'normal', 'HYPHENATION': 'hyphenation', 'BREAK_WORD': 'breakWord'
|
|
654
|
+
};
|
|
655
|
+
paraShape.breakLatinWord = breakMap[breakLatinMatch[1].toUpperCase()] || 'normal';
|
|
656
|
+
}
|
|
657
|
+
const breakNonLatinMatch = shapeContent.match(/breakNonLatinWord="([^"]*)"/);
|
|
658
|
+
if (breakNonLatinMatch) {
|
|
659
|
+
paraShape.breakNonLatinWord = breakNonLatinMatch[1] === '1' || breakNonLatinMatch[1] === 'true';
|
|
660
|
+
}
|
|
661
|
+
const snapToGridMatch = shapeContent.match(/snapToGrid="([^"]*)"/);
|
|
662
|
+
if (snapToGridMatch) {
|
|
663
|
+
paraShape.snapToGrid = snapToGridMatch[1] === '1' || snapToGridMatch[1] === 'true';
|
|
664
|
+
}
|
|
665
|
+
const suppressLineNumMatch = shapeContent.match(/suppressLineNumbers="([^"]*)"/);
|
|
666
|
+
if (suppressLineNumMatch) {
|
|
667
|
+
paraShape.suppressLineNumbers = suppressLineNumMatch[1] === '1' || suppressLineNumMatch[1] === 'true';
|
|
668
|
+
}
|
|
669
|
+
const headingMatch = shapeContent.match(/<(?:hh:)?heading[^>]*type="([^"]*)"[^>]*(?:level="(\d+)")?/i);
|
|
670
|
+
if (headingMatch) {
|
|
671
|
+
const headingMap = {
|
|
672
|
+
'NONE': 'none', 'OUTLINE': 'outline', 'NUMBER': 'number', 'BULLET': 'bullet'
|
|
673
|
+
};
|
|
674
|
+
paraShape.headingType = headingMap[headingMatch[1]?.toUpperCase()] || 'none';
|
|
675
|
+
if (headingMatch[2]) {
|
|
676
|
+
paraShape.headingLevel = parseInt(headingMatch[2]);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const borderFillMatch = shapeContent.match(/borderFillIDRef="(\d+)"/);
|
|
680
|
+
if (borderFillMatch) {
|
|
681
|
+
paraShape.borderFillId = parseInt(borderFillMatch[1]);
|
|
682
|
+
}
|
|
683
|
+
const autoSpaceEAEngMatch = shapeContent.match(/autoSpaceEAsianEng="([^"]*)"/);
|
|
684
|
+
if (autoSpaceEAEngMatch) {
|
|
685
|
+
paraShape.autoSpaceEAsianEng = autoSpaceEAEngMatch[1] === '1' || autoSpaceEAEngMatch[1] === 'true';
|
|
686
|
+
}
|
|
687
|
+
const autoSpaceEANumMatch = shapeContent.match(/autoSpaceEAsianNum="([^"]*)"/);
|
|
688
|
+
if (autoSpaceEANumMatch) {
|
|
689
|
+
paraShape.autoSpaceEAsianNum = autoSpaceEANumMatch[1] === '1' || autoSpaceEANumMatch[1] === 'true';
|
|
690
|
+
}
|
|
691
|
+
const keepWithNextMatch = shapeContent.match(/keepWithNext="([^"]*)"/);
|
|
692
|
+
if (keepWithNextMatch) {
|
|
693
|
+
paraShape.keepWithNext = keepWithNextMatch[1] === '1' || keepWithNextMatch[1] === 'true';
|
|
694
|
+
}
|
|
695
|
+
const keepLinesMatch = shapeContent.match(/keepLines="([^"]*)"/);
|
|
696
|
+
if (keepLinesMatch) {
|
|
697
|
+
paraShape.keepLines = keepLinesMatch[1] === '1' || keepLinesMatch[1] === 'true';
|
|
698
|
+
}
|
|
699
|
+
const pageBreakBeforeMatch = shapeContent.match(/pageBreakBefore="([^"]*)"/);
|
|
700
|
+
if (pageBreakBeforeMatch) {
|
|
701
|
+
paraShape.pageBreakBefore = pageBreakBeforeMatch[1] === '1' || pageBreakBeforeMatch[1] === 'true';
|
|
702
|
+
}
|
|
703
|
+
const widowControlMatch = shapeContent.match(/widowOrphan="([^"]*)"/);
|
|
704
|
+
if (widowControlMatch) {
|
|
705
|
+
paraShape.widowControl = widowControlMatch[1] === '1' || widowControlMatch[1] === 'true';
|
|
706
|
+
}
|
|
707
|
+
this.styles.paraShapes.set(paraShape.id, paraShape);
|
|
708
|
+
shapeId++;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
static parseBorderFills(xml) {
|
|
712
|
+
const borderFillRegex = /<hh:borderFill\s+id="(\d+)"[^>]*>([\s\S]*?)<\/hh:borderFill>/gi;
|
|
713
|
+
let match;
|
|
714
|
+
while ((match = borderFillRegex.exec(xml)) !== null) {
|
|
715
|
+
const id = parseInt(match[1]);
|
|
716
|
+
const content = match[0];
|
|
717
|
+
const tagAttrs = match[0].match(/<hh:borderFill[^>]*>/)?.[0] || '';
|
|
718
|
+
const borderFill = { id };
|
|
719
|
+
const parseBorder = (name) => {
|
|
720
|
+
const regex = new RegExp(`<hh:${name}Border[^>]*type="([^"]*)"[^>]*width="([^"]*)"[^>]*color="([^"]*)"`, 'i');
|
|
721
|
+
const borderMatch = content.match(regex);
|
|
722
|
+
if (borderMatch) {
|
|
723
|
+
const typeMap = {
|
|
724
|
+
'NONE': 'none', 'SOLID': 'solid', 'DASHED': 'dashed', 'DASH': 'dashed', 'DOTTED': 'dotted', 'DOUBLE': 'double'
|
|
725
|
+
};
|
|
726
|
+
const widthStr = borderMatch[2];
|
|
727
|
+
let widthPt = 0.5;
|
|
728
|
+
const widthNumMatch = widthStr.match(/([\d.]+)\s*(mm|pt|cm)?/);
|
|
729
|
+
if (widthNumMatch) {
|
|
730
|
+
const num = parseFloat(widthNumMatch[1]);
|
|
731
|
+
const unit = widthNumMatch[2]?.toLowerCase() || 'mm';
|
|
732
|
+
if (unit === 'mm')
|
|
733
|
+
widthPt = num * 2.83465;
|
|
734
|
+
else if (unit === 'cm')
|
|
735
|
+
widthPt = num * 28.3465;
|
|
736
|
+
else
|
|
737
|
+
widthPt = num;
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
style: typeMap[borderMatch[1].toUpperCase()] || 'solid',
|
|
741
|
+
width: widthPt,
|
|
742
|
+
color: borderMatch[3]
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
return undefined;
|
|
746
|
+
};
|
|
747
|
+
borderFill.leftBorder = parseBorder('left');
|
|
748
|
+
borderFill.rightBorder = parseBorder('right');
|
|
749
|
+
borderFill.topBorder = parseBorder('top');
|
|
750
|
+
borderFill.bottomBorder = parseBorder('bottom');
|
|
751
|
+
borderFill.diagonalBorder = parseBorder('diagonal');
|
|
752
|
+
borderFill.antiDiagonalBorder = parseBorder('antiDiagonal');
|
|
753
|
+
const threeDMatch = tagAttrs.match(/threeD="([^"]*)"/);
|
|
754
|
+
if (threeDMatch) {
|
|
755
|
+
borderFill.threeD = threeDMatch[1] === '1' || threeDMatch[1] === 'true';
|
|
756
|
+
}
|
|
757
|
+
const shadowMatch = tagAttrs.match(/shadow="([^"]*)"/);
|
|
758
|
+
if (shadowMatch) {
|
|
759
|
+
borderFill.shadow = shadowMatch[1] === '1' || shadowMatch[1] === 'true';
|
|
760
|
+
}
|
|
761
|
+
const centerLineMatch = tagAttrs.match(/centerLine="([^"]*)"/);
|
|
762
|
+
if (centerLineMatch) {
|
|
763
|
+
borderFill.centerLine = centerLineMatch[1] === '1' || centerLineMatch[1] === 'true';
|
|
764
|
+
}
|
|
765
|
+
// Support both hh: and hc: namespace prefixes for fillBrush
|
|
766
|
+
const fillBrushMatch = content.match(/<(?:hh|hc):fillBrush[^>]*>([\s\S]*?)<\/(?:hh|hc):fillBrush>/i);
|
|
767
|
+
if (fillBrushMatch) {
|
|
768
|
+
const fillContent = fillBrushMatch[1];
|
|
769
|
+
// Support both hh: and hc: namespace for winBrush
|
|
770
|
+
const windowColorMatch = fillContent.match(/<(?:hh|hc):winBrush[^>]*faceColor="([^"]*)"/i);
|
|
771
|
+
if (windowColorMatch && windowColorMatch[1] !== 'none') {
|
|
772
|
+
borderFill.fillColor = windowColorMatch[1];
|
|
773
|
+
borderFill.fillType = 'color';
|
|
774
|
+
}
|
|
775
|
+
const gradationMatch = fillContent.match(/<(?:hh|hc):gradation[^>]*type="([^"]*)"[^>]*(?:angle="([^"]*)")?[^>]*(?:centerX="([^"]*)")?[^>]*(?:centerY="([^"]*)")?[^>]*(?:step="([^"]*)")?[^>]*>([\s\S]*?)<\/(?:hh|hc):gradation>/i);
|
|
776
|
+
if (gradationMatch) {
|
|
777
|
+
const typeMap = {
|
|
778
|
+
'LINEAR': 'linear', 'RADIAL': 'radial', 'CONICAL': 'conical', 'SQUARE': 'square'
|
|
779
|
+
};
|
|
780
|
+
borderFill.fillType = 'gradation';
|
|
781
|
+
borderFill.gradation = {
|
|
782
|
+
type: typeMap[gradationMatch[1]?.toUpperCase()] || 'linear',
|
|
783
|
+
colors: []
|
|
784
|
+
};
|
|
785
|
+
if (gradationMatch[2])
|
|
786
|
+
borderFill.gradation.angle = parseInt(gradationMatch[2]);
|
|
787
|
+
if (gradationMatch[3])
|
|
788
|
+
borderFill.gradation.centerX = parseInt(gradationMatch[3]);
|
|
789
|
+
if (gradationMatch[4])
|
|
790
|
+
borderFill.gradation.centerY = parseInt(gradationMatch[4]);
|
|
791
|
+
if (gradationMatch[5])
|
|
792
|
+
borderFill.gradation.step = parseInt(gradationMatch[5]);
|
|
793
|
+
const colorRegex = /<(?:hh|hc):color[^>]*value="([^"]*)"/gi;
|
|
794
|
+
let colorMatch;
|
|
795
|
+
while ((colorMatch = colorRegex.exec(gradationMatch[6])) !== null) {
|
|
796
|
+
borderFill.gradation.colors.push(colorMatch[1]);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const imgBrushMatch = fillContent.match(/<(?:hh|hc):imgBrush[^>]*mode="([^"]*)"[^>]*(?:alpha="([^"]*)")?[^>]*(?:binaryItemIDRef="([^"]*)")?/i);
|
|
800
|
+
if (imgBrushMatch) {
|
|
801
|
+
const modeMap = {
|
|
802
|
+
'TILE': 'tile', 'TILE_HORZ': 'tileHorz', 'TILE_VERT': 'tileVert',
|
|
803
|
+
'TOTAL_FIT': 'totalFit', 'FIT': 'fit', 'CENTER': 'center',
|
|
804
|
+
'ONCE_ABSOLUTE_SCALE': 'onceAbsoluteScale'
|
|
805
|
+
};
|
|
806
|
+
borderFill.fillType = 'image';
|
|
807
|
+
borderFill.imageFill = {
|
|
808
|
+
mode: modeMap[imgBrushMatch[1]?.toUpperCase()] || 'tile'
|
|
809
|
+
};
|
|
810
|
+
if (imgBrushMatch[2]) {
|
|
811
|
+
borderFill.imageFill.alpha = parseInt(imgBrushMatch[2]) / 255;
|
|
812
|
+
}
|
|
813
|
+
if (imgBrushMatch[3]) {
|
|
814
|
+
borderFill.imageFill.binaryItemId = imgBrushMatch[3];
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (!borderFill.fillColor) {
|
|
819
|
+
const fillMatch = content.match(/faceColor="([^"]*)"/);
|
|
820
|
+
if (fillMatch && fillMatch[1] !== 'none') {
|
|
821
|
+
borderFill.fillColor = fillMatch[1];
|
|
822
|
+
borderFill.fillType = 'color';
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
this.styles.borderFills.set(id, borderFill);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
static parseTabDefs(xml) {
|
|
829
|
+
const tabPrRegex = /<hh:tabPr\s+id="(\d+)"[^>]*>([\s\S]*?)<\/hh:tabPr>|<hh:tabPr\s+id="(\d+)"[^>]*\/>/gi;
|
|
830
|
+
let match;
|
|
831
|
+
while ((match = tabPrRegex.exec(xml)) !== null) {
|
|
832
|
+
const id = parseInt(match[1] || match[3]);
|
|
833
|
+
const content = match[0];
|
|
834
|
+
const tabDef = { id, items: [] };
|
|
835
|
+
const autoLeftMatch = content.match(/autoTabLeft="([^"]*)"/);
|
|
836
|
+
if (autoLeftMatch) {
|
|
837
|
+
tabDef.autoTabLeft = autoLeftMatch[1] === '1' || autoLeftMatch[1] === 'true';
|
|
838
|
+
}
|
|
839
|
+
const autoRightMatch = content.match(/autoTabRight="([^"]*)"/);
|
|
840
|
+
if (autoRightMatch) {
|
|
841
|
+
tabDef.autoTabRight = autoRightMatch[1] === '1' || autoRightMatch[1] === 'true';
|
|
842
|
+
}
|
|
843
|
+
const tabItemRegex = /<hh:tabItem[^>]*pos="(\d+)"[^>]*type="([^"]*)"[^>]*leader="([^"]*)"/gi;
|
|
844
|
+
let itemMatch;
|
|
845
|
+
while ((itemMatch = tabItemRegex.exec(content)) !== null) {
|
|
846
|
+
const typeMap = {
|
|
847
|
+
'LEFT': 'left', 'RIGHT': 'right', 'CENTER': 'center', 'DECIMAL': 'decimal'
|
|
848
|
+
};
|
|
849
|
+
const leaderMap = {
|
|
850
|
+
'NONE': 'none', 'SOLID': 'solid', 'DASH': 'dash', 'DOT': 'dot',
|
|
851
|
+
'DASH_DOT': 'dashDot', 'DASH_DOT_DOT': 'dashDotDot'
|
|
852
|
+
};
|
|
853
|
+
tabDef.items.push({
|
|
854
|
+
pos: parseInt(itemMatch[1]) / 100,
|
|
855
|
+
type: typeMap[itemMatch[2].toUpperCase()] || 'left',
|
|
856
|
+
leader: leaderMap[itemMatch[3].toUpperCase()] || 'none'
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
this.styles.tabDefs.set(id, tabDef);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
static parseNumberings(xml) {
|
|
863
|
+
const numberingRegex = /<hh:numbering\s+id="(\d+)"[^>]*>([\s\S]*?)<\/hh:numbering>/gi;
|
|
864
|
+
let match;
|
|
865
|
+
while ((match = numberingRegex.exec(xml)) !== null) {
|
|
866
|
+
const id = parseInt(match[1]);
|
|
867
|
+
const content = match[0];
|
|
868
|
+
const numberingDef = { id, paraHeads: [] };
|
|
869
|
+
const startMatch = content.match(/\bstart="(\d+)"/);
|
|
870
|
+
if (startMatch) {
|
|
871
|
+
numberingDef.start = parseInt(startMatch[1]);
|
|
872
|
+
}
|
|
873
|
+
const paraHeadRegex = /<hh:paraHead[^>]*level="(\d+)"[^>]*numFormat="([^"]*)"[^>]*>([^<]*)<\/hh:paraHead>|<hh:paraHead[^>]*level="(\d+)"[^>]*numFormat="([^"]*)"[^>]*\/>/gi;
|
|
874
|
+
let headMatch;
|
|
875
|
+
while ((headMatch = paraHeadRegex.exec(content)) !== null) {
|
|
876
|
+
const level = parseInt(headMatch[1] || headMatch[4]);
|
|
877
|
+
const numFormatStr = headMatch[2] || headMatch[5];
|
|
878
|
+
const text = headMatch[3] || '';
|
|
879
|
+
const formatMap = {
|
|
880
|
+
'DIGIT': 'digit', 'ROMAN_CAPITAL': 'romanCapital', 'ROMAN_SMALL': 'romanSmall',
|
|
881
|
+
'LATIN_CAPITAL': 'latinCapital', 'LATIN_SMALL': 'latinSmall',
|
|
882
|
+
'HANGUL_SYLLABLE': 'hangulSyllable', 'HANGUL_JAMO': 'hangulJamo',
|
|
883
|
+
'CIRCLED_DIGIT': 'circledDigit'
|
|
884
|
+
};
|
|
885
|
+
numberingDef.paraHeads.push({
|
|
886
|
+
level,
|
|
887
|
+
numFormat: formatMap[numFormatStr?.toUpperCase()] || 'digit',
|
|
888
|
+
text: text || undefined
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
this.styles.numberings.set(id, numberingDef);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
static parseBullets(xml) {
|
|
895
|
+
const bulletRegex = /<hh:bullet\s+id="(\d+)"[^>]*>([\s\S]*?)<\/hh:bullet>|<hh:bullet\s+id="(\d+)"[^>]*\/>/gi;
|
|
896
|
+
let match;
|
|
897
|
+
while ((match = bulletRegex.exec(xml)) !== null) {
|
|
898
|
+
const id = parseInt(match[1] || match[3]);
|
|
899
|
+
const content = match[0];
|
|
900
|
+
const bulletDef = { id };
|
|
901
|
+
const charMatch = content.match(/\bchar="([^"]*)"/);
|
|
902
|
+
if (charMatch) {
|
|
903
|
+
bulletDef.char = charMatch[1];
|
|
904
|
+
}
|
|
905
|
+
const useImageMatch = content.match(/useImage="([^"]*)"/);
|
|
906
|
+
if (useImageMatch) {
|
|
907
|
+
bulletDef.useImage = useImageMatch[1] === '1' || useImageMatch[1] === 'true';
|
|
908
|
+
}
|
|
909
|
+
this.styles.bullets.set(id, bulletDef);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
static parseStyleDefs(xml) {
|
|
913
|
+
const styleRegex = /<hh:style\s+[^>]*id="(\d+)"[^>]*>([\s\S]*?)<\/hh:style>|<hh:style\s+[^>]*id="(\d+)"[^>]*\/>/gi;
|
|
914
|
+
let match;
|
|
915
|
+
while ((match = styleRegex.exec(xml)) !== null) {
|
|
916
|
+
const id = parseInt(match[1] || match[3]);
|
|
917
|
+
const content = match[0];
|
|
918
|
+
const styleDef = { id };
|
|
919
|
+
const typeMatch = content.match(/\btype="([^"]*)"/);
|
|
920
|
+
if (typeMatch) {
|
|
921
|
+
styleDef.type = typeMatch[1].toLowerCase() === 'char' ? 'char' : 'para';
|
|
922
|
+
}
|
|
923
|
+
const nameMatch = content.match(/\bname="([^"]*)"/);
|
|
924
|
+
if (nameMatch) {
|
|
925
|
+
styleDef.name = nameMatch[1];
|
|
926
|
+
}
|
|
927
|
+
const engNameMatch = content.match(/engName="([^"]*)"/);
|
|
928
|
+
if (engNameMatch) {
|
|
929
|
+
styleDef.engName = engNameMatch[1];
|
|
930
|
+
}
|
|
931
|
+
const paraPrMatch = content.match(/paraPrIDRef="(\d+)"/);
|
|
932
|
+
if (paraPrMatch) {
|
|
933
|
+
styleDef.paraPrIdRef = parseInt(paraPrMatch[1]);
|
|
934
|
+
}
|
|
935
|
+
const charPrMatch = content.match(/charPrIDRef="(\d+)"/);
|
|
936
|
+
if (charPrMatch) {
|
|
937
|
+
styleDef.charPrIdRef = parseInt(charPrMatch[1]);
|
|
938
|
+
}
|
|
939
|
+
const nextStyleMatch = content.match(/nextStyleIDRef="(\d+)"/);
|
|
940
|
+
if (nextStyleMatch) {
|
|
941
|
+
styleDef.nextStyleIdRef = parseInt(nextStyleMatch[1]);
|
|
942
|
+
}
|
|
943
|
+
this.styles.styles.set(id, styleDef);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
static async parseImages(zip, content) {
|
|
947
|
+
const binDataFolder = zip.folder('BinData');
|
|
948
|
+
if (!binDataFolder)
|
|
949
|
+
return;
|
|
950
|
+
const imageFiles = Object.keys(zip.files).filter((f) => f.startsWith('BinData/') && !f.endsWith('/'));
|
|
951
|
+
for (const imagePath of imageFiles) {
|
|
952
|
+
const file = zip.file(imagePath);
|
|
953
|
+
if (!file)
|
|
954
|
+
continue;
|
|
955
|
+
const data = await file.async('base64');
|
|
956
|
+
const fileName = imagePath.split('/').pop() || '';
|
|
957
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
958
|
+
let mimeType = 'image/png';
|
|
959
|
+
if (ext === 'jpg' || ext === 'jpeg')
|
|
960
|
+
mimeType = 'image/jpeg';
|
|
961
|
+
else if (ext === 'gif')
|
|
962
|
+
mimeType = 'image/gif';
|
|
963
|
+
else if (ext === 'bmp')
|
|
964
|
+
mimeType = 'image/bmp';
|
|
965
|
+
else if (ext === 'svg')
|
|
966
|
+
mimeType = 'image/svg+xml';
|
|
967
|
+
const imageId = fileName.replace(/\.[^.]+$/, '');
|
|
968
|
+
content.images.set(imageId, {
|
|
969
|
+
id: imageId,
|
|
970
|
+
binaryId: imagePath,
|
|
971
|
+
width: 0,
|
|
972
|
+
height: 0,
|
|
973
|
+
data: `data:${mimeType};base64,${data}`,
|
|
974
|
+
mimeType,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
static parseSection(xml, content, sectionIndex) {
|
|
979
|
+
const section = {
|
|
980
|
+
elements: [],
|
|
981
|
+
pageSettings: this.parsePageSettings(xml),
|
|
982
|
+
sectionProperties: this.parseSectionProperties(xml),
|
|
983
|
+
memos: [], // Store memo data for sidebar display
|
|
984
|
+
};
|
|
985
|
+
// Extract MEMO data before removing from XML
|
|
986
|
+
// Pattern: fieldBegin...content.../fieldBegin followed by linked text then fieldEnd
|
|
987
|
+
const memoFullRegex = /<hp:fieldBegin[^>]*id="([^"]*)"[^>]*type="MEMO"[^>]*>([\s\S]*?)<\/hp:fieldBegin>([\s\S]*?)<hp:ctrl>\s*<hp:fieldEnd/gi;
|
|
988
|
+
let memoMatch;
|
|
989
|
+
while ((memoMatch = memoFullRegex.exec(xml)) !== null) {
|
|
990
|
+
const memoId = memoMatch[1];
|
|
991
|
+
const memoContent = memoMatch[2];
|
|
992
|
+
const linkedSection = memoMatch[3];
|
|
993
|
+
const memo = {
|
|
994
|
+
id: memoId,
|
|
995
|
+
author: memoContent.match(/<hp:stringParam[^>]*name="Author"[^>]*>([^<]*)<\/hp:stringParam>/i)?.[1] || 'Unknown',
|
|
996
|
+
date: memoContent.match(/<hp:stringParam[^>]*name="CreateDateTime"[^>]*>([^<]*)<\/hp:stringParam>/i)?.[1] || '',
|
|
997
|
+
content: [],
|
|
998
|
+
};
|
|
999
|
+
// Extract all text from subList paragraphs (memo content)
|
|
1000
|
+
const textMatches = memoContent.matchAll(/<hp:t[^>]*>([^<]*)<\/hp:t>/gi);
|
|
1001
|
+
for (const textMatch of textMatches) {
|
|
1002
|
+
if (textMatch[1])
|
|
1003
|
+
memo.content.push(textMatch[1]);
|
|
1004
|
+
}
|
|
1005
|
+
// Extract linked text (text between fieldBegin end and fieldEnd)
|
|
1006
|
+
const linkedTexts = linkedSection.match(/<hp:t[^>]*>([^<]*)<\/hp:t>/gi);
|
|
1007
|
+
if (linkedTexts) {
|
|
1008
|
+
const texts = linkedTexts.map(t => t.replace(/<[^>]+>/g, '')).filter(t => t);
|
|
1009
|
+
memo.linkedText = texts.join('');
|
|
1010
|
+
}
|
|
1011
|
+
section.memos.push(memo);
|
|
1012
|
+
}
|
|
1013
|
+
// Remove MEMO fieldBegin content to prevent memo text from appearing as document content
|
|
1014
|
+
// This removes the entire fieldBegin tag including subList with memo paragraphs
|
|
1015
|
+
let cleanedXml = xml.replace(/<hp:fieldBegin[^>]*type="MEMO"[^>]*>[\s\S]*?<\/hp:fieldBegin>/gi, '');
|
|
1016
|
+
// Extract footnote reference positions BEFORE removing footnote content
|
|
1017
|
+
// This allows us to add footnote markers to the correct paragraphs later
|
|
1018
|
+
const footnoteRefPositions = [];
|
|
1019
|
+
const fnPosRegex = /<hp:footNote\b[^>]*number="(\d+)"[^>]*>/gi;
|
|
1020
|
+
let fnPosMatch;
|
|
1021
|
+
while ((fnPosMatch = fnPosRegex.exec(cleanedXml)) !== null) {
|
|
1022
|
+
footnoteRefPositions.push({
|
|
1023
|
+
position: fnPosMatch.index,
|
|
1024
|
+
number: parseInt(fnPosMatch[1]),
|
|
1025
|
+
type: 'footnote'
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
const enPosRegex = /<hp:endNote\b[^>]*number="(\d+)"[^>]*>/gi;
|
|
1029
|
+
let enPosMatch;
|
|
1030
|
+
while ((enPosMatch = enPosRegex.exec(cleanedXml)) !== null) {
|
|
1031
|
+
footnoteRefPositions.push({
|
|
1032
|
+
position: enPosMatch.index,
|
|
1033
|
+
number: parseInt(enPosMatch[1]),
|
|
1034
|
+
type: 'endnote'
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
// Remove footnote/endnote content to prevent footnote text from appearing in document body
|
|
1038
|
+
// Only remove the content inside, but track where they were for reference markers
|
|
1039
|
+
// Use a more precise regex to only remove the footNote element and its content
|
|
1040
|
+
cleanedXml = cleanedXml.replace(/<hp:footNote\b[^>]*>[\s\S]*?<\/hp:footNote>/gi, '');
|
|
1041
|
+
cleanedXml = cleanedXml.replace(/<hp:endNote\b[^>]*>[\s\S]*?<\/hp:endNote>/gi, '');
|
|
1042
|
+
const elements = [];
|
|
1043
|
+
// Extract ALL paragraphs first to find parent paragraphs for tables
|
|
1044
|
+
const paragraphs = this.extractAllParagraphs(cleanedXml);
|
|
1045
|
+
// Also extract top-level paragraphs from ORIGINAL xml for position caching
|
|
1046
|
+
// This enables direct XML updates without re-parsing during save()
|
|
1047
|
+
const originalParagraphs = this.extractAllParagraphs(xml);
|
|
1048
|
+
const originalTables = this.extractBalancedTags(xml, 'hp:tbl');
|
|
1049
|
+
const originalTableRanges = [];
|
|
1050
|
+
for (const tableXml of originalTables) {
|
|
1051
|
+
const tableIndex = xml.indexOf(tableXml);
|
|
1052
|
+
if (tableIndex !== -1) {
|
|
1053
|
+
originalTableRanges.push({ start: tableIndex, end: tableIndex + tableXml.length });
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
// Build list of top-level paragraphs in original XML (not inside tables)
|
|
1057
|
+
const originalTopLevelParas = [];
|
|
1058
|
+
for (const para of originalParagraphs) {
|
|
1059
|
+
const isInsideTable = originalTableRanges.some(range => para.start > range.start && para.start < range.end);
|
|
1060
|
+
const containsTable = originalTableRanges.some(range => range.start >= para.start && range.end <= para.end);
|
|
1061
|
+
if (!isInsideTable) {
|
|
1062
|
+
if (containsTable) {
|
|
1063
|
+
// Check if paragraph has text content after removing table
|
|
1064
|
+
let paraXmlWithoutTable = para.xml;
|
|
1065
|
+
for (const range of originalTableRanges) {
|
|
1066
|
+
if (range.start >= para.start && range.end <= para.start + para.xml.length) {
|
|
1067
|
+
const tableStartInPara = range.start - para.start;
|
|
1068
|
+
const tableEndInPara = range.end - para.start;
|
|
1069
|
+
const tableXmlInPara = para.xml.substring(tableStartInPara, tableEndInPara);
|
|
1070
|
+
paraXmlWithoutTable = paraXmlWithoutTable.replace(tableXmlInPara, '');
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const hasTextContent = /<hp:t\b[^>]*>/.test(paraXmlWithoutTable);
|
|
1074
|
+
if (hasTextContent) {
|
|
1075
|
+
originalTopLevelParas.push({ start: para.start, end: para.end, xml: para.xml });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
originalTopLevelParas.push({ start: para.start, end: para.end, xml: para.xml });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Counter for matching cleaned paragraphs to original positions
|
|
1084
|
+
let originalParaIndex = 0;
|
|
1085
|
+
// Extract all tables from cleaned XML (without MEMOs and footnotes) to maintain consistent indices
|
|
1086
|
+
const tables = this.extractBalancedTags(cleanedXml, 'hp:tbl');
|
|
1087
|
+
const tableRanges = [];
|
|
1088
|
+
for (const tableXml of tables) {
|
|
1089
|
+
const tableIndex = cleanedXml.indexOf(tableXml);
|
|
1090
|
+
// Find parent paragraph that contains this table
|
|
1091
|
+
let parentLinesegs;
|
|
1092
|
+
for (const para of paragraphs) {
|
|
1093
|
+
if (tableIndex >= para.start && tableIndex < para.start + para.xml.length) {
|
|
1094
|
+
// This paragraph contains the table, extract its lineseg
|
|
1095
|
+
// The paragraph's own lineseg is at the END (after nested content like table cells)
|
|
1096
|
+
// So we use the LAST linesegarray in the paragraph
|
|
1097
|
+
const linesegArrayRegex = /<hp:linesegarray>([\s\S]*?)<\/hp:linesegarray>/g;
|
|
1098
|
+
let lastLinesegArray = null;
|
|
1099
|
+
let arrayMatch;
|
|
1100
|
+
while ((arrayMatch = linesegArrayRegex.exec(para.xml)) !== null) {
|
|
1101
|
+
lastLinesegArray = arrayMatch[1];
|
|
1102
|
+
}
|
|
1103
|
+
if (lastLinesegArray) {
|
|
1104
|
+
const linesegRegex = /<hp:lineseg[^>]*vertpos="(\d+)"[^>]*vertsize="(\d+)"[^>]*textheight="(\d+)"[^>]*baseline="(\d+)"[^>]*spacing="(\d+)"/g;
|
|
1105
|
+
let linesegMatch;
|
|
1106
|
+
const linesegs = [];
|
|
1107
|
+
while ((linesegMatch = linesegRegex.exec(lastLinesegArray)) !== null) {
|
|
1108
|
+
linesegs.push({
|
|
1109
|
+
vertpos: parseInt(linesegMatch[1]) / 100,
|
|
1110
|
+
vertsize: parseInt(linesegMatch[2]) / 100,
|
|
1111
|
+
textheight: parseInt(linesegMatch[3]) / 100,
|
|
1112
|
+
baseline: parseInt(linesegMatch[4]) / 100,
|
|
1113
|
+
spacing: parseInt(linesegMatch[5]) / 100,
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
if (linesegs.length > 0) {
|
|
1117
|
+
parentLinesegs = linesegs;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
elements.push({ index: tableIndex, type: 'tbl', xml: tableXml, parentLinesegs });
|
|
1124
|
+
tableRanges.push({ start: tableIndex, end: tableIndex + tableXml.length });
|
|
1125
|
+
}
|
|
1126
|
+
// Add paragraphs that are not inside tables
|
|
1127
|
+
// For paragraphs that contain tables, still parse the text content (excluding the table XML)
|
|
1128
|
+
for (const para of paragraphs) {
|
|
1129
|
+
const isInsideTable = tableRanges.some(range => para.start > range.start && para.start < range.end);
|
|
1130
|
+
const containsTable = tableRanges.some(range => range.start >= para.start && range.end <= para.end);
|
|
1131
|
+
if (!isInsideTable) {
|
|
1132
|
+
// Get corresponding original XML position for this top-level paragraph
|
|
1133
|
+
const origPos = originalParaIndex < originalTopLevelParas.length
|
|
1134
|
+
? { start: originalTopLevelParas[originalParaIndex].start, end: originalTopLevelParas[originalParaIndex].end }
|
|
1135
|
+
: undefined;
|
|
1136
|
+
if (containsTable) {
|
|
1137
|
+
// Paragraph contains a table - remove the table XML and parse the remaining content
|
|
1138
|
+
let paraXmlWithoutTable = para.xml;
|
|
1139
|
+
for (const range of tableRanges) {
|
|
1140
|
+
if (range.start >= para.start && range.end <= para.start + para.xml.length) {
|
|
1141
|
+
// Find and remove the table from paragraph XML
|
|
1142
|
+
const tableStartInPara = range.start - para.start;
|
|
1143
|
+
const tableEndInPara = range.end - para.start;
|
|
1144
|
+
const tableXmlInPara = para.xml.substring(tableStartInPara, tableEndInPara);
|
|
1145
|
+
paraXmlWithoutTable = paraXmlWithoutTable.replace(tableXmlInPara, '');
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
// Only add if there's remaining content besides lineseg
|
|
1149
|
+
const hasTextContent = /<hp:t\b[^>]*>/.test(paraXmlWithoutTable);
|
|
1150
|
+
if (hasTextContent) {
|
|
1151
|
+
elements.push({ index: para.start, type: 'p', xml: paraXmlWithoutTable, originalXmlPosition: origPos });
|
|
1152
|
+
originalParaIndex++;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
elements.push({ index: para.start, type: 'p', xml: para.xml, originalXmlPosition: origPos });
|
|
1157
|
+
originalParaIndex++;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const lineRegex = /<hp:line\b[^>]*(?:\/>|>[\s\S]*?<\/hp:line>)/g;
|
|
1162
|
+
let lineMatch;
|
|
1163
|
+
while ((lineMatch = lineRegex.exec(xml)) !== null) {
|
|
1164
|
+
elements.push({ index: lineMatch.index, type: 'line', xml: lineMatch[0] });
|
|
1165
|
+
}
|
|
1166
|
+
const rectRegex = /<hp:rect\b[^>]*(?:\/>|>[\s\S]*?<\/hp:rect>)/g;
|
|
1167
|
+
let rectMatch;
|
|
1168
|
+
while ((rectMatch = rectRegex.exec(xml)) !== null) {
|
|
1169
|
+
elements.push({ index: rectMatch.index, type: 'rect', xml: rectMatch[0] });
|
|
1170
|
+
}
|
|
1171
|
+
const ellipseRegex = /<hp:ellipse\b[^>]*(?:\/>|>[\s\S]*?<\/hp:ellipse>)/g;
|
|
1172
|
+
let ellipseMatch;
|
|
1173
|
+
while ((ellipseMatch = ellipseRegex.exec(xml)) !== null) {
|
|
1174
|
+
elements.push({ index: ellipseMatch.index, type: 'ellipse', xml: ellipseMatch[0] });
|
|
1175
|
+
}
|
|
1176
|
+
// Arc (호)
|
|
1177
|
+
const arcRegex = /<hp:arc\b[^>]*(?:\/>|>[\s\S]*?<\/hp:arc>)/g;
|
|
1178
|
+
let arcMatch;
|
|
1179
|
+
while ((arcMatch = arcRegex.exec(xml)) !== null) {
|
|
1180
|
+
elements.push({ index: arcMatch.index, type: 'arc', xml: arcMatch[0] });
|
|
1181
|
+
}
|
|
1182
|
+
// Polygon (다각형)
|
|
1183
|
+
const polygonRegex = /<hp:polygon\b[^>]*(?:\/>|>[\s\S]*?<\/hp:polygon>)/g;
|
|
1184
|
+
let polygonMatch;
|
|
1185
|
+
while ((polygonMatch = polygonRegex.exec(xml)) !== null) {
|
|
1186
|
+
elements.push({ index: polygonMatch.index, type: 'polygon', xml: polygonMatch[0] });
|
|
1187
|
+
}
|
|
1188
|
+
// Curve (곡선)
|
|
1189
|
+
const curveRegex = /<hp:curve\b[^>]*(?:\/>|>[\s\S]*?<\/hp:curve>)/g;
|
|
1190
|
+
let curveMatch;
|
|
1191
|
+
while ((curveMatch = curveRegex.exec(xml)) !== null) {
|
|
1192
|
+
elements.push({ index: curveMatch.index, type: 'curve', xml: curveMatch[0] });
|
|
1193
|
+
}
|
|
1194
|
+
// ConnectLine (연결선)
|
|
1195
|
+
const connectLineRegex = /<hp:connectLine\b[^>]*(?:\/>|>[\s\S]*?<\/hp:connectLine>)/g;
|
|
1196
|
+
let connectLineMatch;
|
|
1197
|
+
while ((connectLineMatch = connectLineRegex.exec(xml)) !== null) {
|
|
1198
|
+
elements.push({ index: connectLineMatch.index, type: 'connectline', xml: connectLineMatch[0] });
|
|
1199
|
+
}
|
|
1200
|
+
// Container (묶음객체)
|
|
1201
|
+
const containerRegex = /<hp:container\b[^>]*>[\s\S]*?<\/hp:container>/g;
|
|
1202
|
+
let containerMatch;
|
|
1203
|
+
while ((containerMatch = containerRegex.exec(xml)) !== null) {
|
|
1204
|
+
elements.push({ index: containerMatch.index, type: 'container', xml: containerMatch[0] });
|
|
1205
|
+
}
|
|
1206
|
+
// OLE
|
|
1207
|
+
const oleRegex = /<hp:ole\b[^>]*(?:\/>|>[\s\S]*?<\/hp:ole>)/g;
|
|
1208
|
+
let oleMatch;
|
|
1209
|
+
while ((oleMatch = oleRegex.exec(xml)) !== null) {
|
|
1210
|
+
elements.push({ index: oleMatch.index, type: 'ole', xml: oleMatch[0] });
|
|
1211
|
+
}
|
|
1212
|
+
// Equation (수식)
|
|
1213
|
+
const equationRegex = /<hp:equation\b[^>]*(?:\/>|>[\s\S]*?<\/hp:equation>)/g;
|
|
1214
|
+
let equationMatch;
|
|
1215
|
+
while ((equationMatch = equationRegex.exec(xml)) !== null) {
|
|
1216
|
+
elements.push({ index: equationMatch.index, type: 'equation', xml: equationMatch[0] });
|
|
1217
|
+
}
|
|
1218
|
+
// TextArt (글맵시)
|
|
1219
|
+
const textArtRegex = /<hp:textArt\b[^>]*(?:\/>|>[\s\S]*?<\/hp:textArt>)/g;
|
|
1220
|
+
let textArtMatch;
|
|
1221
|
+
while ((textArtMatch = textArtRegex.exec(xml)) !== null) {
|
|
1222
|
+
elements.push({ index: textArtMatch.index, type: 'textart', xml: textArtMatch[0] });
|
|
1223
|
+
}
|
|
1224
|
+
// UnknownObject
|
|
1225
|
+
const unknownObjRegex = /<hp:unknownObj\b[^>]*(?:\/>|>[\s\S]*?<\/hp:unknownObj>)/g;
|
|
1226
|
+
let unknownObjMatch;
|
|
1227
|
+
while ((unknownObjMatch = unknownObjRegex.exec(xml)) !== null) {
|
|
1228
|
+
elements.push({ index: unknownObjMatch.index, type: 'unknownobject', xml: unknownObjMatch[0] });
|
|
1229
|
+
}
|
|
1230
|
+
// Form Objects
|
|
1231
|
+
const buttonRegex = /<hp:button\b[^>]*(?:\/>|>[\s\S]*?<\/hp:button>)/g;
|
|
1232
|
+
let buttonMatch;
|
|
1233
|
+
while ((buttonMatch = buttonRegex.exec(xml)) !== null) {
|
|
1234
|
+
elements.push({ index: buttonMatch.index, type: 'button', xml: buttonMatch[0] });
|
|
1235
|
+
}
|
|
1236
|
+
const radioButtonRegex = /<hp:radioButton\b[^>]*(?:\/>|>[\s\S]*?<\/hp:radioButton>)/g;
|
|
1237
|
+
let radioButtonMatch;
|
|
1238
|
+
while ((radioButtonMatch = radioButtonRegex.exec(xml)) !== null) {
|
|
1239
|
+
elements.push({ index: radioButtonMatch.index, type: 'radiobutton', xml: radioButtonMatch[0] });
|
|
1240
|
+
}
|
|
1241
|
+
const checkButtonRegex = /<hp:checkButton\b[^>]*(?:\/>|>[\s\S]*?<\/hp:checkButton>)/g;
|
|
1242
|
+
let checkButtonMatch;
|
|
1243
|
+
while ((checkButtonMatch = checkButtonRegex.exec(xml)) !== null) {
|
|
1244
|
+
elements.push({ index: checkButtonMatch.index, type: 'checkbutton', xml: checkButtonMatch[0] });
|
|
1245
|
+
}
|
|
1246
|
+
const comboBoxRegex = /<hp:comboBox\b[^>]*(?:\/>|>[\s\S]*?<\/hp:comboBox>)/g;
|
|
1247
|
+
let comboBoxMatch;
|
|
1248
|
+
while ((comboBoxMatch = comboBoxRegex.exec(xml)) !== null) {
|
|
1249
|
+
elements.push({ index: comboBoxMatch.index, type: 'combobox', xml: comboBoxMatch[0] });
|
|
1250
|
+
}
|
|
1251
|
+
const editRegex = /<hp:edit\b[^>]*(?:\/>|>[\s\S]*?<\/hp:edit>)/g;
|
|
1252
|
+
let editMatch;
|
|
1253
|
+
while ((editMatch = editRegex.exec(xml)) !== null) {
|
|
1254
|
+
elements.push({ index: editMatch.index, type: 'edit', xml: editMatch[0] });
|
|
1255
|
+
}
|
|
1256
|
+
const listBoxRegex = /<hp:listBox\b[^>]*(?:\/>|>[\s\S]*?<\/hp:listBox>)/g;
|
|
1257
|
+
let listBoxMatch;
|
|
1258
|
+
while ((listBoxMatch = listBoxRegex.exec(xml)) !== null) {
|
|
1259
|
+
elements.push({ index: listBoxMatch.index, type: 'listbox', xml: listBoxMatch[0] });
|
|
1260
|
+
}
|
|
1261
|
+
const scrollBarRegex = /<hp:scrollBar\b[^>]*(?:\/>|>[\s\S]*?<\/hp:scrollBar>)/g;
|
|
1262
|
+
let scrollBarMatch;
|
|
1263
|
+
while ((scrollBarMatch = scrollBarRegex.exec(xml)) !== null) {
|
|
1264
|
+
elements.push({ index: scrollBarMatch.index, type: 'scrollbar', xml: scrollBarMatch[0] });
|
|
1265
|
+
}
|
|
1266
|
+
const picRegex = /<hp:pic\b[^>]*>[\s\S]*?<\/hp:pic>/g;
|
|
1267
|
+
let picMatch;
|
|
1268
|
+
while ((picMatch = picRegex.exec(xml)) !== null) {
|
|
1269
|
+
elements.push({ index: picMatch.index, type: 'pic', xml: picMatch[0] });
|
|
1270
|
+
}
|
|
1271
|
+
// Video element
|
|
1272
|
+
const videoRegex = /<hp:video\b[^>]*(?:\/>|>[\s\S]*?<\/hp:video>)/g;
|
|
1273
|
+
let videoMatch;
|
|
1274
|
+
while ((videoMatch = videoRegex.exec(xml)) !== null) {
|
|
1275
|
+
elements.push({ index: videoMatch.index, type: 'video', xml: videoMatch[0] });
|
|
1276
|
+
}
|
|
1277
|
+
// Chart element
|
|
1278
|
+
const chartRegex = /<hp:chart\b[^>]*(?:\/>|>[\s\S]*?<\/hp:chart>)/g;
|
|
1279
|
+
let chartMatch;
|
|
1280
|
+
while ((chartMatch = chartRegex.exec(xml)) !== null) {
|
|
1281
|
+
elements.push({ index: chartMatch.index, type: 'chart', xml: chartMatch[0] });
|
|
1282
|
+
}
|
|
1283
|
+
elements.sort((a, b) => a.index - b.index);
|
|
1284
|
+
for (const el of elements) {
|
|
1285
|
+
if (el.type === 'p') {
|
|
1286
|
+
const paragraph = this.parseParagraph(el.xml);
|
|
1287
|
+
// Store XML position from original XML for direct updates
|
|
1288
|
+
// This enables fast paragraph updates without re-parsing during save()
|
|
1289
|
+
if (el.originalXmlPosition) {
|
|
1290
|
+
paragraph._xmlPosition = {
|
|
1291
|
+
sectionIndex,
|
|
1292
|
+
start: el.originalXmlPosition.start,
|
|
1293
|
+
end: el.originalXmlPosition.end,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
// Check if this paragraph should have a footnote reference
|
|
1297
|
+
// (footnote was in original XML but removed from cleanedXml)
|
|
1298
|
+
for (const fnRef of footnoteRefPositions) {
|
|
1299
|
+
// Adjust position check - the footnote was within the original paragraph range
|
|
1300
|
+
// Since we removed footnotes, positions shift, but we can check if the footnote
|
|
1301
|
+
// position was within the paragraph's approximate range
|
|
1302
|
+
if (fnRef.position >= el.index && fnRef.position < el.index + el.xml.length + 500) {
|
|
1303
|
+
// Add footnote reference marker to the last run
|
|
1304
|
+
paragraph.runs.push({
|
|
1305
|
+
text: `${fnRef.number})`,
|
|
1306
|
+
footnoteRef: fnRef.type === 'footnote' ? fnRef.number : undefined,
|
|
1307
|
+
endnoteRef: fnRef.type === 'endnote' ? fnRef.number : undefined,
|
|
1308
|
+
charStyle: { superscript: true, fontSize: 7 },
|
|
1309
|
+
});
|
|
1310
|
+
// Remove this footnote from the list so it's not added again
|
|
1311
|
+
const idx = footnoteRefPositions.indexOf(fnRef);
|
|
1312
|
+
if (idx > -1)
|
|
1313
|
+
footnoteRefPositions.splice(idx, 1);
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
section.elements.push({ type: 'paragraph', data: paragraph });
|
|
1318
|
+
}
|
|
1319
|
+
else if (el.type === 'tbl') {
|
|
1320
|
+
const table = this.parseTable(el.xml);
|
|
1321
|
+
// Add parent paragraph's lineseg info to table for page break detection
|
|
1322
|
+
if (el.parentLinesegs && el.parentLinesegs.length > 0) {
|
|
1323
|
+
table.linesegs = el.parentLinesegs;
|
|
1324
|
+
}
|
|
1325
|
+
section.elements.push({ type: 'table', data: table });
|
|
1326
|
+
}
|
|
1327
|
+
else if (el.type === 'pic') {
|
|
1328
|
+
const image = this.parseImageElement(el.xml, content);
|
|
1329
|
+
if (image) {
|
|
1330
|
+
section.elements.push({ type: 'image', data: image });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
else if (el.type === 'line') {
|
|
1334
|
+
const line = this.parseLine(el.xml);
|
|
1335
|
+
section.elements.push({ type: 'line', data: line });
|
|
1336
|
+
}
|
|
1337
|
+
else if (el.type === 'rect') {
|
|
1338
|
+
const rect = this.parseRect(el.xml);
|
|
1339
|
+
section.elements.push({ type: 'rect', data: rect });
|
|
1340
|
+
}
|
|
1341
|
+
else if (el.type === 'ellipse') {
|
|
1342
|
+
const ellipse = this.parseEllipse(el.xml);
|
|
1343
|
+
section.elements.push({ type: 'ellipse', data: ellipse });
|
|
1344
|
+
}
|
|
1345
|
+
else if (el.type === 'arc') {
|
|
1346
|
+
const arc = this.parseArc(el.xml);
|
|
1347
|
+
section.elements.push({ type: 'arc', data: arc });
|
|
1348
|
+
}
|
|
1349
|
+
else if (el.type === 'polygon') {
|
|
1350
|
+
const polygon = this.parsePolygon(el.xml);
|
|
1351
|
+
section.elements.push({ type: 'polygon', data: polygon });
|
|
1352
|
+
}
|
|
1353
|
+
else if (el.type === 'curve') {
|
|
1354
|
+
const curve = this.parseCurve(el.xml);
|
|
1355
|
+
section.elements.push({ type: 'curve', data: curve });
|
|
1356
|
+
}
|
|
1357
|
+
else if (el.type === 'connectline') {
|
|
1358
|
+
const connectLine = this.parseConnectLine(el.xml);
|
|
1359
|
+
section.elements.push({ type: 'connectline', data: connectLine });
|
|
1360
|
+
}
|
|
1361
|
+
else if (el.type === 'container') {
|
|
1362
|
+
const container = this.parseContainer(el.xml, content);
|
|
1363
|
+
section.elements.push({ type: 'container', data: container });
|
|
1364
|
+
}
|
|
1365
|
+
else if (el.type === 'ole') {
|
|
1366
|
+
const ole = this.parseOle(el.xml);
|
|
1367
|
+
section.elements.push({ type: 'ole', data: ole });
|
|
1368
|
+
}
|
|
1369
|
+
else if (el.type === 'equation') {
|
|
1370
|
+
const equation = this.parseEquation(el.xml);
|
|
1371
|
+
section.elements.push({ type: 'equation', data: equation });
|
|
1372
|
+
}
|
|
1373
|
+
else if (el.type === 'textart') {
|
|
1374
|
+
const textArt = this.parseTextArt(el.xml);
|
|
1375
|
+
section.elements.push({ type: 'textart', data: textArt });
|
|
1376
|
+
}
|
|
1377
|
+
else if (el.type === 'unknownobject') {
|
|
1378
|
+
const unknownObj = this.parseUnknownObject(el.xml);
|
|
1379
|
+
section.elements.push({ type: 'unknownobject', data: unknownObj });
|
|
1380
|
+
}
|
|
1381
|
+
else if (el.type === 'button') {
|
|
1382
|
+
const button = this.parseButton(el.xml);
|
|
1383
|
+
section.elements.push({ type: 'button', data: button });
|
|
1384
|
+
}
|
|
1385
|
+
else if (el.type === 'radiobutton') {
|
|
1386
|
+
const radioButton = this.parseRadioButton(el.xml);
|
|
1387
|
+
section.elements.push({ type: 'radiobutton', data: radioButton });
|
|
1388
|
+
}
|
|
1389
|
+
else if (el.type === 'checkbutton') {
|
|
1390
|
+
const checkButton = this.parseCheckButton(el.xml);
|
|
1391
|
+
section.elements.push({ type: 'checkbutton', data: checkButton });
|
|
1392
|
+
}
|
|
1393
|
+
else if (el.type === 'combobox') {
|
|
1394
|
+
const comboBox = this.parseComboBox(el.xml);
|
|
1395
|
+
section.elements.push({ type: 'combobox', data: comboBox });
|
|
1396
|
+
}
|
|
1397
|
+
else if (el.type === 'edit') {
|
|
1398
|
+
const edit = this.parseEdit(el.xml);
|
|
1399
|
+
section.elements.push({ type: 'edit', data: edit });
|
|
1400
|
+
}
|
|
1401
|
+
else if (el.type === 'listbox') {
|
|
1402
|
+
const listBox = this.parseListBox(el.xml);
|
|
1403
|
+
section.elements.push({ type: 'listbox', data: listBox });
|
|
1404
|
+
}
|
|
1405
|
+
else if (el.type === 'scrollbar') {
|
|
1406
|
+
const scrollBar = this.parseScrollBar(el.xml);
|
|
1407
|
+
section.elements.push({ type: 'scrollbar', data: scrollBar });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
this.parseHorizontalRules(xml, section);
|
|
1411
|
+
if (section.elements.length === 0) {
|
|
1412
|
+
const paragraphs = this.parseParagraphsSimple(xml);
|
|
1413
|
+
for (const p of paragraphs) {
|
|
1414
|
+
section.elements.push({ type: 'paragraph', data: p });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
section.header = this.parseHeaderFooter(xml, 'header');
|
|
1418
|
+
section.footer = this.parseHeaderFooter(xml, 'footer');
|
|
1419
|
+
this.parseFootnotes(xml, content);
|
|
1420
|
+
this.parseEndnotes(xml, content);
|
|
1421
|
+
this.parseHiddenComments(xml, section);
|
|
1422
|
+
return section;
|
|
1423
|
+
}
|
|
1424
|
+
static parseEndnotes(xml, content) {
|
|
1425
|
+
const endnoteRegex = /<hp:endnote[^>]*>([\s\S]*?)<\/hp:endnote>/gi;
|
|
1426
|
+
let match;
|
|
1427
|
+
let endnoteIndex = 0;
|
|
1428
|
+
while ((match = endnoteRegex.exec(xml)) !== null) {
|
|
1429
|
+
const endnoteContent = match[0];
|
|
1430
|
+
const paragraphs = [];
|
|
1431
|
+
const paraRegex = /<hp:p\b[^>]*>[\s\S]*?<\/hp:p>/g;
|
|
1432
|
+
let paraMatch;
|
|
1433
|
+
while ((paraMatch = paraRegex.exec(endnoteContent)) !== null) {
|
|
1434
|
+
paragraphs.push(this.parseParagraph(paraMatch[0]));
|
|
1435
|
+
}
|
|
1436
|
+
if (paragraphs.length > 0) {
|
|
1437
|
+
content.endnotes.push({
|
|
1438
|
+
id: `endnote_${endnoteIndex++}`,
|
|
1439
|
+
paragraphs
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
static parseHiddenComments(xml, section) {
|
|
1445
|
+
const hiddenCommentRegex = /<hp:hiddenComment[^>]*>([\s\S]*?)<\/hp:hiddenComment>/gi;
|
|
1446
|
+
let match;
|
|
1447
|
+
while ((match = hiddenCommentRegex.exec(xml)) !== null) {
|
|
1448
|
+
const commentContent = match[0];
|
|
1449
|
+
const paragraphs = [];
|
|
1450
|
+
const paraRegex = /<hp:p\b[^>]*>[\s\S]*?<\/hp:p>/g;
|
|
1451
|
+
let paraMatch;
|
|
1452
|
+
while ((paraMatch = paraRegex.exec(commentContent)) !== null) {
|
|
1453
|
+
paragraphs.push(this.parseParagraph(paraMatch[0]));
|
|
1454
|
+
}
|
|
1455
|
+
// Hidden comments are not rendered but stored for reference
|
|
1456
|
+
// They could be stored in section or in a separate collection
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
static parseHeaderFooter(xml, type) {
|
|
1460
|
+
const tagName = type === 'header' ? 'hp:header' : 'hp:footer';
|
|
1461
|
+
const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'gi');
|
|
1462
|
+
const match = xml.match(regex);
|
|
1463
|
+
if (!match)
|
|
1464
|
+
return undefined;
|
|
1465
|
+
const content = match[0];
|
|
1466
|
+
const paragraphs = [];
|
|
1467
|
+
const paraRegex = /<hp:p\b[^>]*>[\s\S]*?<\/hp:p>/g;
|
|
1468
|
+
let paraMatch;
|
|
1469
|
+
while ((paraMatch = paraRegex.exec(content)) !== null) {
|
|
1470
|
+
paragraphs.push(this.parseParagraph(paraMatch[0]));
|
|
1471
|
+
}
|
|
1472
|
+
if (paragraphs.length === 0)
|
|
1473
|
+
return undefined;
|
|
1474
|
+
return { paragraphs };
|
|
1475
|
+
}
|
|
1476
|
+
static parseFootnotes(xml, content) {
|
|
1477
|
+
// Match hp:footNote (note the capital N in actual HWPX files)
|
|
1478
|
+
const footnoteRegex = /<hp:footNote\b[^>]*>([\s\S]*?)<\/hp:footNote>/gi;
|
|
1479
|
+
let match;
|
|
1480
|
+
let footnoteIndex = 0;
|
|
1481
|
+
while ((match = footnoteRegex.exec(xml)) !== null) {
|
|
1482
|
+
const footnoteContent = match[0];
|
|
1483
|
+
const paragraphs = [];
|
|
1484
|
+
// Extract footnote number from attribute
|
|
1485
|
+
const numberMatch = footnoteContent.match(/number="(\d+)"/);
|
|
1486
|
+
const footnoteNumber = numberMatch ? parseInt(numberMatch[1]) : footnoteIndex + 1;
|
|
1487
|
+
const paraRegex = /<hp:p\b[^>]*>[\s\S]*?<\/hp:p>/g;
|
|
1488
|
+
let paraMatch;
|
|
1489
|
+
while ((paraMatch = paraRegex.exec(footnoteContent)) !== null) {
|
|
1490
|
+
paragraphs.push(this.parseParagraph(paraMatch[0]));
|
|
1491
|
+
}
|
|
1492
|
+
if (paragraphs.length > 0) {
|
|
1493
|
+
content.footnotes.push({
|
|
1494
|
+
id: `footnote_${footnoteIndex}`,
|
|
1495
|
+
number: footnoteNumber,
|
|
1496
|
+
type: 'footnote',
|
|
1497
|
+
paragraphs,
|
|
1498
|
+
});
|
|
1499
|
+
footnoteIndex++;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
// Match hp:endNote (note the capital N in actual HWPX files)
|
|
1503
|
+
const endnoteRegex = /<hp:endNote\b[^>]*>([\s\S]*?)<\/hp:endNote>/gi;
|
|
1504
|
+
while ((match = endnoteRegex.exec(xml)) !== null) {
|
|
1505
|
+
const endnoteContent = match[0];
|
|
1506
|
+
const paragraphs = [];
|
|
1507
|
+
const numberMatch = endnoteContent.match(/number="(\d+)"/);
|
|
1508
|
+
const endnoteNumber = numberMatch ? parseInt(numberMatch[1]) : footnoteIndex + 1;
|
|
1509
|
+
const paraRegex = /<hp:p\b[^>]*>[\s\S]*?<\/hp:p>/g;
|
|
1510
|
+
let paraMatch;
|
|
1511
|
+
while ((paraMatch = paraRegex.exec(endnoteContent)) !== null) {
|
|
1512
|
+
paragraphs.push(this.parseParagraph(paraMatch[0]));
|
|
1513
|
+
}
|
|
1514
|
+
if (paragraphs.length > 0) {
|
|
1515
|
+
content.footnotes.push({
|
|
1516
|
+
id: `endnote_${footnoteIndex}`,
|
|
1517
|
+
number: endnoteNumber,
|
|
1518
|
+
type: 'endnote',
|
|
1519
|
+
paragraphs,
|
|
1520
|
+
});
|
|
1521
|
+
footnoteIndex++;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
static parsePageSettings(xml) {
|
|
1526
|
+
const defaults = {
|
|
1527
|
+
width: 595,
|
|
1528
|
+
height: 842,
|
|
1529
|
+
marginTop: 56.7,
|
|
1530
|
+
marginBottom: 56.7,
|
|
1531
|
+
marginLeft: 56.7,
|
|
1532
|
+
marginRight: 56.7,
|
|
1533
|
+
orientation: 'portrait',
|
|
1534
|
+
};
|
|
1535
|
+
const pagePrMatch = xml.match(/<hp:pagePr[^>]*>([\s\S]*?)<\/hp:pagePr>/);
|
|
1536
|
+
if (pagePrMatch) {
|
|
1537
|
+
const pagePr = pagePrMatch[0];
|
|
1538
|
+
const widthMatch = pagePr.match(/width="(\d+)"/);
|
|
1539
|
+
const heightMatch = pagePr.match(/height="(\d+)"/);
|
|
1540
|
+
if (widthMatch)
|
|
1541
|
+
defaults.width = parseInt(widthMatch[1]) / 100;
|
|
1542
|
+
if (heightMatch)
|
|
1543
|
+
defaults.height = parseInt(heightMatch[1]) / 100;
|
|
1544
|
+
const landscapeMatch = pagePr.match(/landscape="([^"]*)"/);
|
|
1545
|
+
if (landscapeMatch) {
|
|
1546
|
+
const val = landscapeMatch[1].toUpperCase();
|
|
1547
|
+
if (val === 'WIDELY' || val === '1' || val === 'TRUE' || val === 'LANDSCAPE') {
|
|
1548
|
+
defaults.orientation = 'landscape';
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const marginMatch = pagePr.match(/<hp:margin[^>]*left="(\d+)"[^>]*right="(\d+)"[^>]*top="(\d+)"[^>]*bottom="(\d+)"/);
|
|
1552
|
+
if (marginMatch) {
|
|
1553
|
+
defaults.marginLeft = parseInt(marginMatch[1]) / 100;
|
|
1554
|
+
defaults.marginRight = parseInt(marginMatch[2]) / 100;
|
|
1555
|
+
defaults.marginTop = parseInt(marginMatch[3]) / 100;
|
|
1556
|
+
defaults.marginBottom = parseInt(marginMatch[4]) / 100;
|
|
1557
|
+
}
|
|
1558
|
+
// Parse header and footer margins
|
|
1559
|
+
const marginTag = pagePr.match(/<hp:margin[^>]*>/);
|
|
1560
|
+
if (marginTag) {
|
|
1561
|
+
const headerMatch = marginTag[0].match(/header="(\d+)"/);
|
|
1562
|
+
const footerMatch = marginTag[0].match(/footer="(\d+)"/);
|
|
1563
|
+
const gutterMatch = marginTag[0].match(/gutter="(\d+)"/);
|
|
1564
|
+
if (headerMatch)
|
|
1565
|
+
defaults.headerMargin = parseInt(headerMatch[1]) / 100;
|
|
1566
|
+
if (footerMatch)
|
|
1567
|
+
defaults.footerMargin = parseInt(footerMatch[1]) / 100;
|
|
1568
|
+
if (gutterMatch)
|
|
1569
|
+
defaults.gutterMargin = parseInt(gutterMatch[1]) / 100;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const pageDefMatch = xml.match(/<hp:pageDef[^>]*>/);
|
|
1573
|
+
if (pageDefMatch && defaults.width === 595) {
|
|
1574
|
+
const pageDef = pageDefMatch[0];
|
|
1575
|
+
const widthMatch = pageDef.match(/width="(\d+)"/);
|
|
1576
|
+
const heightMatch = pageDef.match(/height="(\d+)"/);
|
|
1577
|
+
if (widthMatch)
|
|
1578
|
+
defaults.width = parseInt(widthMatch[1]) / 100;
|
|
1579
|
+
if (heightMatch)
|
|
1580
|
+
defaults.height = parseInt(heightMatch[1]) / 100;
|
|
1581
|
+
const landscapeMatch = pageDef.match(/landscape="([^"]*)"/);
|
|
1582
|
+
if (landscapeMatch?.[1] === '1' || landscapeMatch?.[1] === 'true') {
|
|
1583
|
+
defaults.orientation = 'landscape';
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return defaults;
|
|
1587
|
+
}
|
|
1588
|
+
static parseSectionProperties(xml) {
|
|
1589
|
+
const secPrMatch = xml.match(/<hp:secPr[^>]*>([\s\S]*?)<\/hp:secPr>/);
|
|
1590
|
+
if (!secPrMatch)
|
|
1591
|
+
return undefined;
|
|
1592
|
+
const content = secPrMatch[0];
|
|
1593
|
+
const props = {};
|
|
1594
|
+
const textDirMatch = content.match(/textDirection="([^"]*)"/);
|
|
1595
|
+
if (textDirMatch) {
|
|
1596
|
+
props.textDirection = textDirMatch[1].toLowerCase() === 'vertical' ? 'vertical' : 'horizontal';
|
|
1597
|
+
}
|
|
1598
|
+
const spaceColsMatch = content.match(/spaceColumns="(\d+)"/);
|
|
1599
|
+
if (spaceColsMatch) {
|
|
1600
|
+
props.spaceColumns = parseInt(spaceColsMatch[1]) / 100;
|
|
1601
|
+
}
|
|
1602
|
+
const tabStopMatch = content.match(/tabStop="(\d+)"/);
|
|
1603
|
+
if (tabStopMatch) {
|
|
1604
|
+
props.tabStop = parseInt(tabStopMatch[1]) / 100;
|
|
1605
|
+
}
|
|
1606
|
+
const masterPageCntMatch = content.match(/masterPageCnt="(\d+)"/);
|
|
1607
|
+
if (masterPageCntMatch) {
|
|
1608
|
+
props.masterPageCnt = parseInt(masterPageCntMatch[1]);
|
|
1609
|
+
}
|
|
1610
|
+
const gridMatch = content.match(/<hp:grid[^>]*lineGrid="(\d+)"[^>]*charGrid="(\d+)"/);
|
|
1611
|
+
if (gridMatch) {
|
|
1612
|
+
props.grid = {
|
|
1613
|
+
lineGrid: parseInt(gridMatch[1]),
|
|
1614
|
+
charGrid: parseInt(gridMatch[2])
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
const startNumMatch = content.match(/<hp:startNum[^>]*pageStartsOn="([^"]*)"[^>]*page="(\d+)"/);
|
|
1618
|
+
if (startNumMatch) {
|
|
1619
|
+
props.startNum = {
|
|
1620
|
+
pageStartsOn: startNumMatch[1].toLowerCase(),
|
|
1621
|
+
page: parseInt(startNumMatch[2])
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
const visMatch = content.match(/<hp:visibility[^>]*/);
|
|
1625
|
+
if (visMatch) {
|
|
1626
|
+
const vis = visMatch[0];
|
|
1627
|
+
props.visibility = {
|
|
1628
|
+
hideFirstHeader: vis.includes('hideFirstHeader="1"') || vis.includes('hideFirstHeader="true"'),
|
|
1629
|
+
hideFirstFooter: vis.includes('hideFirstFooter="1"') || vis.includes('hideFirstFooter="true"'),
|
|
1630
|
+
hideFirstMasterPage: vis.includes('hideFirstMasterPage="1"') || vis.includes('hideFirstMasterPage="true"'),
|
|
1631
|
+
hideFirstPageNum: vis.includes('hideFirstPageNum="1"') || vis.includes('hideFirstPageNum="true"'),
|
|
1632
|
+
showLineNumber: vis.includes('showLineNumber="1"') || vis.includes('showLineNumber="true"')
|
|
1633
|
+
};
|
|
1634
|
+
const borderMatch = vis.match(/border="([^"]*)"/);
|
|
1635
|
+
if (borderMatch) {
|
|
1636
|
+
const borderMap = {
|
|
1637
|
+
'SHOW_ALL': 'showAll', 'HIDE_ALL': 'hideAll',
|
|
1638
|
+
'SHOW_FIRST_PAGE_ONLY': 'showFirstPageOnly', 'SHOW_ALL_BUT_FIRST_PAGE': 'showAllButFirstPage'
|
|
1639
|
+
};
|
|
1640
|
+
props.visibility.border = borderMap[borderMatch[1].toUpperCase()] || 'showAll';
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
const pageBorderFills = [];
|
|
1644
|
+
const pbfRegex = /<hp:pageBorderFill[^>]*type="([^"]*)"[^>]*borderFillIDRef="(\d+)"[^>]*>([\s\S]*?)<\/hp:pageBorderFill>/gi;
|
|
1645
|
+
let pbfMatch;
|
|
1646
|
+
while ((pbfMatch = pbfRegex.exec(content)) !== null) {
|
|
1647
|
+
const pbf = {
|
|
1648
|
+
type: pbfMatch[1].toLowerCase(),
|
|
1649
|
+
borderFillIdRef: parseInt(pbfMatch[2])
|
|
1650
|
+
};
|
|
1651
|
+
const offsetMatch = pbfMatch[3].match(/<hp:offset[^>]*left="(\d+)"[^>]*right="(\d+)"[^>]*top="(\d+)"[^>]*bottom="(\d+)"/);
|
|
1652
|
+
if (offsetMatch) {
|
|
1653
|
+
pbf.offset = {
|
|
1654
|
+
left: parseInt(offsetMatch[1]) / 100,
|
|
1655
|
+
right: parseInt(offsetMatch[2]) / 100,
|
|
1656
|
+
top: parseInt(offsetMatch[3]) / 100,
|
|
1657
|
+
bottom: parseInt(offsetMatch[4]) / 100
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
pageBorderFills.push(pbf);
|
|
1661
|
+
}
|
|
1662
|
+
if (pageBorderFills.length > 0) {
|
|
1663
|
+
props.pageBorderFill = pageBorderFills;
|
|
1664
|
+
}
|
|
1665
|
+
// Parse MasterPage
|
|
1666
|
+
const masterPages = this.parseMasterPages(content);
|
|
1667
|
+
if (masterPages && masterPages.length > 0) {
|
|
1668
|
+
props.masterPage = masterPages;
|
|
1669
|
+
}
|
|
1670
|
+
return props;
|
|
1671
|
+
}
|
|
1672
|
+
static parseMasterPages(xml) {
|
|
1673
|
+
const masterPages = [];
|
|
1674
|
+
const masterPageRegex = /<hp:masterPage[^>]*>([\s\S]*?)<\/hp:masterPage>/gi;
|
|
1675
|
+
let match;
|
|
1676
|
+
while ((match = masterPageRegex.exec(xml)) !== null) {
|
|
1677
|
+
const content = match[0];
|
|
1678
|
+
const masterPage = {};
|
|
1679
|
+
const typeMatch = content.match(/type="([^"]*)"/);
|
|
1680
|
+
if (typeMatch)
|
|
1681
|
+
masterPage.type = typeMatch[1].toLowerCase();
|
|
1682
|
+
const textWidthMatch = content.match(/textWidth="(\d+)"/);
|
|
1683
|
+
if (textWidthMatch)
|
|
1684
|
+
masterPage.textWidth = parseInt(textWidthMatch[1]);
|
|
1685
|
+
const textHeightMatch = content.match(/textHeight="(\d+)"/);
|
|
1686
|
+
if (textHeightMatch)
|
|
1687
|
+
masterPage.textHeight = parseInt(textHeightMatch[1]);
|
|
1688
|
+
const hasTextRefMatch = content.match(/hasTextRef="(true|false|1|0)"/i);
|
|
1689
|
+
if (hasTextRefMatch)
|
|
1690
|
+
masterPage.hasTextRef = hasTextRefMatch[1] === 'true' || hasTextRefMatch[1] === '1';
|
|
1691
|
+
const hasNumRefMatch = content.match(/hasNumRef="(true|false|1|0)"/i);
|
|
1692
|
+
if (hasNumRefMatch)
|
|
1693
|
+
masterPage.hasNumRef = hasNumRefMatch[1] === 'true' || hasNumRefMatch[1] === '1';
|
|
1694
|
+
// Parse paragraphs in master page
|
|
1695
|
+
const paragraphs = [];
|
|
1696
|
+
const paraRegex = /<hp:p\b[^>]*>[\s\S]*?<\/hp:p>/g;
|
|
1697
|
+
let paraMatch;
|
|
1698
|
+
while ((paraMatch = paraRegex.exec(content)) !== null) {
|
|
1699
|
+
paragraphs.push(this.parseParagraph(paraMatch[0]));
|
|
1700
|
+
}
|
|
1701
|
+
if (paragraphs.length > 0) {
|
|
1702
|
+
masterPage.paragraphs = paragraphs;
|
|
1703
|
+
}
|
|
1704
|
+
masterPages.push(masterPage);
|
|
1705
|
+
}
|
|
1706
|
+
// Parse extended master pages
|
|
1707
|
+
const extMasterPageRegex = /<hp:extMasterPage[^>]*>([\s\S]*?)<\/hp:extMasterPage>/gi;
|
|
1708
|
+
while ((match = extMasterPageRegex.exec(xml)) !== null) {
|
|
1709
|
+
const content = match[0];
|
|
1710
|
+
const masterPage = { isExtended: true };
|
|
1711
|
+
const typeMatch = content.match(/type="([^"]*)"/);
|
|
1712
|
+
if (typeMatch)
|
|
1713
|
+
masterPage.type = typeMatch[1].toLowerCase();
|
|
1714
|
+
const pageNumberMatch = content.match(/pageNumber="(\d+)"/);
|
|
1715
|
+
if (pageNumberMatch)
|
|
1716
|
+
masterPage.pageNumber = parseInt(pageNumberMatch[1]);
|
|
1717
|
+
const pageDuplicateMatch = content.match(/pageDuplicate="(true|false|1|0)"/i);
|
|
1718
|
+
if (pageDuplicateMatch)
|
|
1719
|
+
masterPage.pageDuplicate = pageDuplicateMatch[1] === 'true' || pageDuplicateMatch[1] === '1';
|
|
1720
|
+
const pageFrontMatch = content.match(/pageFront="(true|false|1|0)"/i);
|
|
1721
|
+
if (pageFrontMatch)
|
|
1722
|
+
masterPage.pageFront = pageFrontMatch[1] === 'true' || pageFrontMatch[1] === '1';
|
|
1723
|
+
const paragraphs = [];
|
|
1724
|
+
const paraRegex = /<hp:p\b[^>]*>[\s\S]*?<\/hp:p>/g;
|
|
1725
|
+
let paraMatch;
|
|
1726
|
+
while ((paraMatch = paraRegex.exec(content)) !== null) {
|
|
1727
|
+
paragraphs.push(this.parseParagraph(paraMatch[0]));
|
|
1728
|
+
}
|
|
1729
|
+
if (paragraphs.length > 0) {
|
|
1730
|
+
masterPage.paragraphs = paragraphs;
|
|
1731
|
+
}
|
|
1732
|
+
masterPages.push(masterPage);
|
|
1733
|
+
}
|
|
1734
|
+
return masterPages.length > 0 ? masterPages : undefined;
|
|
1735
|
+
}
|
|
1736
|
+
static parseColumnDef(xml) {
|
|
1737
|
+
const colDefMatch = xml.match(/<hp:colDef[^>]*>([\s\S]*?)<\/hp:colDef>/i);
|
|
1738
|
+
if (!colDefMatch)
|
|
1739
|
+
return undefined;
|
|
1740
|
+
const content = colDefMatch[0];
|
|
1741
|
+
const colDef = {};
|
|
1742
|
+
const typeMatch = content.match(/type="([^"]*)"/);
|
|
1743
|
+
if (typeMatch)
|
|
1744
|
+
colDef.type = typeMatch[1].toLowerCase();
|
|
1745
|
+
const countMatch = content.match(/count="(\d+)"/);
|
|
1746
|
+
if (countMatch)
|
|
1747
|
+
colDef.count = parseInt(countMatch[1]);
|
|
1748
|
+
const layoutMatch = content.match(/layout="([^"]*)"/);
|
|
1749
|
+
if (layoutMatch)
|
|
1750
|
+
colDef.layout = layoutMatch[1].toLowerCase();
|
|
1751
|
+
const sameSizeMatch = content.match(/sameSize="(true|false|1|0)"/i);
|
|
1752
|
+
if (sameSizeMatch)
|
|
1753
|
+
colDef.sameSize = sameSizeMatch[1] === 'true' || sameSizeMatch[1] === '1';
|
|
1754
|
+
const sameGapMatch = content.match(/sameGap="(\d+)"/);
|
|
1755
|
+
if (sameGapMatch)
|
|
1756
|
+
colDef.sameGap = parseInt(sameGapMatch[1]);
|
|
1757
|
+
// Parse column line
|
|
1758
|
+
const columnLineMatch = content.match(/<hp:columnLine[^>]*type="([^"]*)"[^>]*width="([^"]*)"[^>]*color="([^"]*)"/i);
|
|
1759
|
+
if (columnLineMatch) {
|
|
1760
|
+
colDef.columnLine = {
|
|
1761
|
+
type: columnLineMatch[1].toLowerCase(),
|
|
1762
|
+
width: columnLineMatch[2],
|
|
1763
|
+
color: columnLineMatch[3]
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
// Parse columns
|
|
1767
|
+
const columns = [];
|
|
1768
|
+
const columnRegex = /<hp:column[^>]*width="(\d+)"[^>]*gap="(\d+)"/gi;
|
|
1769
|
+
let columnMatch;
|
|
1770
|
+
while ((columnMatch = columnRegex.exec(content)) !== null) {
|
|
1771
|
+
columns.push({
|
|
1772
|
+
width: parseInt(columnMatch[1]),
|
|
1773
|
+
gap: parseInt(columnMatch[2])
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
if (columns.length > 0) {
|
|
1777
|
+
colDef.columns = columns;
|
|
1778
|
+
}
|
|
1779
|
+
return colDef;
|
|
1780
|
+
}
|
|
1781
|
+
static parseImageEffects(xml) {
|
|
1782
|
+
const effectsMatch = xml.match(/<hp:effects[^>]*>([\s\S]*?)<\/hp:effects>/i);
|
|
1783
|
+
if (!effectsMatch)
|
|
1784
|
+
return undefined;
|
|
1785
|
+
const content = effectsMatch[1];
|
|
1786
|
+
const effects = {};
|
|
1787
|
+
// Parse shadow effect
|
|
1788
|
+
const shadowMatch = content.match(/<hp:shadowEffect[^>]*>([\s\S]*?)<\/hp:shadowEffect>/i);
|
|
1789
|
+
if (shadowMatch) {
|
|
1790
|
+
const shadowContent = shadowMatch[0];
|
|
1791
|
+
effects.shadow = {};
|
|
1792
|
+
const styleMatch = shadowContent.match(/style="([^"]*)"/);
|
|
1793
|
+
if (styleMatch)
|
|
1794
|
+
effects.shadow.style = styleMatch[1];
|
|
1795
|
+
const alphaMatch = shadowContent.match(/alpha="([^"]*)"/);
|
|
1796
|
+
if (alphaMatch)
|
|
1797
|
+
effects.shadow.alpha = parseFloat(alphaMatch[1]);
|
|
1798
|
+
const radiusMatch = shadowContent.match(/radius="([^"]*)"/);
|
|
1799
|
+
if (radiusMatch)
|
|
1800
|
+
effects.shadow.radius = parseFloat(radiusMatch[1]);
|
|
1801
|
+
const directionMatch = shadowContent.match(/direction="([^"]*)"/);
|
|
1802
|
+
if (directionMatch)
|
|
1803
|
+
effects.shadow.direction = parseFloat(directionMatch[1]);
|
|
1804
|
+
const distanceMatch = shadowContent.match(/distance="([^"]*)"/);
|
|
1805
|
+
if (distanceMatch)
|
|
1806
|
+
effects.shadow.distance = parseFloat(distanceMatch[1]);
|
|
1807
|
+
// Parse color
|
|
1808
|
+
const colorMatch = shadowContent.match(/<hp:effectsColor[^>]*colorR="(\d+)"[^>]*colorG="(\d+)"[^>]*colorB="(\d+)"/i);
|
|
1809
|
+
if (colorMatch) {
|
|
1810
|
+
effects.shadow.color = `rgb(${colorMatch[1]},${colorMatch[2]},${colorMatch[3]})`;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
// Parse glow effect
|
|
1814
|
+
const glowMatch = content.match(/<hp:glow[^>]*>([\s\S]*?)<\/hp:glow>/i);
|
|
1815
|
+
if (glowMatch) {
|
|
1816
|
+
const glowContent = glowMatch[0];
|
|
1817
|
+
effects.glow = {};
|
|
1818
|
+
const alphaMatch = glowContent.match(/alpha="([^"]*)"/);
|
|
1819
|
+
if (alphaMatch)
|
|
1820
|
+
effects.glow.alpha = parseFloat(alphaMatch[1]);
|
|
1821
|
+
const radiusMatch = glowContent.match(/radius="([^"]*)"/);
|
|
1822
|
+
if (radiusMatch)
|
|
1823
|
+
effects.glow.radius = parseFloat(radiusMatch[1]);
|
|
1824
|
+
const colorMatch = glowContent.match(/<hp:effectsColor[^>]*colorR="(\d+)"[^>]*colorG="(\d+)"[^>]*colorB="(\d+)"/i);
|
|
1825
|
+
if (colorMatch) {
|
|
1826
|
+
effects.glow.color = `rgb(${colorMatch[1]},${colorMatch[2]},${colorMatch[3]})`;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Parse soft edge effect
|
|
1830
|
+
const softEdgeMatch = content.match(/<hp:softEdge[^>]*radius="([^"]*)"/i);
|
|
1831
|
+
if (softEdgeMatch) {
|
|
1832
|
+
effects.softEdge = { radius: parseFloat(softEdgeMatch[1]) };
|
|
1833
|
+
}
|
|
1834
|
+
// Parse reflection effect
|
|
1835
|
+
const reflectionMatch = content.match(/<hp:reflection[^>]*>([\s\S]*?)<\/hp:reflection>|<hp:reflection([^>]*)\/>/i);
|
|
1836
|
+
if (reflectionMatch) {
|
|
1837
|
+
const refContent = reflectionMatch[0];
|
|
1838
|
+
effects.reflection = {};
|
|
1839
|
+
const radiusMatch = refContent.match(/radius="([^"]*)"/);
|
|
1840
|
+
if (radiusMatch)
|
|
1841
|
+
effects.reflection.radius = parseFloat(radiusMatch[1]);
|
|
1842
|
+
const directionMatch = refContent.match(/direction="([^"]*)"/);
|
|
1843
|
+
if (directionMatch)
|
|
1844
|
+
effects.reflection.direction = parseFloat(directionMatch[1]);
|
|
1845
|
+
const distanceMatch = refContent.match(/distance="([^"]*)"/);
|
|
1846
|
+
if (distanceMatch)
|
|
1847
|
+
effects.reflection.distance = parseFloat(distanceMatch[1]);
|
|
1848
|
+
const startAlphaMatch = refContent.match(/startAlpha="([^"]*)"/);
|
|
1849
|
+
if (startAlphaMatch)
|
|
1850
|
+
effects.reflection.startAlpha = parseFloat(startAlphaMatch[1]);
|
|
1851
|
+
const endAlphaMatch = refContent.match(/endAlpha="([^"]*)"/);
|
|
1852
|
+
if (endAlphaMatch)
|
|
1853
|
+
effects.reflection.endAlpha = parseFloat(endAlphaMatch[1]);
|
|
1854
|
+
}
|
|
1855
|
+
return Object.keys(effects).length > 0 ? effects : undefined;
|
|
1856
|
+
}
|
|
1857
|
+
static parseParagraphsSimple(xml) {
|
|
1858
|
+
const paragraphs = [];
|
|
1859
|
+
const paragraphRegex = /<hp:p[^>]*>([\s\S]*?)<\/hp:p>/g;
|
|
1860
|
+
let match;
|
|
1861
|
+
while ((match = paragraphRegex.exec(xml)) !== null) {
|
|
1862
|
+
const paragraph = this.parseParagraph(match[0]);
|
|
1863
|
+
paragraphs.push(paragraph);
|
|
1864
|
+
}
|
|
1865
|
+
return paragraphs;
|
|
1866
|
+
}
|
|
1867
|
+
static parseParagraph(xml) {
|
|
1868
|
+
// Extract only the opening <hp:p ...> tag to get paragraph attributes
|
|
1869
|
+
const pTagMatch = xml.match(/^<hp:p\s+([^>]*)>/);
|
|
1870
|
+
const pTagAttrs = pTagMatch ? pTagMatch[1] : '';
|
|
1871
|
+
// Extract original id from XML if present, otherwise generate new one
|
|
1872
|
+
const idMatch = pTagAttrs.match(/\bid="([^"]+)"/);
|
|
1873
|
+
const originalId = idMatch ? idMatch[1] : generateId();
|
|
1874
|
+
const paragraph = {
|
|
1875
|
+
id: originalId,
|
|
1876
|
+
runs: [],
|
|
1877
|
+
};
|
|
1878
|
+
// Check for page break on this paragraph (only in the <hp:p> tag itself)
|
|
1879
|
+
const pageBreakMatch = pTagAttrs.match(/pageBreak="([^"]*)"/);
|
|
1880
|
+
if (pageBreakMatch && pageBreakMatch[1] === '1') {
|
|
1881
|
+
paragraph.pageBreak = true;
|
|
1882
|
+
}
|
|
1883
|
+
// Get paragraph shape reference from the <hp:p> tag
|
|
1884
|
+
const paraShapeRefMatch = pTagAttrs.match(/paraPrIDRef="(\d+)"/);
|
|
1885
|
+
if (paraShapeRefMatch) {
|
|
1886
|
+
const paraShape = this.styles.paraShapes.get(parseInt(paraShapeRefMatch[1]));
|
|
1887
|
+
if (paraShape) {
|
|
1888
|
+
paragraph.paraStyle = {
|
|
1889
|
+
align: paraShape.align,
|
|
1890
|
+
lineSpacing: paraShape.lineSpacing,
|
|
1891
|
+
marginTop: paraShape.marginTop,
|
|
1892
|
+
marginBottom: paraShape.marginBottom,
|
|
1893
|
+
marginLeft: paraShape.marginLeft,
|
|
1894
|
+
marginRight: paraShape.marginRight,
|
|
1895
|
+
firstLineIndent: paraShape.firstLineIndent,
|
|
1896
|
+
keepWithNext: paraShape.keepWithNext,
|
|
1897
|
+
keepLines: paraShape.keepLines,
|
|
1898
|
+
};
|
|
1899
|
+
// Check pageBreakBefore from paragraph shape
|
|
1900
|
+
if (paraShape.pageBreakBefore) {
|
|
1901
|
+
paragraph.pageBreak = true;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const runRegex = /<hp:run[^>]*>([\s\S]*?)<\/hp:run>/g;
|
|
1906
|
+
let runMatch;
|
|
1907
|
+
while ((runMatch = runRegex.exec(xml)) !== null) {
|
|
1908
|
+
const runContent = runMatch[0];
|
|
1909
|
+
const parsedRuns = this.parseRun(runContent);
|
|
1910
|
+
paragraph.runs.push(...parsedRuns);
|
|
1911
|
+
}
|
|
1912
|
+
if (paragraph.runs.length === 0) {
|
|
1913
|
+
const textRegex = /<hp:t[^>]*>([^<]*)<\/hp:t>/g;
|
|
1914
|
+
let textMatch;
|
|
1915
|
+
while ((textMatch = textRegex.exec(xml)) !== null) {
|
|
1916
|
+
paragraph.runs.push({ text: this.decodeXmlEntities(textMatch[1]) });
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
const listMatch = xml.match(/<hp:lineseg[^>]*listLevel="(\d+)"/);
|
|
1920
|
+
if (listMatch) {
|
|
1921
|
+
paragraph.listLevel = parseInt(listMatch[1]);
|
|
1922
|
+
paragraph.listType = 'bullet';
|
|
1923
|
+
}
|
|
1924
|
+
// Parse lineseg info for accurate layout
|
|
1925
|
+
const linesegRegex = /<hp:lineseg[^>]*vertpos="(\d+)"[^>]*vertsize="(\d+)"[^>]*textheight="(\d+)"[^>]*baseline="(\d+)"[^>]*spacing="(\d+)"/g;
|
|
1926
|
+
let linesegMatch;
|
|
1927
|
+
const linesegs = [];
|
|
1928
|
+
while ((linesegMatch = linesegRegex.exec(xml)) !== null) {
|
|
1929
|
+
linesegs.push({
|
|
1930
|
+
vertpos: parseInt(linesegMatch[1]) / 100,
|
|
1931
|
+
vertsize: parseInt(linesegMatch[2]) / 100,
|
|
1932
|
+
textheight: parseInt(linesegMatch[3]) / 100,
|
|
1933
|
+
baseline: parseInt(linesegMatch[4]) / 100,
|
|
1934
|
+
spacing: parseInt(linesegMatch[5]) / 100,
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
if (linesegs.length > 0) {
|
|
1938
|
+
paragraph.linesegs = linesegs;
|
|
1939
|
+
}
|
|
1940
|
+
return paragraph;
|
|
1941
|
+
}
|
|
1942
|
+
static parseRun(xml) {
|
|
1943
|
+
const runs = [];
|
|
1944
|
+
let charStyle;
|
|
1945
|
+
const charShapeRefMatch = xml.match(/charPrIDRef="(\d+)"/);
|
|
1946
|
+
if (charShapeRefMatch) {
|
|
1947
|
+
const charShape = this.styles.charShapes.get(parseInt(charShapeRefMatch[1]));
|
|
1948
|
+
if (charShape) {
|
|
1949
|
+
charStyle = {
|
|
1950
|
+
fontName: charShape.fontName,
|
|
1951
|
+
fontSize: charShape.fontSize,
|
|
1952
|
+
bold: charShape.bold,
|
|
1953
|
+
italic: charShape.italic,
|
|
1954
|
+
underline: charShape.underline,
|
|
1955
|
+
underlineType: charShape.underlineType,
|
|
1956
|
+
underlineShape: charShape.underlineShape,
|
|
1957
|
+
underlineColor: charShape.underlineColor,
|
|
1958
|
+
strikethrough: charShape.strikethrough,
|
|
1959
|
+
strikeoutShape: charShape.strikeoutShape,
|
|
1960
|
+
strikeoutColor: charShape.strikeoutColor,
|
|
1961
|
+
fontColor: charShape.color,
|
|
1962
|
+
backgroundColor: charShape.backgroundColor,
|
|
1963
|
+
charSpacing: charShape.charSpacing,
|
|
1964
|
+
relativeSize: charShape.relativeSize,
|
|
1965
|
+
charOffset: charShape.charOffset,
|
|
1966
|
+
emphasisMark: charShape.emphasisMark,
|
|
1967
|
+
useFontSpace: charShape.useFontSpace,
|
|
1968
|
+
useKerning: charShape.useKerning,
|
|
1969
|
+
outline: charShape.outline,
|
|
1970
|
+
shadow: charShape.shadow,
|
|
1971
|
+
shadowX: charShape.shadowX,
|
|
1972
|
+
shadowY: charShape.shadowY,
|
|
1973
|
+
shadowColor: charShape.shadowColor,
|
|
1974
|
+
emboss: charShape.emboss,
|
|
1975
|
+
engrave: charShape.engrave,
|
|
1976
|
+
smallCaps: charShape.smallCaps,
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
let hyperlink;
|
|
1981
|
+
let field;
|
|
1982
|
+
const fieldBeginMatch = xml.match(/<hp:fieldBegin[^>]*type="([^"]*)"[^>]*>([\s\S]*?)<\/hp:fieldBegin>/i);
|
|
1983
|
+
if (fieldBeginMatch) {
|
|
1984
|
+
const fieldType = fieldBeginMatch[1].toUpperCase();
|
|
1985
|
+
const fieldContent = fieldBeginMatch[2];
|
|
1986
|
+
if (fieldType === 'HYPERLINK') {
|
|
1987
|
+
const paramMatch = fieldContent.match(/<hp:stringParam[^>]*name="URL"[^>]*>([^<]*)<\/hp:stringParam>/i);
|
|
1988
|
+
if (paramMatch) {
|
|
1989
|
+
hyperlink = {
|
|
1990
|
+
fieldType: 'hyperlink',
|
|
1991
|
+
url: this.decodeXmlEntities(paramMatch[1].trim()),
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
else {
|
|
1995
|
+
const commandMatch = fieldContent.match(/<hp:stringParam[^>]*name="Command"[^>]*>([^<]*)<\/hp:stringParam>/i);
|
|
1996
|
+
if (commandMatch) {
|
|
1997
|
+
const urlPart = commandMatch[1].split(';')[0] || commandMatch[1];
|
|
1998
|
+
hyperlink = {
|
|
1999
|
+
fieldType: 'hyperlink',
|
|
2000
|
+
url: this.decodeXmlEntities(urlPart.trim()),
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
else if (fieldType === 'MEMO') {
|
|
2006
|
+
const memoField = { fieldType: 'memo' };
|
|
2007
|
+
const authorMatch = fieldContent.match(/<hp:stringParam[^>]*name="Author"[^>]*>([^<]*)<\/hp:stringParam>/i);
|
|
2008
|
+
if (authorMatch)
|
|
2009
|
+
memoField.author = authorMatch[1];
|
|
2010
|
+
const dateMatch = fieldContent.match(/<hp:stringParam[^>]*name="CreateDateTime"[^>]*>([^<]*)<\/hp:stringParam>/i);
|
|
2011
|
+
if (dateMatch)
|
|
2012
|
+
memoField.date = dateMatch[1];
|
|
2013
|
+
const memoTextMatch = fieldContent.match(/<hp:subList[^>]*>[\s\S]*?<hp:t[^>]*>([^<]*)<\/hp:t>/i);
|
|
2014
|
+
if (memoTextMatch)
|
|
2015
|
+
memoField.memoContent = memoTextMatch[1];
|
|
2016
|
+
field = memoField;
|
|
2017
|
+
}
|
|
2018
|
+
else if (fieldType === 'FORMULA') {
|
|
2019
|
+
const formulaField = { fieldType: 'formula' };
|
|
2020
|
+
const scriptMatch = fieldContent.match(/<hp:stringParam[^>]*name="(?:Script|Command)"[^>]*>([^<]*)<\/hp:stringParam>/i);
|
|
2021
|
+
if (scriptMatch)
|
|
2022
|
+
formulaField.formulaScript = scriptMatch[1];
|
|
2023
|
+
field = formulaField;
|
|
2024
|
+
}
|
|
2025
|
+
else if (fieldType === 'BOOKMARK') {
|
|
2026
|
+
const bookmarkField = { fieldType: 'bookmark', bookmarkName: '' };
|
|
2027
|
+
const nameMatch = fieldContent.match(/<hp:stringParam[^>]*name="(?:Name|BookmarkName)"[^>]*>([^<]*)<\/hp:stringParam>/i);
|
|
2028
|
+
if (nameMatch)
|
|
2029
|
+
bookmarkField.bookmarkName = nameMatch[1];
|
|
2030
|
+
field = bookmarkField;
|
|
2031
|
+
}
|
|
2032
|
+
else {
|
|
2033
|
+
const fieldTypeMap = {
|
|
2034
|
+
'DATE': 'date', 'DOCDATE': 'docDate', 'PATH': 'path',
|
|
2035
|
+
'MAILMERGE': 'mailMerge', 'CROSSREF': 'crossRef', 'CLICKHERE': 'clickHere',
|
|
2036
|
+
'SUMMARY': 'summary', 'USERINFO': 'userInfo', 'REVISIONSIGN': 'revisionSign',
|
|
2037
|
+
'PRIVATETXT': 'privateTxt', 'TABLEOFCONTENTS': 'tableOfContents'
|
|
2038
|
+
};
|
|
2039
|
+
field = {
|
|
2040
|
+
fieldType: fieldTypeMap[fieldType] || 'unknown',
|
|
2041
|
+
name: fieldBeginMatch[0].match(/name="([^"]*)"/)?.[1],
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (!hyperlink) {
|
|
2046
|
+
const hyperlinkMatch = xml.match(/<hp:ctrl[^>]*>[\s\S]*?<hp:fieldBegin[^>]*type="HYPERLINK"[^>]*(?:param="([^"]*)")?/i);
|
|
2047
|
+
if (hyperlinkMatch) {
|
|
2048
|
+
const paramStr = hyperlinkMatch[1] || '';
|
|
2049
|
+
const urlMatch = paramStr.match(/url:([^;]*)/i) || paramStr.match(/^([^;]+)/);
|
|
2050
|
+
if (urlMatch) {
|
|
2051
|
+
hyperlink = {
|
|
2052
|
+
fieldType: 'hyperlink',
|
|
2053
|
+
url: this.decodeXmlEntities(urlMatch[1].trim()),
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
// Check if this run has a MEMO field and extract memo ID
|
|
2059
|
+
const hasMemo = /<hp:fieldBegin[^>]*type="MEMO"/i.test(xml);
|
|
2060
|
+
let memoId;
|
|
2061
|
+
if (hasMemo) {
|
|
2062
|
+
const memoIdMatch = xml.match(/<hp:fieldBegin[^>]*type="MEMO"[^>]*id="([^"]*)"/i);
|
|
2063
|
+
if (memoIdMatch)
|
|
2064
|
+
memoId = memoIdMatch[1];
|
|
2065
|
+
}
|
|
2066
|
+
// Remove MEMO field's subList content to prevent memo text from appearing in document body
|
|
2067
|
+
// Memo content is stored separately, not rendered as regular text
|
|
2068
|
+
// Always remove MEMO fieldBegin content regardless of field detection
|
|
2069
|
+
let textSearchXml = xml.replace(/<hp:fieldBegin[^>]*type="MEMO"[^>]*>[\s\S]*?<\/hp:fieldBegin>/gi, '');
|
|
2070
|
+
// Detect footnote/endnote references and extract their numbers for markers
|
|
2071
|
+
const footnoteMatch = xml.match(/<hp:footNote\b[^>]*number="(\d+)"[^>]*>/i);
|
|
2072
|
+
const endnoteMatch = xml.match(/<hp:endNote\b[^>]*number="(\d+)"[^>]*>/i);
|
|
2073
|
+
const footnoteNumber = footnoteMatch ? parseInt(footnoteMatch[1]) : null;
|
|
2074
|
+
const endnoteNumber = endnoteMatch ? parseInt(endnoteMatch[1]) : null;
|
|
2075
|
+
// Remove footnote/endnote content to prevent it from appearing in document body
|
|
2076
|
+
// Footnotes are parsed separately and displayed at the bottom of the page
|
|
2077
|
+
textSearchXml = textSearchXml.replace(/<hp:footNote\b[^>]*>[\s\S]*?<\/hp:footNote>/gi, '');
|
|
2078
|
+
textSearchXml = textSearchXml.replace(/<hp:endNote\b[^>]*>[\s\S]*?<\/hp:endNote>/gi, '');
|
|
2079
|
+
const allTextTagsRegex = /<hp:t(?:\s[^>]*)?>(?:([\s\S]*?)<\/hp:t>)?|<hp:t\s*\/>/g;
|
|
2080
|
+
let tMatch;
|
|
2081
|
+
let foundTextTags = false;
|
|
2082
|
+
while ((tMatch = allTextTagsRegex.exec(textSearchXml)) !== null) {
|
|
2083
|
+
foundTextTags = true;
|
|
2084
|
+
const tContent = tMatch[1] || '';
|
|
2085
|
+
this.processTextContent(tContent, charStyle, runs, hyperlink, field);
|
|
2086
|
+
}
|
|
2087
|
+
if (!foundTextTags) {
|
|
2088
|
+
return runs;
|
|
2089
|
+
}
|
|
2090
|
+
if (runs.length === 0) {
|
|
2091
|
+
runs.push({ text: '', charStyle, hyperlink, field });
|
|
2092
|
+
}
|
|
2093
|
+
// Mark all runs in this block as having memo if applicable
|
|
2094
|
+
if (hasMemo) {
|
|
2095
|
+
for (const run of runs) {
|
|
2096
|
+
run.hasMemo = true;
|
|
2097
|
+
if (memoId)
|
|
2098
|
+
run.memoId = memoId;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
// Add footnote/endnote marker as superscript at the end
|
|
2102
|
+
if (footnoteNumber !== null) {
|
|
2103
|
+
runs.push({
|
|
2104
|
+
text: `${footnoteNumber})`,
|
|
2105
|
+
footnoteRef: footnoteNumber,
|
|
2106
|
+
charStyle: { ...charStyle, superscript: true, fontSize: charStyle?.fontSize ? charStyle.fontSize * 0.7 : 7 },
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
if (endnoteNumber !== null) {
|
|
2110
|
+
runs.push({
|
|
2111
|
+
text: `${endnoteNumber})`,
|
|
2112
|
+
endnoteRef: endnoteNumber,
|
|
2113
|
+
charStyle: { ...charStyle, superscript: true, fontSize: charStyle?.fontSize ? charStyle.fontSize * 0.7 : 7 },
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
return runs;
|
|
2117
|
+
}
|
|
2118
|
+
static processTextContent(tContent, charStyle, runs, hyperlink, field) {
|
|
2119
|
+
// Combined regex to match all special elements including:
|
|
2120
|
+
// tab, lineBreak, hypen, nbSpace, fwSpace, titleMark, markPenBegin, markPenEnd,
|
|
2121
|
+
// autoNum, newNum, compose, dutmal, indexMark, pageHiding, pageNumCtrl, pageNum
|
|
2122
|
+
const specialElementRegex = /<hp:(tab|lineBreak|hypen|nbSpace|fwSpace|titleMark|markPenBegin|markPenEnd|autoNum|newNum|compose|dutmal|indexMark|pageHiding|pageNumCtrl|pageNum)(?:\s+([^>]*))?\s*(?:\/>|>([\s\S]*?)<\/hp:\1>)/gi;
|
|
2123
|
+
let lastIndex = 0;
|
|
2124
|
+
let specialMatch;
|
|
2125
|
+
let currentMarkPenColor;
|
|
2126
|
+
while ((specialMatch = specialElementRegex.exec(tContent)) !== null) {
|
|
2127
|
+
// Process text before this special element
|
|
2128
|
+
if (specialMatch.index > lastIndex) {
|
|
2129
|
+
const textBefore = tContent.substring(lastIndex, specialMatch.index);
|
|
2130
|
+
const cleanText = textBefore.replace(/<[^>]+>/g, '');
|
|
2131
|
+
if (cleanText) {
|
|
2132
|
+
const run = { text: this.decodeXmlEntities(cleanText), charStyle, hyperlink, field };
|
|
2133
|
+
if (currentMarkPenColor) {
|
|
2134
|
+
run.markPen = { color: currentMarkPenColor };
|
|
2135
|
+
}
|
|
2136
|
+
runs.push(run);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
const elementType = specialMatch[1].toLowerCase();
|
|
2140
|
+
const attrs = specialMatch[2] || '';
|
|
2141
|
+
switch (elementType) {
|
|
2142
|
+
case 'tab': {
|
|
2143
|
+
const widthMatch = attrs.match(/width="(\d+)"/);
|
|
2144
|
+
const width = widthMatch ? parseInt(widthMatch[1]) / 100 : 0;
|
|
2145
|
+
const leaderMatch = attrs.match(/leader="(\d+)"/);
|
|
2146
|
+
const leaderType = leaderMatch ? parseInt(leaderMatch[1]) : 0;
|
|
2147
|
+
// LineType2: 0=None, 1=Solid, 2=Dash, 3=Dot, 4=DashDot, 5=DashDotDot, 6=LongDash, 7=CircleDot
|
|
2148
|
+
let leader = 'none';
|
|
2149
|
+
if (leaderType === 1)
|
|
2150
|
+
leader = 'solid';
|
|
2151
|
+
else if (leaderType === 2)
|
|
2152
|
+
leader = 'dash';
|
|
2153
|
+
else if (leaderType === 3)
|
|
2154
|
+
leader = 'dot';
|
|
2155
|
+
else if (leaderType === 4)
|
|
2156
|
+
leader = 'dashDot';
|
|
2157
|
+
else if (leaderType === 5)
|
|
2158
|
+
leader = 'dashDotDot';
|
|
2159
|
+
runs.push({ text: '', charStyle, tab: { width, leader }, hyperlink, field });
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
case 'linebreak': {
|
|
2163
|
+
// Line break (강제 줄 나눔, SHIFT-ENTER)
|
|
2164
|
+
runs.push({ text: '\n', charStyle, hyperlink, field });
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
case 'hypen': {
|
|
2168
|
+
// Soft hyphen (하이픈, CTRL-SHIFT-'-')
|
|
2169
|
+
runs.push({ text: '\u00AD', charStyle, hyperlink, field });
|
|
2170
|
+
break;
|
|
2171
|
+
}
|
|
2172
|
+
case 'nbspace': {
|
|
2173
|
+
// Non-breaking space (묶음 빈칸, CTRL-ALT-SPACE)
|
|
2174
|
+
runs.push({ text: '\u00A0', charStyle, hyperlink, field });
|
|
2175
|
+
break;
|
|
2176
|
+
}
|
|
2177
|
+
case 'fwspace': {
|
|
2178
|
+
// Full-width space (고정폭 빈칸, ALT-SPACE)
|
|
2179
|
+
runs.push({ text: '\u3000', charStyle, hyperlink, field });
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
2182
|
+
case 'titlemark': {
|
|
2183
|
+
// Title mark (제목 차례 표시) - just mark it, no visible text
|
|
2184
|
+
const ignoreMatch = attrs.match(/ignore="(true|false|1|0)"/i);
|
|
2185
|
+
const ignore = ignoreMatch ? (ignoreMatch[1] === 'true' || ignoreMatch[1] === '1') : false;
|
|
2186
|
+
// Title mark doesn't produce visible text but marks the title for TOC
|
|
2187
|
+
runs.push({ text: '', charStyle, hyperlink, field });
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
case 'markpenbegin': {
|
|
2191
|
+
// Highlight/mark pen start (형광펜 시작)
|
|
2192
|
+
const colorMatch = attrs.match(/color="([^"]*)"/);
|
|
2193
|
+
currentMarkPenColor = colorMatch ? colorMatch[1] : '#FFFF00'; // default yellow
|
|
2194
|
+
break;
|
|
2195
|
+
}
|
|
2196
|
+
case 'markpenend': {
|
|
2197
|
+
// Highlight/mark pen end (형광펜 끝)
|
|
2198
|
+
currentMarkPenColor = undefined;
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
case 'autonum': {
|
|
2202
|
+
// Auto number (자동번호 - 각주, 표, 그림 등의 자동 번호)
|
|
2203
|
+
const numTypeMatch = attrs.match(/numType="([^"]*)"/);
|
|
2204
|
+
const numType = numTypeMatch ? numTypeMatch[1] : 'Page';
|
|
2205
|
+
// AutoNum generates a number based on context, we just mark its presence
|
|
2206
|
+
runs.push({ text: '', charStyle, hyperlink, field });
|
|
2207
|
+
break;
|
|
2208
|
+
}
|
|
2209
|
+
case 'newnum': {
|
|
2210
|
+
// New number (새 번호 지정)
|
|
2211
|
+
const numTypeMatch = attrs.match(/numType="([^"]*)"/);
|
|
2212
|
+
const numMatch = attrs.match(/num="(\d+)"/);
|
|
2213
|
+
// NewNum resets the numbering
|
|
2214
|
+
runs.push({ text: '', charStyle, hyperlink, field });
|
|
2215
|
+
break;
|
|
2216
|
+
}
|
|
2217
|
+
case 'compose': {
|
|
2218
|
+
// Compose (글자 겹침)
|
|
2219
|
+
const innerContent = specialMatch[3] || '';
|
|
2220
|
+
// Compose overlaps characters - we extract the text content
|
|
2221
|
+
const composeText = innerContent.replace(/<[^>]+>/g, '');
|
|
2222
|
+
if (composeText) {
|
|
2223
|
+
runs.push({ text: this.decodeXmlEntities(composeText), charStyle, hyperlink, field });
|
|
2224
|
+
}
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
case 'dutmal': {
|
|
2228
|
+
// Dutmal (덧말/루비)
|
|
2229
|
+
const mainTextMatch = attrs.match(/mainText="([^"]*)"/);
|
|
2230
|
+
const subTextMatch = attrs.match(/subText="([^"]*)"/);
|
|
2231
|
+
const mainText = mainTextMatch ? mainTextMatch[1] : '';
|
|
2232
|
+
const subText = subTextMatch ? subTextMatch[1] : '';
|
|
2233
|
+
// For now, just output the main text (subText is annotation above/below)
|
|
2234
|
+
if (mainText) {
|
|
2235
|
+
runs.push({ text: this.decodeXmlEntities(mainText), charStyle, hyperlink, field });
|
|
2236
|
+
}
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
case 'indexmark': {
|
|
2240
|
+
// Index mark (찾아보기 표식)
|
|
2241
|
+
const keyFirstMatch = attrs.match(/keyFirst="([^"]*)"/);
|
|
2242
|
+
const keySecondMatch = attrs.match(/keySecond="([^"]*)"/);
|
|
2243
|
+
// Index mark is invisible but marks text for index
|
|
2244
|
+
runs.push({ text: '', charStyle, hyperlink, field });
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
case 'pagehiding': {
|
|
2248
|
+
// Page hiding (감추기)
|
|
2249
|
+
// This is a control element, doesn't produce visible text
|
|
2250
|
+
runs.push({ text: '', charStyle, hyperlink, field });
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2253
|
+
case 'pagenumctrl': {
|
|
2254
|
+
// Page number control (쪽 번호 컨트롤)
|
|
2255
|
+
// This is a control element for page number settings
|
|
2256
|
+
runs.push({ text: '', charStyle, hyperlink, field });
|
|
2257
|
+
break;
|
|
2258
|
+
}
|
|
2259
|
+
case 'pagenum': {
|
|
2260
|
+
// Page number (쪽 번호)
|
|
2261
|
+
// This displays the page number - we use a placeholder
|
|
2262
|
+
runs.push({ text: '#', charStyle, hyperlink, field });
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
lastIndex = specialMatch.index + specialMatch[0].length;
|
|
2267
|
+
}
|
|
2268
|
+
// Process remaining text after last special element
|
|
2269
|
+
if (lastIndex < tContent.length) {
|
|
2270
|
+
const remainingText = tContent.substring(lastIndex);
|
|
2271
|
+
const cleanText = remainingText.replace(/<[^>]+>/g, '');
|
|
2272
|
+
if (cleanText) {
|
|
2273
|
+
const run = { text: this.decodeXmlEntities(cleanText), charStyle, hyperlink, field };
|
|
2274
|
+
if (currentMarkPenColor) {
|
|
2275
|
+
run.markPen = { color: currentMarkPenColor };
|
|
2276
|
+
}
|
|
2277
|
+
runs.push(run);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
else if (lastIndex === 0 && tContent.length > 0) {
|
|
2281
|
+
const cleanText = tContent.replace(/<[^>]+>/g, '');
|
|
2282
|
+
if (cleanText) {
|
|
2283
|
+
runs.push({ text: this.decodeXmlEntities(cleanText), charStyle, hyperlink, field });
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
static parseTable(xml) {
|
|
2288
|
+
const table = {
|
|
2289
|
+
id: generateId(),
|
|
2290
|
+
rows: [],
|
|
2291
|
+
columnWidths: [],
|
|
2292
|
+
};
|
|
2293
|
+
const tblTagMatch = xml.match(/<hp:tbl[^>]*>/);
|
|
2294
|
+
if (tblTagMatch) {
|
|
2295
|
+
const tblAttrs = tblTagMatch[0];
|
|
2296
|
+
const idMatch = tblAttrs.match(/\bid="([^"]+)"/);
|
|
2297
|
+
if (idMatch)
|
|
2298
|
+
table.id = idMatch[1];
|
|
2299
|
+
const zOrderMatch = tblAttrs.match(/zOrder="(\d+)"/);
|
|
2300
|
+
if (zOrderMatch)
|
|
2301
|
+
table.zOrder = parseInt(zOrderMatch[1]);
|
|
2302
|
+
const numTypeMatch = tblAttrs.match(/numberingType="([^"]*)"/);
|
|
2303
|
+
if (numTypeMatch) {
|
|
2304
|
+
const map = {
|
|
2305
|
+
'NONE': 'none', 'PICTURE': 'picture', 'TABLE': 'table', 'EQUATION': 'equation'
|
|
2306
|
+
};
|
|
2307
|
+
table.numberingType = map[numTypeMatch[1].toUpperCase()] || 'none';
|
|
2308
|
+
}
|
|
2309
|
+
const textWrapMatch = tblAttrs.match(/textWrap="([^"]*)"/);
|
|
2310
|
+
if (textWrapMatch) {
|
|
2311
|
+
const map = {
|
|
2312
|
+
'SQUARE': 'square', 'TIGHT': 'tight', 'THROUGH': 'through',
|
|
2313
|
+
'TOP_AND_BOTTOM': 'topAndBottom', 'BEHIND_TEXT': 'behindText', 'IN_FRONT_OF_TEXT': 'inFrontOfText'
|
|
2314
|
+
};
|
|
2315
|
+
table.textWrap = map[textWrapMatch[1].toUpperCase()] || 'square';
|
|
2316
|
+
}
|
|
2317
|
+
const textFlowMatch = tblAttrs.match(/textFlow="([^"]*)"/);
|
|
2318
|
+
if (textFlowMatch) {
|
|
2319
|
+
const map = {
|
|
2320
|
+
'BOTH_SIDES': 'bothSides', 'LEFT_ONLY': 'leftOnly', 'RIGHT_ONLY': 'rightOnly', 'LARGEST_ONLY': 'largestOnly'
|
|
2321
|
+
};
|
|
2322
|
+
table.textFlow = map[textFlowMatch[1].toUpperCase()] || 'bothSides';
|
|
2323
|
+
}
|
|
2324
|
+
const pageBreakMatch = tblAttrs.match(/pageBreak="([^"]*)"/);
|
|
2325
|
+
if (pageBreakMatch) {
|
|
2326
|
+
const map = {
|
|
2327
|
+
'CELL': 'cell', 'NONE': 'none', 'TABLE': 'table'
|
|
2328
|
+
};
|
|
2329
|
+
table.pageBreak = map[pageBreakMatch[1].toUpperCase()] || 'none';
|
|
2330
|
+
}
|
|
2331
|
+
const repeatHeaderMatch = tblAttrs.match(/repeatHeader="([^"]*)"/);
|
|
2332
|
+
if (repeatHeaderMatch) {
|
|
2333
|
+
table.repeatHeader = repeatHeaderMatch[1] === '1' || repeatHeaderMatch[1] === 'true';
|
|
2334
|
+
}
|
|
2335
|
+
const rowCntMatch = tblAttrs.match(/rowCnt="(\d+)"/);
|
|
2336
|
+
if (rowCntMatch)
|
|
2337
|
+
table.rowCnt = parseInt(rowCntMatch[1]);
|
|
2338
|
+
const colCntMatch = tblAttrs.match(/colCnt="(\d+)"/);
|
|
2339
|
+
if (colCntMatch)
|
|
2340
|
+
table.colCnt = parseInt(colCntMatch[1]);
|
|
2341
|
+
const lockMatch = tblAttrs.match(/lock="([^"]*)"/);
|
|
2342
|
+
if (lockMatch) {
|
|
2343
|
+
table.lock = lockMatch[1] === '1' || lockMatch[1] === 'true';
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
const szMatch = xml.match(/<hp:sz\s+width="(\d+)"[^>]*height="(\d+)"/);
|
|
2347
|
+
if (szMatch) {
|
|
2348
|
+
table.width = parseInt(szMatch[1]) / 100;
|
|
2349
|
+
table.height = parseInt(szMatch[2]) / 100;
|
|
2350
|
+
}
|
|
2351
|
+
const cellSpacingMatch = xml.match(/cellSpacing="(\d+)"/);
|
|
2352
|
+
if (cellSpacingMatch) {
|
|
2353
|
+
table.cellSpacing = parseInt(cellSpacingMatch[1]) / 100;
|
|
2354
|
+
}
|
|
2355
|
+
const borderFillMatch = xml.match(/borderFillIDRef="(\d+)"/);
|
|
2356
|
+
if (borderFillMatch) {
|
|
2357
|
+
table.borderFillId = parseInt(borderFillMatch[1]);
|
|
2358
|
+
}
|
|
2359
|
+
// Parse CellZoneList
|
|
2360
|
+
const cellZoneListMatch = xml.match(/<hp:cellzoneList[^>]*>([\s\S]*?)<\/hp:cellzoneList>/i);
|
|
2361
|
+
if (cellZoneListMatch) {
|
|
2362
|
+
const cellZones = [];
|
|
2363
|
+
const cellZoneRegex = /<hp:cellzone[^>]*startRowAddr="(\d+)"[^>]*startColAddr="(\d+)"[^>]*endRowAddr="(\d+)"[^>]*endColAddr="(\d+)"(?:[^>]*borderFillIDRef="(\d+)")?/gi;
|
|
2364
|
+
let czMatch;
|
|
2365
|
+
while ((czMatch = cellZoneRegex.exec(cellZoneListMatch[1])) !== null) {
|
|
2366
|
+
const cellZone = {
|
|
2367
|
+
startRowAddr: parseInt(czMatch[1]),
|
|
2368
|
+
startColAddr: parseInt(czMatch[2]),
|
|
2369
|
+
endRowAddr: parseInt(czMatch[3]),
|
|
2370
|
+
endColAddr: parseInt(czMatch[4])
|
|
2371
|
+
};
|
|
2372
|
+
if (czMatch[5])
|
|
2373
|
+
cellZone.borderFill = parseInt(czMatch[5]);
|
|
2374
|
+
cellZones.push(cellZone);
|
|
2375
|
+
}
|
|
2376
|
+
if (cellZones.length > 0) {
|
|
2377
|
+
table.cellZoneList = cellZones;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
const posMatch = xml.match(/<hp:pos[^>]*>/);
|
|
2381
|
+
if (posMatch) {
|
|
2382
|
+
const pos = posMatch[0];
|
|
2383
|
+
table.position = {};
|
|
2384
|
+
if (pos.includes('treatAsChar="1"') || pos.includes('treatAsChar="true"')) {
|
|
2385
|
+
table.position.treatAsChar = true;
|
|
2386
|
+
}
|
|
2387
|
+
if (pos.includes('flowWithText="1"') || pos.includes('flowWithText="true"')) {
|
|
2388
|
+
table.position.flowWithText = true;
|
|
2389
|
+
}
|
|
2390
|
+
const vertRelMatch = pos.match(/vertRelTo="([^"]*)"/);
|
|
2391
|
+
if (vertRelMatch) {
|
|
2392
|
+
const map = {
|
|
2393
|
+
'PAPER': 'paper', 'PAGE': 'page', 'PARA': 'para'
|
|
2394
|
+
};
|
|
2395
|
+
table.position.vertRelTo = map[vertRelMatch[1].toUpperCase()];
|
|
2396
|
+
}
|
|
2397
|
+
const horzRelMatch = pos.match(/horzRelTo="([^"]*)"/);
|
|
2398
|
+
if (horzRelMatch) {
|
|
2399
|
+
const map = {
|
|
2400
|
+
'PAPER': 'paper', 'PAGE': 'page', 'COLUMN': 'column', 'PARA': 'para'
|
|
2401
|
+
};
|
|
2402
|
+
table.position.horzRelTo = map[horzRelMatch[1].toUpperCase()];
|
|
2403
|
+
}
|
|
2404
|
+
const vertAlignMatch = pos.match(/vertAlign="([^"]*)"/);
|
|
2405
|
+
if (vertAlignMatch) {
|
|
2406
|
+
const map = {
|
|
2407
|
+
'TOP': 'top', 'CENTER': 'center', 'BOTTOM': 'bottom'
|
|
2408
|
+
};
|
|
2409
|
+
table.position.vertAlign = map[vertAlignMatch[1].toUpperCase()];
|
|
2410
|
+
}
|
|
2411
|
+
const horzAlignMatch = pos.match(/horzAlign="([^"]*)"/);
|
|
2412
|
+
if (horzAlignMatch) {
|
|
2413
|
+
const map = {
|
|
2414
|
+
'LEFT': 'left', 'CENTER': 'center', 'RIGHT': 'right'
|
|
2415
|
+
};
|
|
2416
|
+
table.position.horzAlign = map[horzAlignMatch[1].toUpperCase()];
|
|
2417
|
+
}
|
|
2418
|
+
const vertOffsetMatch = pos.match(/vertOffset="(-?\d+)"/);
|
|
2419
|
+
if (vertOffsetMatch) {
|
|
2420
|
+
table.position.vertOffset = parseInt(vertOffsetMatch[1]) / 100;
|
|
2421
|
+
}
|
|
2422
|
+
const horzOffsetMatch = pos.match(/horzOffset="(-?\d+)"/);
|
|
2423
|
+
if (horzOffsetMatch) {
|
|
2424
|
+
table.position.horzOffset = parseInt(horzOffsetMatch[1]) / 100;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
// Parse outMargin - handle attributes in any order
|
|
2428
|
+
const outMarginTagMatch = xml.match(/<hp:outMargin\s+([^>]*)\/?\s*>/);
|
|
2429
|
+
if (outMarginTagMatch) {
|
|
2430
|
+
const attrs = outMarginTagMatch[1];
|
|
2431
|
+
const leftMatch = attrs.match(/left="(\d+)"/);
|
|
2432
|
+
const rightMatch = attrs.match(/right="(\d+)"/);
|
|
2433
|
+
const topMatch = attrs.match(/top="(\d+)"/);
|
|
2434
|
+
const bottomMatch = attrs.match(/bottom="(\d+)"/);
|
|
2435
|
+
table.outMargin = {
|
|
2436
|
+
left: leftMatch ? parseInt(leftMatch[1]) / 100 : 0,
|
|
2437
|
+
right: rightMatch ? parseInt(rightMatch[1]) / 100 : 0,
|
|
2438
|
+
top: topMatch ? parseInt(topMatch[1]) / 100 : 0,
|
|
2439
|
+
bottom: bottomMatch ? parseInt(bottomMatch[1]) / 100 : 0
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
// Parse inMargin - handle attributes in any order
|
|
2443
|
+
const inMarginTagMatch = xml.match(/<hp:inMargin\s+([^>]*)\/?\s*>/);
|
|
2444
|
+
if (inMarginTagMatch) {
|
|
2445
|
+
const attrs = inMarginTagMatch[1];
|
|
2446
|
+
const leftMatch = attrs.match(/left="(\d+)"/);
|
|
2447
|
+
const rightMatch = attrs.match(/right="(\d+)"/);
|
|
2448
|
+
const topMatch = attrs.match(/top="(\d+)"/);
|
|
2449
|
+
const bottomMatch = attrs.match(/bottom="(\d+)"/);
|
|
2450
|
+
table.inMargin = {
|
|
2451
|
+
left: leftMatch ? parseInt(leftMatch[1]) / 100 : 0,
|
|
2452
|
+
right: rightMatch ? parseInt(rightMatch[1]) / 100 : 0,
|
|
2453
|
+
top: topMatch ? parseInt(topMatch[1]) / 100 : 0,
|
|
2454
|
+
bottom: bottomMatch ? parseInt(bottomMatch[1]) / 100 : 0
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
const colWidthsMatch = xml.match(/<hp:colSz[^>]*>([\s\S]*?)<\/hp:colSz>/);
|
|
2458
|
+
if (colWidthsMatch) {
|
|
2459
|
+
const widthRegex = /(\d+)/g;
|
|
2460
|
+
let widthMatch;
|
|
2461
|
+
while ((widthMatch = widthRegex.exec(colWidthsMatch[1])) !== null) {
|
|
2462
|
+
table.columnWidths.push(parseInt(widthMatch[1]) / 100);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
const rows = this.extractBalancedTags(xml, 'hp:tr');
|
|
2466
|
+
for (const rowXml of rows) {
|
|
2467
|
+
const row = this.parseTableRow(rowXml);
|
|
2468
|
+
table.rows.push(row);
|
|
2469
|
+
}
|
|
2470
|
+
return table;
|
|
2471
|
+
}
|
|
2472
|
+
static parseTableRow(xml) {
|
|
2473
|
+
const row = { cells: [] };
|
|
2474
|
+
const heightMatch = xml.match(/height="(\d+)"/);
|
|
2475
|
+
if (heightMatch) {
|
|
2476
|
+
row.height = parseInt(heightMatch[1]) / 100;
|
|
2477
|
+
}
|
|
2478
|
+
const cells = this.extractBalancedTags(xml, 'hp:tc');
|
|
2479
|
+
for (const cellXml of cells) {
|
|
2480
|
+
const cell = this.parseTableCell(cellXml);
|
|
2481
|
+
row.cells.push(cell);
|
|
2482
|
+
}
|
|
2483
|
+
return row;
|
|
2484
|
+
}
|
|
2485
|
+
static extractBalancedTags(xml, tagName) {
|
|
2486
|
+
const results = [];
|
|
2487
|
+
const openTag = `<${tagName}`;
|
|
2488
|
+
const closeTag = `</${tagName}>`;
|
|
2489
|
+
let pos = 0;
|
|
2490
|
+
while (pos < xml.length) {
|
|
2491
|
+
const startIdx = xml.indexOf(openTag, pos);
|
|
2492
|
+
if (startIdx === -1)
|
|
2493
|
+
break;
|
|
2494
|
+
let depth = 1;
|
|
2495
|
+
let searchPos = startIdx + openTag.length;
|
|
2496
|
+
while (depth > 0 && searchPos < xml.length) {
|
|
2497
|
+
const nextOpen = xml.indexOf(openTag, searchPos);
|
|
2498
|
+
const nextClose = xml.indexOf(closeTag, searchPos);
|
|
2499
|
+
if (nextClose === -1)
|
|
2500
|
+
break;
|
|
2501
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
2502
|
+
depth++;
|
|
2503
|
+
searchPos = nextOpen + openTag.length;
|
|
2504
|
+
}
|
|
2505
|
+
else {
|
|
2506
|
+
depth--;
|
|
2507
|
+
if (depth === 0) {
|
|
2508
|
+
results.push(xml.substring(startIdx, nextClose + closeTag.length));
|
|
2509
|
+
}
|
|
2510
|
+
searchPos = nextClose + closeTag.length;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
pos = searchPos;
|
|
2514
|
+
}
|
|
2515
|
+
return results;
|
|
2516
|
+
}
|
|
2517
|
+
// Extract ALL paragraphs including nested ones (not just top-level)
|
|
2518
|
+
static extractAllParagraphs(xml) {
|
|
2519
|
+
const results = [];
|
|
2520
|
+
const closeTag = '</hp:p>';
|
|
2521
|
+
const pOpenRegex = /<hp:p\b[^>]*>/g;
|
|
2522
|
+
// Regex to find opening paragraph tags (must be followed by space, >, or end of attributes)
|
|
2523
|
+
const pOpenSearchRegex = /<hp:p[\s>]/g;
|
|
2524
|
+
let match;
|
|
2525
|
+
while ((match = pOpenRegex.exec(xml)) !== null) {
|
|
2526
|
+
const startPos = match.index;
|
|
2527
|
+
// Find matching close tag using depth tracking
|
|
2528
|
+
let depth = 1;
|
|
2529
|
+
let searchPos = startPos + match[0].length;
|
|
2530
|
+
while (depth > 0 && searchPos < xml.length) {
|
|
2531
|
+
// Find next paragraph opening tag (not other hp:p* tags like hp:pagePr)
|
|
2532
|
+
pOpenSearchRegex.lastIndex = searchPos;
|
|
2533
|
+
const nextOpenMatch = pOpenSearchRegex.exec(xml);
|
|
2534
|
+
const nextOpen = nextOpenMatch ? nextOpenMatch.index : -1;
|
|
2535
|
+
const nextClose = xml.indexOf(closeTag, searchPos);
|
|
2536
|
+
if (nextClose === -1)
|
|
2537
|
+
break;
|
|
2538
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
2539
|
+
depth++;
|
|
2540
|
+
searchPos = nextOpen + 6; // Move past '<hp:p ' or '<hp:p>'
|
|
2541
|
+
}
|
|
2542
|
+
else {
|
|
2543
|
+
depth--;
|
|
2544
|
+
if (depth === 0) {
|
|
2545
|
+
const endPos = nextClose + closeTag.length;
|
|
2546
|
+
results.push({
|
|
2547
|
+
xml: xml.substring(startPos, endPos),
|
|
2548
|
+
start: startPos,
|
|
2549
|
+
end: endPos
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
searchPos = nextClose + closeTag.length;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return results;
|
|
2557
|
+
}
|
|
2558
|
+
static parseTableCell(xml) {
|
|
2559
|
+
const cell = { paragraphs: [] };
|
|
2560
|
+
const tcTagMatch = xml.match(/<hp:tc[^>]*>/);
|
|
2561
|
+
if (tcTagMatch) {
|
|
2562
|
+
const tcAttrs = tcTagMatch[0];
|
|
2563
|
+
// Try to get rowAddr/colAddr from tc attributes first
|
|
2564
|
+
let rowAddrMatch = tcAttrs.match(/rowAddr="(\d+)"/);
|
|
2565
|
+
let colAddrMatch = tcAttrs.match(/colAddr="(\d+)"/);
|
|
2566
|
+
// If not in tc attributes, try cellAddr element
|
|
2567
|
+
if (!rowAddrMatch || !colAddrMatch) {
|
|
2568
|
+
const cellAddrMatch = xml.match(/<hp:cellAddr[^>]*colAddr="(\d+)"[^>]*rowAddr="(\d+)"/);
|
|
2569
|
+
if (cellAddrMatch) {
|
|
2570
|
+
cell.colAddr = parseInt(cellAddrMatch[1]);
|
|
2571
|
+
cell.rowAddr = parseInt(cellAddrMatch[2]);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
else {
|
|
2575
|
+
if (rowAddrMatch)
|
|
2576
|
+
cell.rowAddr = parseInt(rowAddrMatch[1]);
|
|
2577
|
+
if (colAddrMatch)
|
|
2578
|
+
cell.colAddr = parseInt(colAddrMatch[1]);
|
|
2579
|
+
}
|
|
2580
|
+
const headerMatch = tcAttrs.match(/header="([^"]*)"/);
|
|
2581
|
+
if (headerMatch) {
|
|
2582
|
+
cell.header = headerMatch[1] === '1' || headerMatch[1] === 'true';
|
|
2583
|
+
}
|
|
2584
|
+
const protectMatch = tcAttrs.match(/protect="([^"]*)"/);
|
|
2585
|
+
if (protectMatch) {
|
|
2586
|
+
cell.protect = protectMatch[1] === '1' || protectMatch[1] === 'true';
|
|
2587
|
+
}
|
|
2588
|
+
const editableMatch = tcAttrs.match(/editable="([^"]*)"/);
|
|
2589
|
+
if (editableMatch) {
|
|
2590
|
+
cell.editable = editableMatch[1] === '1' || editableMatch[1] === 'true';
|
|
2591
|
+
}
|
|
2592
|
+
// hasMargin="0" means use table's inMargin instead of cell's own margin
|
|
2593
|
+
const hasMarginMatch = tcAttrs.match(/hasMargin="([^"]*)"/);
|
|
2594
|
+
if (hasMarginMatch) {
|
|
2595
|
+
cell.hasMargin = hasMarginMatch[1] === '1' || hasMarginMatch[1] === 'true';
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
// textDirection and lineWrap can be in tc tag or subList element
|
|
2599
|
+
const subListMatch = xml.match(/<hp:subList[^>]*>/);
|
|
2600
|
+
const textDirSource = subListMatch ? subListMatch[0] : (tcTagMatch ? tcTagMatch[0] : '');
|
|
2601
|
+
const textDirMatch = textDirSource.match(/textDirection="([^"]*)"/);
|
|
2602
|
+
if (textDirMatch) {
|
|
2603
|
+
const dir = textDirMatch[1].toUpperCase();
|
|
2604
|
+
if (dir === 'VERTICAL' || dir === 'VERT') {
|
|
2605
|
+
cell.textDirection = 'vertical';
|
|
2606
|
+
}
|
|
2607
|
+
else {
|
|
2608
|
+
cell.textDirection = 'horizontal';
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
const lineWrapMatch = textDirSource.match(/lineWrap="([^"]*)"/);
|
|
2612
|
+
if (lineWrapMatch) {
|
|
2613
|
+
const wrapMap = {
|
|
2614
|
+
'BREAK': 'break', 'SQUEEZE': 'squeeze', 'KEEP': 'keep'
|
|
2615
|
+
};
|
|
2616
|
+
cell.lineWrap = wrapMap[lineWrapMatch[1].toUpperCase()] || 'break';
|
|
2617
|
+
}
|
|
2618
|
+
// Get vertAlign from subList if not already found
|
|
2619
|
+
if (subListMatch) {
|
|
2620
|
+
const vertAlignMatch = subListMatch[0].match(/vertAlign="([^"]*)"/);
|
|
2621
|
+
if (vertAlignMatch && !cell.verticalAlign) {
|
|
2622
|
+
const align = vertAlignMatch[1].toLowerCase();
|
|
2623
|
+
if (align === 'center' || align === 'middle') {
|
|
2624
|
+
cell.verticalAlign = 'middle';
|
|
2625
|
+
}
|
|
2626
|
+
else if (align === 'bottom') {
|
|
2627
|
+
cell.verticalAlign = 'bottom';
|
|
2628
|
+
}
|
|
2629
|
+
else {
|
|
2630
|
+
cell.verticalAlign = 'top';
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
// Parse cellSz - handle attributes in any order
|
|
2635
|
+
const cellSzTagMatch = xml.match(/<hp:cellSz\s+([^>]*)\/?\s*>/);
|
|
2636
|
+
if (cellSzTagMatch) {
|
|
2637
|
+
const attrs = cellSzTagMatch[1];
|
|
2638
|
+
const widthMatch = attrs.match(/width="(\d+)"/);
|
|
2639
|
+
const heightMatch = attrs.match(/height="(\d+)"/);
|
|
2640
|
+
if (widthMatch)
|
|
2641
|
+
cell.width = parseInt(widthMatch[1]) / 100;
|
|
2642
|
+
if (heightMatch)
|
|
2643
|
+
cell.height = parseInt(heightMatch[1]) / 100;
|
|
2644
|
+
}
|
|
2645
|
+
// Parse cellSpan - handle attributes in any order
|
|
2646
|
+
const cellSpanTagMatch = xml.match(/<hp:cellSpan\s+([^>]*)\/?\s*>/);
|
|
2647
|
+
if (cellSpanTagMatch) {
|
|
2648
|
+
const attrs = cellSpanTagMatch[1];
|
|
2649
|
+
const colSpanMatch = attrs.match(/colSpan="(\d+)"/);
|
|
2650
|
+
const rowSpanMatch = attrs.match(/rowSpan="(\d+)"/);
|
|
2651
|
+
if (colSpanMatch)
|
|
2652
|
+
cell.colSpan = parseInt(colSpanMatch[1]);
|
|
2653
|
+
if (rowSpanMatch)
|
|
2654
|
+
cell.rowSpan = parseInt(rowSpanMatch[1]);
|
|
2655
|
+
}
|
|
2656
|
+
else {
|
|
2657
|
+
// Fallback: check for individual attributes
|
|
2658
|
+
const rowSpanMatch = xml.match(/rowSpan="(\d+)"/);
|
|
2659
|
+
if (rowSpanMatch)
|
|
2660
|
+
cell.rowSpan = parseInt(rowSpanMatch[1]);
|
|
2661
|
+
const colSpanMatch = xml.match(/colSpan="(\d+)"/);
|
|
2662
|
+
if (colSpanMatch)
|
|
2663
|
+
cell.colSpan = parseInt(colSpanMatch[1]);
|
|
2664
|
+
}
|
|
2665
|
+
// Parse cellMargin - handle attributes in any order
|
|
2666
|
+
const cellMarginTagMatch = xml.match(/<hp:cellMargin\s+([^>]*)\/?\s*>/);
|
|
2667
|
+
if (cellMarginTagMatch) {
|
|
2668
|
+
const attrs = cellMarginTagMatch[1];
|
|
2669
|
+
const leftMatch = attrs.match(/left="(\d+)"/);
|
|
2670
|
+
const rightMatch = attrs.match(/right="(\d+)"/);
|
|
2671
|
+
const topMatch = attrs.match(/top="(\d+)"/);
|
|
2672
|
+
const bottomMatch = attrs.match(/bottom="(\d+)"/);
|
|
2673
|
+
if (leftMatch)
|
|
2674
|
+
cell.marginLeft = parseInt(leftMatch[1]) / 100;
|
|
2675
|
+
if (rightMatch)
|
|
2676
|
+
cell.marginRight = parseInt(rightMatch[1]) / 100;
|
|
2677
|
+
if (topMatch)
|
|
2678
|
+
cell.marginTop = parseInt(topMatch[1]) / 100;
|
|
2679
|
+
if (bottomMatch)
|
|
2680
|
+
cell.marginBottom = parseInt(bottomMatch[1]) / 100;
|
|
2681
|
+
}
|
|
2682
|
+
const vertAlignMatch = xml.match(/vertAlign="([^"]*)"/);
|
|
2683
|
+
if (vertAlignMatch) {
|
|
2684
|
+
const align = vertAlignMatch[1].toLowerCase();
|
|
2685
|
+
if (align === 'center' || align === 'middle') {
|
|
2686
|
+
cell.verticalAlign = 'middle';
|
|
2687
|
+
}
|
|
2688
|
+
else if (align === 'bottom') {
|
|
2689
|
+
cell.verticalAlign = 'bottom';
|
|
2690
|
+
}
|
|
2691
|
+
else {
|
|
2692
|
+
cell.verticalAlign = 'top';
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
const borderFillRefMatch = xml.match(/borderFillIDRef="(\d+)"/);
|
|
2696
|
+
if (borderFillRefMatch) {
|
|
2697
|
+
const borderFillId = parseInt(borderFillRefMatch[1]);
|
|
2698
|
+
cell.borderFillId = borderFillId;
|
|
2699
|
+
const borderFill = this.styles.borderFills.get(borderFillId);
|
|
2700
|
+
if (borderFill) {
|
|
2701
|
+
if (borderFill.fillColor) {
|
|
2702
|
+
cell.backgroundColor = borderFill.fillColor;
|
|
2703
|
+
}
|
|
2704
|
+
// Add gradation support
|
|
2705
|
+
if (borderFill.gradation && borderFill.gradation.colors.length > 0) {
|
|
2706
|
+
cell.backgroundGradation = {
|
|
2707
|
+
type: borderFill.gradation.type,
|
|
2708
|
+
angle: borderFill.gradation.angle,
|
|
2709
|
+
colors: borderFill.gradation.colors,
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
if (borderFill.leftBorder) {
|
|
2713
|
+
cell.borderLeft = borderFill.leftBorder;
|
|
2714
|
+
}
|
|
2715
|
+
if (borderFill.rightBorder) {
|
|
2716
|
+
cell.borderRight = borderFill.rightBorder;
|
|
2717
|
+
}
|
|
2718
|
+
if (borderFill.topBorder) {
|
|
2719
|
+
cell.borderTop = borderFill.topBorder;
|
|
2720
|
+
}
|
|
2721
|
+
if (borderFill.bottomBorder) {
|
|
2722
|
+
cell.borderBottom = borderFill.bottomBorder;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
if (!cell.backgroundColor) {
|
|
2727
|
+
const bgColorMatch = xml.match(/faceColor="([^"]*)"/);
|
|
2728
|
+
if (bgColorMatch && bgColorMatch[1] !== 'none') {
|
|
2729
|
+
cell.backgroundColor = bgColorMatch[1];
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
const subLists = this.extractBalancedTags(xml, 'hp:subList');
|
|
2733
|
+
const contentXml = subLists.length > 0
|
|
2734
|
+
? subLists[0].replace(/^<hp:subList[^>]*>/, '').replace(/<\/hp:subList>$/, '')
|
|
2735
|
+
: xml;
|
|
2736
|
+
this.parseCellContent(contentXml, cell);
|
|
2737
|
+
return cell;
|
|
2738
|
+
}
|
|
2739
|
+
static parseCellContent(contentXml, cell) {
|
|
2740
|
+
// Remove MEMO fieldBegin content from cell content to prevent memo text appearing in cell
|
|
2741
|
+
const cleanedXml = contentXml.replace(/<hp:fieldBegin[^>]*type="MEMO"[^>]*>[\s\S]*?<\/hp:fieldBegin>/gi, '');
|
|
2742
|
+
const nestedTables = this.extractBalancedTags(cleanedXml, 'hp:tbl');
|
|
2743
|
+
if (nestedTables.length > 0) {
|
|
2744
|
+
cell.nestedTables = [];
|
|
2745
|
+
cell.elements = [];
|
|
2746
|
+
let remainingXml = cleanedXml;
|
|
2747
|
+
for (const tableXml of nestedTables) {
|
|
2748
|
+
const tableIndex = remainingXml.indexOf(tableXml);
|
|
2749
|
+
if (tableIndex > 0) {
|
|
2750
|
+
const beforeTable = remainingXml.substring(0, tableIndex);
|
|
2751
|
+
const paragraphs = this.extractBalancedTags(beforeTable, 'hp:p');
|
|
2752
|
+
for (const pXml of paragraphs) {
|
|
2753
|
+
const paragraph = this.parseParagraph(pXml);
|
|
2754
|
+
cell.paragraphs.push(paragraph);
|
|
2755
|
+
cell.elements.push({ type: 'paragraph', data: paragraph });
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
const nestedTable = this.parseTable(tableXml);
|
|
2759
|
+
cell.nestedTables.push(nestedTable);
|
|
2760
|
+
cell.elements.push({ type: 'table', data: nestedTable });
|
|
2761
|
+
remainingXml = remainingXml.substring(tableIndex + tableXml.length);
|
|
2762
|
+
}
|
|
2763
|
+
if (remainingXml) {
|
|
2764
|
+
const paragraphs = this.extractBalancedTags(remainingXml, 'hp:p');
|
|
2765
|
+
for (const pXml of paragraphs) {
|
|
2766
|
+
const paragraph = this.parseParagraph(pXml);
|
|
2767
|
+
cell.paragraphs.push(paragraph);
|
|
2768
|
+
cell.elements.push({ type: 'paragraph', data: paragraph });
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
else {
|
|
2773
|
+
const paragraphs = this.extractBalancedTags(cleanedXml, 'hp:p');
|
|
2774
|
+
for (const pXml of paragraphs) {
|
|
2775
|
+
const paragraph = this.parseParagraph(pXml);
|
|
2776
|
+
cell.paragraphs.push(paragraph);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
static parseImageElement(xml, content) {
|
|
2781
|
+
let binaryRefMatch = xml.match(/<hc:img[^>]*binaryItemIDRef="([^"]*)"/);
|
|
2782
|
+
if (!binaryRefMatch) {
|
|
2783
|
+
binaryRefMatch = xml.match(/binaryItemIDRef="([^"]*)"/);
|
|
2784
|
+
}
|
|
2785
|
+
if (!binaryRefMatch)
|
|
2786
|
+
return null;
|
|
2787
|
+
const imageId = binaryRefMatch[1];
|
|
2788
|
+
const existingImage = content.images.get(imageId);
|
|
2789
|
+
// Use existing image from BinData if available, or create new
|
|
2790
|
+
const image = existingImage ? {
|
|
2791
|
+
...existingImage,
|
|
2792
|
+
id: existingImage.id || generateId(),
|
|
2793
|
+
width: existingImage.width || 100,
|
|
2794
|
+
height: existingImage.height || 100,
|
|
2795
|
+
} : {
|
|
2796
|
+
id: generateId(),
|
|
2797
|
+
binaryId: imageId,
|
|
2798
|
+
width: 100,
|
|
2799
|
+
height: 100,
|
|
2800
|
+
};
|
|
2801
|
+
// Try to get size from various tags (in order of priority)
|
|
2802
|
+
// 1. hp:sz - standard size
|
|
2803
|
+
// 2. hp:curSz - current display size
|
|
2804
|
+
// 3. hp:orgSz - original size (fallback)
|
|
2805
|
+
const szMatch = xml.match(/<hp:sz\s+width="(\d+)"[^>]*height="(\d+)"/);
|
|
2806
|
+
if (szMatch) {
|
|
2807
|
+
image.width = parseInt(szMatch[1]) / 100;
|
|
2808
|
+
image.height = parseInt(szMatch[2]) / 100;
|
|
2809
|
+
}
|
|
2810
|
+
const curSzMatch = xml.match(/<hp:curSz\s+width="(\d+)"[^>]*height="(\d+)"/);
|
|
2811
|
+
if (curSzMatch) {
|
|
2812
|
+
// curSz takes priority over sz if both exist
|
|
2813
|
+
image.width = parseInt(curSzMatch[1]) / 100;
|
|
2814
|
+
image.height = parseInt(curSzMatch[2]) / 100;
|
|
2815
|
+
}
|
|
2816
|
+
const orgSzMatch = xml.match(/<hp:orgSz\s+width="(\d+)"[^>]*height="(\d+)"/);
|
|
2817
|
+
if (orgSzMatch) {
|
|
2818
|
+
image.orgWidth = parseInt(orgSzMatch[1]) / 100;
|
|
2819
|
+
image.orgHeight = parseInt(orgSzMatch[2]) / 100;
|
|
2820
|
+
// Use orgSz as fallback if neither sz nor curSz found
|
|
2821
|
+
if (!szMatch && !curSzMatch) {
|
|
2822
|
+
image.width = image.orgWidth;
|
|
2823
|
+
image.height = image.orgHeight;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
const picTagMatch = xml.match(/<hp:pic[^>]*>/);
|
|
2827
|
+
if (picTagMatch) {
|
|
2828
|
+
const picAttrs = picTagMatch[0];
|
|
2829
|
+
const zOrderMatch = picAttrs.match(/zOrder="(\d+)"/);
|
|
2830
|
+
if (zOrderMatch)
|
|
2831
|
+
image.zOrder = parseInt(zOrderMatch[1]);
|
|
2832
|
+
const numTypeMatch = picAttrs.match(/numberingType="([^"]*)"/);
|
|
2833
|
+
if (numTypeMatch) {
|
|
2834
|
+
const map = {
|
|
2835
|
+
'NONE': 'none', 'PICTURE': 'picture', 'TABLE': 'table', 'EQUATION': 'equation'
|
|
2836
|
+
};
|
|
2837
|
+
image.numberingType = map[numTypeMatch[1].toUpperCase()] || 'none';
|
|
2838
|
+
}
|
|
2839
|
+
const textWrapMatch = picAttrs.match(/textWrap="([^"]*)"/);
|
|
2840
|
+
if (textWrapMatch) {
|
|
2841
|
+
const map = {
|
|
2842
|
+
'SQUARE': 'square', 'TIGHT': 'tight', 'THROUGH': 'through',
|
|
2843
|
+
'TOP_AND_BOTTOM': 'topAndBottom', 'BEHIND_TEXT': 'behindText', 'IN_FRONT_OF_TEXT': 'inFrontOfText'
|
|
2844
|
+
};
|
|
2845
|
+
image.textWrap = map[textWrapMatch[1].toUpperCase()] || 'square';
|
|
2846
|
+
}
|
|
2847
|
+
const textFlowMatch = picAttrs.match(/textFlow="([^"]*)"/);
|
|
2848
|
+
if (textFlowMatch) {
|
|
2849
|
+
const map = {
|
|
2850
|
+
'BOTH_SIDES': 'bothSides', 'LEFT_ONLY': 'leftOnly', 'RIGHT_ONLY': 'rightOnly', 'LARGEST_ONLY': 'largestOnly'
|
|
2851
|
+
};
|
|
2852
|
+
image.textFlow = map[textFlowMatch[1].toUpperCase()] || 'bothSides';
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
const posMatch = xml.match(/<hp:pos[^>]*>/);
|
|
2856
|
+
if (posMatch) {
|
|
2857
|
+
const pos = posMatch[0];
|
|
2858
|
+
image.position = {};
|
|
2859
|
+
if (pos.includes('treatAsChar="1"') || pos.includes('treatAsChar="true"')) {
|
|
2860
|
+
image.position.treatAsChar = true;
|
|
2861
|
+
}
|
|
2862
|
+
if (pos.includes('affectLSpacing="1"') || pos.includes('affectLSpacing="true"')) {
|
|
2863
|
+
image.position.affectLSpacing = true;
|
|
2864
|
+
}
|
|
2865
|
+
if (pos.includes('flowWithText="1"') || pos.includes('flowWithText="true"')) {
|
|
2866
|
+
image.position.flowWithText = true;
|
|
2867
|
+
}
|
|
2868
|
+
if (pos.includes('allowOverlap="1"') || pos.includes('allowOverlap="true"')) {
|
|
2869
|
+
image.position.allowOverlap = true;
|
|
2870
|
+
}
|
|
2871
|
+
if (pos.includes('holdAnchorAndSO="1"') || pos.includes('holdAnchorAndSO="true"')) {
|
|
2872
|
+
image.position.holdAnchorAndSO = true;
|
|
2873
|
+
}
|
|
2874
|
+
const vertRelMatch = pos.match(/vertRelTo="([^"]*)"/);
|
|
2875
|
+
if (vertRelMatch) {
|
|
2876
|
+
const map = {
|
|
2877
|
+
'PAPER': 'paper', 'PAGE': 'page', 'PARA': 'para'
|
|
2878
|
+
};
|
|
2879
|
+
image.position.vertRelTo = map[vertRelMatch[1].toUpperCase()];
|
|
2880
|
+
}
|
|
2881
|
+
const horzRelMatch = pos.match(/horzRelTo="([^"]*)"/);
|
|
2882
|
+
if (horzRelMatch) {
|
|
2883
|
+
const map = {
|
|
2884
|
+
'PAPER': 'paper', 'PAGE': 'page', 'COLUMN': 'column', 'PARA': 'para'
|
|
2885
|
+
};
|
|
2886
|
+
image.position.horzRelTo = map[horzRelMatch[1].toUpperCase()];
|
|
2887
|
+
}
|
|
2888
|
+
const vertAlignMatch = pos.match(/vertAlign="([^"]*)"/);
|
|
2889
|
+
if (vertAlignMatch) {
|
|
2890
|
+
const map = {
|
|
2891
|
+
'TOP': 'top', 'CENTER': 'center', 'BOTTOM': 'bottom', 'INSIDE': 'inside', 'OUTSIDE': 'outside'
|
|
2892
|
+
};
|
|
2893
|
+
image.position.vertAlign = map[vertAlignMatch[1].toUpperCase()];
|
|
2894
|
+
}
|
|
2895
|
+
const horzAlignMatch = pos.match(/horzAlign="([^"]*)"/);
|
|
2896
|
+
if (horzAlignMatch) {
|
|
2897
|
+
const map = {
|
|
2898
|
+
'LEFT': 'left', 'CENTER': 'center', 'RIGHT': 'right', 'INSIDE': 'inside', 'OUTSIDE': 'outside'
|
|
2899
|
+
};
|
|
2900
|
+
image.position.horzAlign = map[horzAlignMatch[1].toUpperCase()];
|
|
2901
|
+
}
|
|
2902
|
+
const vertOffsetMatch = pos.match(/vertOffset="(-?\d+)"/);
|
|
2903
|
+
if (vertOffsetMatch) {
|
|
2904
|
+
image.position.vertOffset = parseInt(vertOffsetMatch[1]) / 100;
|
|
2905
|
+
}
|
|
2906
|
+
const horzOffsetMatch = pos.match(/horzOffset="(-?\d+)"/);
|
|
2907
|
+
if (horzOffsetMatch) {
|
|
2908
|
+
image.position.horzOffset = parseInt(horzOffsetMatch[1]) / 100;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
const outMarginMatch = xml.match(/<hp:outMargin[^>]*left="(\d+)"[^>]*right="(\d+)"[^>]*top="(\d+)"[^>]*bottom="(\d+)"/);
|
|
2912
|
+
if (outMarginMatch) {
|
|
2913
|
+
image.outMargin = {
|
|
2914
|
+
left: parseInt(outMarginMatch[1]) / 100,
|
|
2915
|
+
right: parseInt(outMarginMatch[2]) / 100,
|
|
2916
|
+
top: parseInt(outMarginMatch[3]) / 100,
|
|
2917
|
+
bottom: parseInt(outMarginMatch[4]) / 100
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
const flipMatch = xml.match(/<hc:flip[^>]*horizontal="([^"]*)"[^>]*vertical="([^"]*)"/);
|
|
2921
|
+
if (flipMatch) {
|
|
2922
|
+
image.flip = {
|
|
2923
|
+
horizontal: flipMatch[1] === '1' || flipMatch[1] === 'true',
|
|
2924
|
+
vertical: flipMatch[2] === '1' || flipMatch[2] === 'true'
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
const rotationMatch = xml.match(/<hp:rotationInfo[^>]*angle="(-?\d+)"(?:[^>]*centerX="(\d+)")?(?:[^>]*centerY="(\d+)")?/);
|
|
2928
|
+
if (rotationMatch) {
|
|
2929
|
+
image.rotation = {
|
|
2930
|
+
angle: parseInt(rotationMatch[1]),
|
|
2931
|
+
centerX: rotationMatch[2] ? parseInt(rotationMatch[2]) / 100 : undefined,
|
|
2932
|
+
centerY: rotationMatch[3] ? parseInt(rotationMatch[3]) / 100 : undefined
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
const imgEffectMatch = xml.match(/<hc:imgEffect[^>]*>/);
|
|
2936
|
+
if (imgEffectMatch) {
|
|
2937
|
+
const effect = imgEffectMatch[0];
|
|
2938
|
+
const brightnessMatch = effect.match(/brightness="(-?\d+)"/);
|
|
2939
|
+
if (brightnessMatch) {
|
|
2940
|
+
image.brightness = parseInt(brightnessMatch[1]);
|
|
2941
|
+
}
|
|
2942
|
+
const contrastMatch = effect.match(/contrast="(-?\d+)"/);
|
|
2943
|
+
if (contrastMatch) {
|
|
2944
|
+
image.contrast = parseInt(contrastMatch[1]);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
const alphaMatch = xml.match(/<hc:img[^>]*alpha="(\d+)"/);
|
|
2948
|
+
if (alphaMatch) {
|
|
2949
|
+
image.alpha = parseInt(alphaMatch[1]) / 255;
|
|
2950
|
+
}
|
|
2951
|
+
const shapeCommentMatch = xml.match(/<hp:shapeComment>([^<]*)<\/hp:shapeComment>/);
|
|
2952
|
+
if (shapeCommentMatch) {
|
|
2953
|
+
image.shapeComment = shapeCommentMatch[1];
|
|
2954
|
+
}
|
|
2955
|
+
// Update content.images with parsed width/height
|
|
2956
|
+
// This ensures getImages() returns correct dimensions
|
|
2957
|
+
content.images.set(imageId, image);
|
|
2958
|
+
return image;
|
|
2959
|
+
}
|
|
2960
|
+
static decodeXmlEntities(text) {
|
|
2961
|
+
return text
|
|
2962
|
+
.replace(/&/g, '&')
|
|
2963
|
+
.replace(/</g, '<')
|
|
2964
|
+
.replace(/>/g, '>')
|
|
2965
|
+
.replace(/"/g, '"')
|
|
2966
|
+
.replace(/'/g, "'");
|
|
2967
|
+
}
|
|
2968
|
+
static async updateZip(zip, content) {
|
|
2969
|
+
for (let sectionIndex = 0; sectionIndex < content.sections.length; sectionIndex++) {
|
|
2970
|
+
const sectionPath = `Contents/section${sectionIndex}.xml`;
|
|
2971
|
+
const existingXml = await this.readXmlFile(zip, sectionPath);
|
|
2972
|
+
if (existingXml) {
|
|
2973
|
+
const updatedXml = this.updateSectionXml(existingXml, content.sections[sectionIndex]);
|
|
2974
|
+
zip.file(sectionPath, updatedXml);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
static updateSectionXml(xml, section) {
|
|
2979
|
+
let updatedXml = xml;
|
|
2980
|
+
let elementIndex = 0;
|
|
2981
|
+
const paragraphElements = section.elements.filter((e) => e.type === 'paragraph');
|
|
2982
|
+
const paragraphRegex = /<hp:p[^>]*>([\s\S]*?)<\/hp:p>/g;
|
|
2983
|
+
updatedXml = xml.replace(paragraphRegex, (match) => {
|
|
2984
|
+
if (elementIndex < paragraphElements.length) {
|
|
2985
|
+
const paragraph = paragraphElements[elementIndex].data;
|
|
2986
|
+
elementIndex++;
|
|
2987
|
+
return this.updateParagraphXml(match, paragraph);
|
|
2988
|
+
}
|
|
2989
|
+
elementIndex++;
|
|
2990
|
+
return match;
|
|
2991
|
+
});
|
|
2992
|
+
return updatedXml;
|
|
2993
|
+
}
|
|
2994
|
+
static updateParagraphXml(xml, paragraph) {
|
|
2995
|
+
const fullText = paragraph.runs.map((r) => r.text).join('');
|
|
2996
|
+
const textTagRegex = /(<hp:t[^>]*>)[^<]*(<\/hp:t>)/;
|
|
2997
|
+
if (textTagRegex.test(xml)) {
|
|
2998
|
+
return xml.replace(textTagRegex, `$1${this.escapeXml(fullText)}$2`);
|
|
2999
|
+
}
|
|
3000
|
+
return xml;
|
|
3001
|
+
}
|
|
3002
|
+
static escapeXml(text) {
|
|
3003
|
+
return text
|
|
3004
|
+
.replace(/&/g, '&')
|
|
3005
|
+
.replace(/</g, '<')
|
|
3006
|
+
.replace(/>/g, '>')
|
|
3007
|
+
.replace(/"/g, '"')
|
|
3008
|
+
.replace(/'/g, ''');
|
|
3009
|
+
}
|
|
3010
|
+
static parseLine(xml) {
|
|
3011
|
+
const x1 = this.parseNumber(xml, /x1="([^"]*)"/) || this.parseNumber(xml, /startX="([^"]*)"/);
|
|
3012
|
+
const y1 = this.parseNumber(xml, /y1="([^"]*)"/) || this.parseNumber(xml, /startY="([^"]*)"/);
|
|
3013
|
+
const x2 = this.parseNumber(xml, /x2="([^"]*)"/) || this.parseNumber(xml, /endX="([^"]*)"/);
|
|
3014
|
+
const y2 = this.parseNumber(xml, /y2="([^"]*)"/) || this.parseNumber(xml, /endY="([^"]*)"/);
|
|
3015
|
+
const strokeColor = xml.match(/(?:stroke|lineColor)="([^"]*)"/)?.[1];
|
|
3016
|
+
const strokeWidth = this.parseNumber(xml, /(?:strokeWidth|lineWidth)="([^"]*)"/);
|
|
3017
|
+
return {
|
|
3018
|
+
id: generateId(),
|
|
3019
|
+
x1: x1 || 0,
|
|
3020
|
+
y1: y1 || 0,
|
|
3021
|
+
x2: x2 || 100,
|
|
3022
|
+
y2: y2 || 0,
|
|
3023
|
+
strokeColor: strokeColor || '#000000',
|
|
3024
|
+
strokeWidth: strokeWidth || 1,
|
|
3025
|
+
strokeStyle: 'solid',
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
static parseRect(xml) {
|
|
3029
|
+
const x = this.parseNumber(xml, /(?:x|left)="([^"]*)"/);
|
|
3030
|
+
const y = this.parseNumber(xml, /(?:y|top)="([^"]*)"/);
|
|
3031
|
+
const width = this.parseNumber(xml, /width="([^"]*)"/);
|
|
3032
|
+
const height = this.parseNumber(xml, /height="([^"]*)"/);
|
|
3033
|
+
const fillColor = xml.match(/(?:fill|fillColor)="([^"]*)"/)?.[1];
|
|
3034
|
+
const strokeColor = xml.match(/(?:stroke|lineColor)="([^"]*)"/)?.[1];
|
|
3035
|
+
const cornerRadius = this.parseNumber(xml, /(?:rx|cornerRadius)="([^"]*)"/);
|
|
3036
|
+
return {
|
|
3037
|
+
id: generateId(),
|
|
3038
|
+
x: x || 0,
|
|
3039
|
+
y: y || 0,
|
|
3040
|
+
width: width || 100,
|
|
3041
|
+
height: height || 50,
|
|
3042
|
+
fillColor,
|
|
3043
|
+
strokeColor: strokeColor || '#000000',
|
|
3044
|
+
strokeWidth: 1,
|
|
3045
|
+
cornerRadius,
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
static parseEllipse(xml) {
|
|
3049
|
+
const cx = this.parseNumber(xml, /(?:cx|centerX)="([^"]*)"/);
|
|
3050
|
+
const cy = this.parseNumber(xml, /(?:cy|centerY)="([^"]*)"/);
|
|
3051
|
+
const rx = this.parseNumber(xml, /(?:rx|radiusX)="([^"]*)"/);
|
|
3052
|
+
const ry = this.parseNumber(xml, /(?:ry|radiusY)="([^"]*)"/);
|
|
3053
|
+
const fillColor = xml.match(/(?:fill|fillColor)="([^"]*)"/)?.[1];
|
|
3054
|
+
const strokeColor = xml.match(/(?:stroke|lineColor)="([^"]*)"/)?.[1];
|
|
3055
|
+
return {
|
|
3056
|
+
id: generateId(),
|
|
3057
|
+
cx: cx || 50,
|
|
3058
|
+
cy: cy || 50,
|
|
3059
|
+
rx: rx || 50,
|
|
3060
|
+
ry: ry || 50,
|
|
3061
|
+
fillColor,
|
|
3062
|
+
strokeColor: strokeColor || '#000000',
|
|
3063
|
+
strokeWidth: 1,
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
static parseTextBox(xml) {
|
|
3067
|
+
const x = this.parseNumber(xml, /(?:x|left)="([^"]*)"/);
|
|
3068
|
+
const y = this.parseNumber(xml, /(?:y|top)="([^"]*)"/);
|
|
3069
|
+
const width = this.parseNumber(xml, /width="([^"]*)"/);
|
|
3070
|
+
const height = this.parseNumber(xml, /height="([^"]*)"/);
|
|
3071
|
+
const fillColor = xml.match(/(?:fill|fillColor)="([^"]*)"/)?.[1];
|
|
3072
|
+
const strokeColor = xml.match(/(?:stroke|lineColor)="([^"]*)"/)?.[1];
|
|
3073
|
+
const paragraphs = [];
|
|
3074
|
+
const paragraphRegex = /<hp:p[^>]*>([\s\S]*?)<\/hp:p>/g;
|
|
3075
|
+
let match;
|
|
3076
|
+
while ((match = paragraphRegex.exec(xml)) !== null) {
|
|
3077
|
+
paragraphs.push(this.parseParagraph(match[0]));
|
|
3078
|
+
}
|
|
3079
|
+
return {
|
|
3080
|
+
id: generateId(),
|
|
3081
|
+
x: x || 0,
|
|
3082
|
+
y: y || 0,
|
|
3083
|
+
width: width || 200,
|
|
3084
|
+
height: height || 100,
|
|
3085
|
+
paragraphs,
|
|
3086
|
+
fillColor,
|
|
3087
|
+
strokeColor,
|
|
3088
|
+
strokeWidth: strokeColor ? 1 : 0,
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
static parseHorizontalRules(xml, section) {
|
|
3092
|
+
const hrOnlyPatterns = [
|
|
3093
|
+
/^[\s]*[─]{10,}[\s]*$/,
|
|
3094
|
+
/^[\s]*[━]{10,}[\s]*$/,
|
|
3095
|
+
/^[\s]*[═]{10,}[\s]*$/,
|
|
3096
|
+
/^[\s]*[▬]{10,}[\s]*$/,
|
|
3097
|
+
/^[\s]*[-]{20,}[\s]*$/,
|
|
3098
|
+
];
|
|
3099
|
+
for (let i = 0; i < section.elements.length; i++) {
|
|
3100
|
+
const el = section.elements[i];
|
|
3101
|
+
if (el.type === 'paragraph') {
|
|
3102
|
+
const text = el.data.runs.map(r => r.text).join('').trim();
|
|
3103
|
+
const isHrOnly = hrOnlyPatterns.some(pattern => pattern.test(text));
|
|
3104
|
+
if (isHrOnly) {
|
|
3105
|
+
section.elements[i] = {
|
|
3106
|
+
type: 'hr',
|
|
3107
|
+
data: {
|
|
3108
|
+
id: generateId(),
|
|
3109
|
+
width: 'full',
|
|
3110
|
+
height: 1,
|
|
3111
|
+
color: '#000000',
|
|
3112
|
+
style: 'solid',
|
|
3113
|
+
align: 'center',
|
|
3114
|
+
},
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
static parseNumber(xml, regex) {
|
|
3121
|
+
const match = xml.match(regex);
|
|
3122
|
+
if (match) {
|
|
3123
|
+
const val = parseFloat(match[1]);
|
|
3124
|
+
return isNaN(val) ? undefined : val / 100;
|
|
3125
|
+
}
|
|
3126
|
+
return undefined;
|
|
3127
|
+
}
|
|
3128
|
+
static parseShapeObject(xml) {
|
|
3129
|
+
const szMatch = xml.match(/<hp:sz\s+width="(\d+)"[^>]*height="(\d+)"/);
|
|
3130
|
+
const posMatch = xml.match(/<hp:pos[^>]*>/);
|
|
3131
|
+
if (!szMatch && !posMatch)
|
|
3132
|
+
return undefined;
|
|
3133
|
+
const shapeObject = {};
|
|
3134
|
+
if (szMatch) {
|
|
3135
|
+
shapeObject.size = {
|
|
3136
|
+
width: parseInt(szMatch[1]) / 100,
|
|
3137
|
+
height: parseInt(szMatch[2]) / 100,
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
if (posMatch) {
|
|
3141
|
+
const pos = posMatch[0];
|
|
3142
|
+
shapeObject.position = {};
|
|
3143
|
+
if (pos.includes('treatAsChar="1"') || pos.includes('treatAsChar="true"')) {
|
|
3144
|
+
shapeObject.position.treatAsChar = true;
|
|
3145
|
+
}
|
|
3146
|
+
const vertRelMatch = pos.match(/vertRelTo="([^"]*)"/);
|
|
3147
|
+
if (vertRelMatch) {
|
|
3148
|
+
const map = {
|
|
3149
|
+
'PAPER': 'paper', 'PAGE': 'page', 'PARA': 'para'
|
|
3150
|
+
};
|
|
3151
|
+
shapeObject.position.vertRelTo = map[vertRelMatch[1].toUpperCase()];
|
|
3152
|
+
}
|
|
3153
|
+
const horzRelMatch = pos.match(/horzRelTo="([^"]*)"/);
|
|
3154
|
+
if (horzRelMatch) {
|
|
3155
|
+
const map = {
|
|
3156
|
+
'PAPER': 'paper', 'PAGE': 'page', 'COLUMN': 'column', 'PARA': 'para'
|
|
3157
|
+
};
|
|
3158
|
+
shapeObject.position.horzRelTo = map[horzRelMatch[1].toUpperCase()];
|
|
3159
|
+
}
|
|
3160
|
+
const vertOffsetMatch = pos.match(/vertOffset="(-?\d+)"/);
|
|
3161
|
+
if (vertOffsetMatch) {
|
|
3162
|
+
shapeObject.position.vertOffset = parseInt(vertOffsetMatch[1]) / 100;
|
|
3163
|
+
}
|
|
3164
|
+
const horzOffsetMatch = pos.match(/horzOffset="(-?\d+)"/);
|
|
3165
|
+
if (horzOffsetMatch) {
|
|
3166
|
+
shapeObject.position.horzOffset = parseInt(horzOffsetMatch[1]) / 100;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
const instIdMatch = xml.match(/instId="([^"]*)"/);
|
|
3170
|
+
if (instIdMatch)
|
|
3171
|
+
shapeObject.instId = instIdMatch[1];
|
|
3172
|
+
const zOrderMatch = xml.match(/zOrder="(\d+)"/);
|
|
3173
|
+
if (zOrderMatch)
|
|
3174
|
+
shapeObject.zOrder = parseInt(zOrderMatch[1]);
|
|
3175
|
+
return shapeObject;
|
|
3176
|
+
}
|
|
3177
|
+
static parseDrawingObject(xml) {
|
|
3178
|
+
const drawingObject = {};
|
|
3179
|
+
const lineShapeMatch = xml.match(/<hc:lineShape[^>]*(?:\/>|>([\s\S]*?)<\/hc:lineShape>)/);
|
|
3180
|
+
if (lineShapeMatch) {
|
|
3181
|
+
const content = lineShapeMatch[0];
|
|
3182
|
+
drawingObject.lineShape = {
|
|
3183
|
+
color: content.match(/color="([^"]*)"/)?.[1],
|
|
3184
|
+
width: this.parseNumber(content, /width="([^"]*)"/),
|
|
3185
|
+
};
|
|
3186
|
+
const styleMatch = content.match(/style="([^"]*)"/);
|
|
3187
|
+
if (styleMatch) {
|
|
3188
|
+
const styleMap = {
|
|
3189
|
+
'SOLID': 'Solid', 'DASH': 'Dash', 'DOT': 'Dot', 'DASH_DOT': 'DashDot'
|
|
3190
|
+
};
|
|
3191
|
+
drawingObject.lineShape.style = styleMap[styleMatch[1].toUpperCase()] || 'Solid';
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
const fillBrushMatch = xml.match(/<hc:fillBrush[^>]*>([\s\S]*?)<\/hc:fillBrush>/);
|
|
3195
|
+
if (fillBrushMatch) {
|
|
3196
|
+
drawingObject.fillBrush = {};
|
|
3197
|
+
const fillContent = fillBrushMatch[1];
|
|
3198
|
+
const winBrushMatch = fillContent.match(/<hc:winBrush[^>]*faceColor="([^"]*)"/);
|
|
3199
|
+
if (winBrushMatch) {
|
|
3200
|
+
drawingObject.fillBrush.windowBrush = { faceColor: winBrushMatch[1] };
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
return Object.keys(drawingObject).length > 0 ? drawingObject : undefined;
|
|
3204
|
+
}
|
|
3205
|
+
static parseArc(xml) {
|
|
3206
|
+
const arc = {
|
|
3207
|
+
id: generateId(),
|
|
3208
|
+
centerX: 0,
|
|
3209
|
+
centerY: 0,
|
|
3210
|
+
};
|
|
3211
|
+
const typeMatch = xml.match(/\btype="([^"]*)"/);
|
|
3212
|
+
if (typeMatch) {
|
|
3213
|
+
const typeMap = {
|
|
3214
|
+
'NORMAL': 'Normal', 'PIE': 'Pie', 'CHORD': 'Chord'
|
|
3215
|
+
};
|
|
3216
|
+
arc.type = typeMap[typeMatch[1].toUpperCase()] || 'Normal';
|
|
3217
|
+
}
|
|
3218
|
+
const centerXMatch = xml.match(/centerX="(-?\d+)"/);
|
|
3219
|
+
if (centerXMatch)
|
|
3220
|
+
arc.centerX = parseInt(centerXMatch[1]) / 100;
|
|
3221
|
+
const centerYMatch = xml.match(/centerY="(-?\d+)"/);
|
|
3222
|
+
if (centerYMatch)
|
|
3223
|
+
arc.centerY = parseInt(centerYMatch[1]) / 100;
|
|
3224
|
+
const axis1XMatch = xml.match(/axis1X="(-?\d+)"/);
|
|
3225
|
+
if (axis1XMatch)
|
|
3226
|
+
arc.axis1X = parseInt(axis1XMatch[1]) / 100;
|
|
3227
|
+
const axis1YMatch = xml.match(/axis1Y="(-?\d+)"/);
|
|
3228
|
+
if (axis1YMatch)
|
|
3229
|
+
arc.axis1Y = parseInt(axis1YMatch[1]) / 100;
|
|
3230
|
+
const axis2XMatch = xml.match(/axis2X="(-?\d+)"/);
|
|
3231
|
+
if (axis2XMatch)
|
|
3232
|
+
arc.axis2X = parseInt(axis2XMatch[1]) / 100;
|
|
3233
|
+
const axis2YMatch = xml.match(/axis2Y="(-?\d+)"/);
|
|
3234
|
+
if (axis2YMatch)
|
|
3235
|
+
arc.axis2Y = parseInt(axis2YMatch[1]) / 100;
|
|
3236
|
+
arc.shapeObject = this.parseShapeObject(xml);
|
|
3237
|
+
arc.drawingObject = this.parseDrawingObject(xml);
|
|
3238
|
+
return arc;
|
|
3239
|
+
}
|
|
3240
|
+
static parsePolygon(xml) {
|
|
3241
|
+
const polygon = {
|
|
3242
|
+
id: generateId(),
|
|
3243
|
+
points: [],
|
|
3244
|
+
};
|
|
3245
|
+
const pointRegex = /<(?:hp:|hc:)?pt[^>]*x="(-?\d+)"[^>]*y="(-?\d+)"/gi;
|
|
3246
|
+
let pointMatch;
|
|
3247
|
+
while ((pointMatch = pointRegex.exec(xml)) !== null) {
|
|
3248
|
+
polygon.points.push({
|
|
3249
|
+
x: parseInt(pointMatch[1]) / 100,
|
|
3250
|
+
y: parseInt(pointMatch[2]) / 100,
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
if (polygon.points.length === 0) {
|
|
3254
|
+
const altPointRegex = /<(?:hp:|hc:)?point[^>]*x="(-?\d+)"[^>]*y="(-?\d+)"/gi;
|
|
3255
|
+
while ((pointMatch = altPointRegex.exec(xml)) !== null) {
|
|
3256
|
+
polygon.points.push({
|
|
3257
|
+
x: parseInt(pointMatch[1]) / 100,
|
|
3258
|
+
y: parseInt(pointMatch[2]) / 100,
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
polygon.shapeObject = this.parseShapeObject(xml);
|
|
3263
|
+
polygon.drawingObject = this.parseDrawingObject(xml);
|
|
3264
|
+
return polygon;
|
|
3265
|
+
}
|
|
3266
|
+
static parseCurve(xml) {
|
|
3267
|
+
const curve = {
|
|
3268
|
+
id: generateId(),
|
|
3269
|
+
segments: [],
|
|
3270
|
+
};
|
|
3271
|
+
const segmentRegex = /<(?:hp:|hc:)?seg[^>]*type="([^"]*)"[^>]*x1="(-?\d+)"[^>]*y1="(-?\d+)"[^>]*x2="(-?\d+)"[^>]*y2="(-?\d+)"/gi;
|
|
3272
|
+
let segMatch;
|
|
3273
|
+
while ((segMatch = segmentRegex.exec(xml)) !== null) {
|
|
3274
|
+
const segment = {
|
|
3275
|
+
type: segMatch[1].toUpperCase() === 'CURVE' ? 'Curve' : 'Line',
|
|
3276
|
+
x1: parseInt(segMatch[2]) / 100,
|
|
3277
|
+
y1: parseInt(segMatch[3]) / 100,
|
|
3278
|
+
x2: parseInt(segMatch[4]) / 100,
|
|
3279
|
+
y2: parseInt(segMatch[5]) / 100,
|
|
3280
|
+
};
|
|
3281
|
+
curve.segments.push(segment);
|
|
3282
|
+
}
|
|
3283
|
+
curve.shapeObject = this.parseShapeObject(xml);
|
|
3284
|
+
curve.drawingObject = this.parseDrawingObject(xml);
|
|
3285
|
+
return curve;
|
|
3286
|
+
}
|
|
3287
|
+
static parseConnectLine(xml) {
|
|
3288
|
+
const connectLine = {
|
|
3289
|
+
id: generateId(),
|
|
3290
|
+
};
|
|
3291
|
+
const typeMatch = xml.match(/\btype="([^"]*)"/);
|
|
3292
|
+
if (typeMatch)
|
|
3293
|
+
connectLine.type = typeMatch[1];
|
|
3294
|
+
const startXMatch = xml.match(/startX="(-?\d+)"/);
|
|
3295
|
+
if (startXMatch)
|
|
3296
|
+
connectLine.startX = parseInt(startXMatch[1]) / 100;
|
|
3297
|
+
const startYMatch = xml.match(/startY="(-?\d+)"/);
|
|
3298
|
+
if (startYMatch)
|
|
3299
|
+
connectLine.startY = parseInt(startYMatch[1]) / 100;
|
|
3300
|
+
const endXMatch = xml.match(/endX="(-?\d+)"/);
|
|
3301
|
+
if (endXMatch)
|
|
3302
|
+
connectLine.endX = parseInt(endXMatch[1]) / 100;
|
|
3303
|
+
const endYMatch = xml.match(/endY="(-?\d+)"/);
|
|
3304
|
+
if (endYMatch)
|
|
3305
|
+
connectLine.endY = parseInt(endYMatch[1]) / 100;
|
|
3306
|
+
const startSubjectIDMatch = xml.match(/startSubjectID="([^"]*)"/);
|
|
3307
|
+
if (startSubjectIDMatch)
|
|
3308
|
+
connectLine.startSubjectID = startSubjectIDMatch[1];
|
|
3309
|
+
const startSubjectIndexMatch = xml.match(/startSubjectIndex="(\d+)"/);
|
|
3310
|
+
if (startSubjectIndexMatch)
|
|
3311
|
+
connectLine.startSubjectIndex = parseInt(startSubjectIndexMatch[1]);
|
|
3312
|
+
const endSubjectIDMatch = xml.match(/endSubjectID="([^"]*)"/);
|
|
3313
|
+
if (endSubjectIDMatch)
|
|
3314
|
+
connectLine.endSubjectID = endSubjectIDMatch[1];
|
|
3315
|
+
const endSubjectIndexMatch = xml.match(/endSubjectIndex="(\d+)"/);
|
|
3316
|
+
if (endSubjectIndexMatch)
|
|
3317
|
+
connectLine.endSubjectIndex = parseInt(endSubjectIndexMatch[1]);
|
|
3318
|
+
connectLine.shapeObject = this.parseShapeObject(xml);
|
|
3319
|
+
connectLine.drawingObject = this.parseDrawingObject(xml);
|
|
3320
|
+
return connectLine;
|
|
3321
|
+
}
|
|
3322
|
+
static parseContainer(xml, content) {
|
|
3323
|
+
const container = {
|
|
3324
|
+
id: generateId(),
|
|
3325
|
+
children: [],
|
|
3326
|
+
};
|
|
3327
|
+
container.shapeObject = this.parseShapeObject(xml);
|
|
3328
|
+
const lineMatches = xml.matchAll(/<hp:line\b[^>]*(?:\/>|>[\s\S]*?<\/hp:line>)/g);
|
|
3329
|
+
for (const match of lineMatches) {
|
|
3330
|
+
container.children.push(this.parseLine(match[0]));
|
|
3331
|
+
}
|
|
3332
|
+
const rectMatches = xml.matchAll(/<hp:rect\b[^>]*(?:\/>|>[\s\S]*?<\/hp:rect>)/g);
|
|
3333
|
+
for (const match of rectMatches) {
|
|
3334
|
+
container.children.push(this.parseRect(match[0]));
|
|
3335
|
+
}
|
|
3336
|
+
const ellipseMatches = xml.matchAll(/<hp:ellipse\b[^>]*(?:\/>|>[\s\S]*?<\/hp:ellipse>)/g);
|
|
3337
|
+
for (const match of ellipseMatches) {
|
|
3338
|
+
container.children.push(this.parseEllipse(match[0]));
|
|
3339
|
+
}
|
|
3340
|
+
const arcMatches = xml.matchAll(/<hp:arc\b[^>]*(?:\/>|>[\s\S]*?<\/hp:arc>)/g);
|
|
3341
|
+
for (const match of arcMatches) {
|
|
3342
|
+
container.children.push(this.parseArc(match[0]));
|
|
3343
|
+
}
|
|
3344
|
+
const polygonMatches = xml.matchAll(/<hp:polygon\b[^>]*(?:\/>|>[\s\S]*?<\/hp:polygon>)/g);
|
|
3345
|
+
for (const match of polygonMatches) {
|
|
3346
|
+
container.children.push(this.parsePolygon(match[0]));
|
|
3347
|
+
}
|
|
3348
|
+
const curveMatches = xml.matchAll(/<hp:curve\b[^>]*(?:\/>|>[\s\S]*?<\/hp:curve>)/g);
|
|
3349
|
+
for (const match of curveMatches) {
|
|
3350
|
+
container.children.push(this.parseCurve(match[0]));
|
|
3351
|
+
}
|
|
3352
|
+
const picMatches = xml.matchAll(/<hp:pic\b[^>]*>[\s\S]*?<\/hp:pic>/g);
|
|
3353
|
+
for (const match of picMatches) {
|
|
3354
|
+
const image = this.parseImageElement(match[0], content);
|
|
3355
|
+
if (image)
|
|
3356
|
+
container.children.push(image);
|
|
3357
|
+
}
|
|
3358
|
+
const nestedContainerMatches = xml.matchAll(/<hp:container\b[^>]*>[\s\S]*?<\/hp:container>/g);
|
|
3359
|
+
for (const match of nestedContainerMatches) {
|
|
3360
|
+
if (match[0] !== xml) {
|
|
3361
|
+
container.children.push(this.parseContainer(match[0], content));
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
return container;
|
|
3365
|
+
}
|
|
3366
|
+
static parseOle(xml) {
|
|
3367
|
+
const ole = {
|
|
3368
|
+
id: generateId(),
|
|
3369
|
+
};
|
|
3370
|
+
const objectTypeMatch = xml.match(/objectType="([^"]*)"/);
|
|
3371
|
+
if (objectTypeMatch) {
|
|
3372
|
+
const typeMap = {
|
|
3373
|
+
'UNKNOWN': 'Unknown', 'EMBEDDED': 'Embedded', 'LINK': 'Link',
|
|
3374
|
+
'STATIC': 'Static', 'EQUATION': 'Equation'
|
|
3375
|
+
};
|
|
3376
|
+
ole.objectType = typeMap[objectTypeMatch[1].toUpperCase()] || 'Unknown';
|
|
3377
|
+
}
|
|
3378
|
+
const extentXMatch = xml.match(/extentX="(\d+)"/);
|
|
3379
|
+
if (extentXMatch)
|
|
3380
|
+
ole.extentX = parseInt(extentXMatch[1]) / 100;
|
|
3381
|
+
const extentYMatch = xml.match(/extentY="(\d+)"/);
|
|
3382
|
+
if (extentYMatch)
|
|
3383
|
+
ole.extentY = parseInt(extentYMatch[1]) / 100;
|
|
3384
|
+
const binItemMatch = xml.match(/binaryItemIDRef="([^"]*)"/);
|
|
3385
|
+
if (binItemMatch)
|
|
3386
|
+
ole.binItem = binItemMatch[1];
|
|
3387
|
+
const drawAspectMatch = xml.match(/drawAspect="([^"]*)"/);
|
|
3388
|
+
if (drawAspectMatch) {
|
|
3389
|
+
const aspectMap = {
|
|
3390
|
+
'CONTENT': 'Content', 'THUMBNAIL': 'ThumbNail', 'ICON': 'Icon', 'DOCPRINT': 'DocPrint'
|
|
3391
|
+
};
|
|
3392
|
+
ole.drawAspect = aspectMap[drawAspectMatch[1].toUpperCase()] || 'Content';
|
|
3393
|
+
}
|
|
3394
|
+
const hasMonikerMatch = xml.match(/hasMoniker="([^"]*)"/);
|
|
3395
|
+
if (hasMonikerMatch) {
|
|
3396
|
+
ole.hasMoniker = hasMonikerMatch[1] === '1' || hasMonikerMatch[1] === 'true';
|
|
3397
|
+
}
|
|
3398
|
+
const eqBaseLineMatch = xml.match(/eqBaseLine="(-?\d+)"/);
|
|
3399
|
+
if (eqBaseLineMatch)
|
|
3400
|
+
ole.eqBaseLine = parseInt(eqBaseLineMatch[1]) / 100;
|
|
3401
|
+
ole.shapeObject = this.parseShapeObject(xml);
|
|
3402
|
+
return ole;
|
|
3403
|
+
}
|
|
3404
|
+
static parseEquation(xml) {
|
|
3405
|
+
const equation = {
|
|
3406
|
+
id: generateId(),
|
|
3407
|
+
};
|
|
3408
|
+
const lineModeMatch = xml.match(/lineMode="([^"]*)"/);
|
|
3409
|
+
if (lineModeMatch) {
|
|
3410
|
+
equation.lineMode = lineModeMatch[1] === '1' || lineModeMatch[1] === 'true';
|
|
3411
|
+
}
|
|
3412
|
+
const baseUnitMatch = xml.match(/baseUnit="(\d+)"/);
|
|
3413
|
+
if (baseUnitMatch)
|
|
3414
|
+
equation.baseUnit = parseInt(baseUnitMatch[1]);
|
|
3415
|
+
const textColorMatch = xml.match(/textColor="([^"]*)"/);
|
|
3416
|
+
if (textColorMatch)
|
|
3417
|
+
equation.textColor = textColorMatch[1];
|
|
3418
|
+
const baseLineMatch = xml.match(/baseLine="(-?\d+)"/);
|
|
3419
|
+
if (baseLineMatch)
|
|
3420
|
+
equation.baseLine = parseInt(baseLineMatch[1]) / 100;
|
|
3421
|
+
const versionMatch = xml.match(/version="([^"]*)"/);
|
|
3422
|
+
if (versionMatch)
|
|
3423
|
+
equation.version = versionMatch[1];
|
|
3424
|
+
const scriptMatch = xml.match(/<hp:script[^>]*>([^<]*)<\/hp:script>/i);
|
|
3425
|
+
if (scriptMatch) {
|
|
3426
|
+
equation.script = this.decodeXmlEntities(scriptMatch[1]);
|
|
3427
|
+
}
|
|
3428
|
+
equation.shapeObject = this.parseShapeObject(xml);
|
|
3429
|
+
return equation;
|
|
3430
|
+
}
|
|
3431
|
+
static parseTextArt(xml) {
|
|
3432
|
+
const textArt = {
|
|
3433
|
+
id: generateId(),
|
|
3434
|
+
};
|
|
3435
|
+
const textMatch = xml.match(/<hp:textArt[^>]*>[\s\S]*?<hp:text>([^<]*)<\/hp:text>/i);
|
|
3436
|
+
if (textMatch) {
|
|
3437
|
+
textArt.text = this.decodeXmlEntities(textMatch[1]);
|
|
3438
|
+
}
|
|
3439
|
+
const x0Match = xml.match(/x0="(-?\d+)"/);
|
|
3440
|
+
if (x0Match)
|
|
3441
|
+
textArt.x0 = parseInt(x0Match[1]) / 100;
|
|
3442
|
+
const y0Match = xml.match(/y0="(-?\d+)"/);
|
|
3443
|
+
if (y0Match)
|
|
3444
|
+
textArt.y0 = parseInt(y0Match[1]) / 100;
|
|
3445
|
+
const x1Match = xml.match(/x1="(-?\d+)"/);
|
|
3446
|
+
if (x1Match)
|
|
3447
|
+
textArt.x1 = parseInt(x1Match[1]) / 100;
|
|
3448
|
+
const y1Match = xml.match(/y1="(-?\d+)"/);
|
|
3449
|
+
if (y1Match)
|
|
3450
|
+
textArt.y1 = parseInt(y1Match[1]) / 100;
|
|
3451
|
+
const x2Match = xml.match(/x2="(-?\d+)"/);
|
|
3452
|
+
if (x2Match)
|
|
3453
|
+
textArt.x2 = parseInt(x2Match[1]) / 100;
|
|
3454
|
+
const y2Match = xml.match(/y2="(-?\d+)"/);
|
|
3455
|
+
if (y2Match)
|
|
3456
|
+
textArt.y2 = parseInt(y2Match[1]) / 100;
|
|
3457
|
+
const x3Match = xml.match(/x3="(-?\d+)"/);
|
|
3458
|
+
if (x3Match)
|
|
3459
|
+
textArt.x3 = parseInt(x3Match[1]) / 100;
|
|
3460
|
+
const y3Match = xml.match(/y3="(-?\d+)"/);
|
|
3461
|
+
if (y3Match)
|
|
3462
|
+
textArt.y3 = parseInt(y3Match[1]) / 100;
|
|
3463
|
+
const shapeMatch = xml.match(/<hp:textArtShape[^>]*>([\s\S]*?)<\/hp:textArtShape>/i);
|
|
3464
|
+
if (shapeMatch) {
|
|
3465
|
+
textArt.shape = {};
|
|
3466
|
+
const shapeContent = shapeMatch[0];
|
|
3467
|
+
const fontNameMatch = shapeContent.match(/fontName="([^"]*)"/);
|
|
3468
|
+
if (fontNameMatch)
|
|
3469
|
+
textArt.shape.fontName = fontNameMatch[1];
|
|
3470
|
+
const fontStyleMatch = shapeContent.match(/fontStyle="([^"]*)"/);
|
|
3471
|
+
if (fontStyleMatch)
|
|
3472
|
+
textArt.shape.fontStyle = fontStyleMatch[1];
|
|
3473
|
+
const textShapeMatch = shapeContent.match(/textShape="(\d+)"/);
|
|
3474
|
+
if (textShapeMatch)
|
|
3475
|
+
textArt.shape.textShape = parseInt(textShapeMatch[1]);
|
|
3476
|
+
const lineSpacingMatch = shapeContent.match(/lineSpacing="(\d+)"/);
|
|
3477
|
+
if (lineSpacingMatch)
|
|
3478
|
+
textArt.shape.lineSpacing = parseInt(lineSpacingMatch[1]);
|
|
3479
|
+
const charSpacingMatch = shapeContent.match(/charSpacing="(-?\d+)"/);
|
|
3480
|
+
if (charSpacingMatch)
|
|
3481
|
+
textArt.shape.charSpacing = parseInt(charSpacingMatch[1]);
|
|
3482
|
+
}
|
|
3483
|
+
const outlineDataMatch = xml.match(/<hp:outlineData[^>]*>([\s\S]*?)<\/hp:outlineData>/i);
|
|
3484
|
+
if (outlineDataMatch) {
|
|
3485
|
+
textArt.outlineData = [];
|
|
3486
|
+
const pointRegex = /<(?:hp:|hc:)?pt[^>]*x="(-?\d+)"[^>]*y="(-?\d+)"/gi;
|
|
3487
|
+
let pointMatch;
|
|
3488
|
+
while ((pointMatch = pointRegex.exec(outlineDataMatch[1])) !== null) {
|
|
3489
|
+
textArt.outlineData.push({
|
|
3490
|
+
x: parseInt(pointMatch[1]) / 100,
|
|
3491
|
+
y: parseInt(pointMatch[2]) / 100,
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
return textArt;
|
|
3496
|
+
}
|
|
3497
|
+
static parseUnknownObject(xml) {
|
|
3498
|
+
const unknownObj = {
|
|
3499
|
+
id: generateId(),
|
|
3500
|
+
};
|
|
3501
|
+
const ctrlIdMatch = xml.match(/ctrlId="([^"]*)"/);
|
|
3502
|
+
if (ctrlIdMatch)
|
|
3503
|
+
unknownObj.ctrlId = ctrlIdMatch[1];
|
|
3504
|
+
const x0Match = xml.match(/x0="(-?\d+)"/);
|
|
3505
|
+
if (x0Match)
|
|
3506
|
+
unknownObj.x0 = parseInt(x0Match[1]) / 100;
|
|
3507
|
+
const y0Match = xml.match(/y0="(-?\d+)"/);
|
|
3508
|
+
if (y0Match)
|
|
3509
|
+
unknownObj.y0 = parseInt(y0Match[1]) / 100;
|
|
3510
|
+
const x1Match = xml.match(/x1="(-?\d+)"/);
|
|
3511
|
+
if (x1Match)
|
|
3512
|
+
unknownObj.x1 = parseInt(x1Match[1]) / 100;
|
|
3513
|
+
const y1Match = xml.match(/y1="(-?\d+)"/);
|
|
3514
|
+
if (y1Match)
|
|
3515
|
+
unknownObj.y1 = parseInt(y1Match[1]) / 100;
|
|
3516
|
+
const x2Match = xml.match(/x2="(-?\d+)"/);
|
|
3517
|
+
if (x2Match)
|
|
3518
|
+
unknownObj.x2 = parseInt(x2Match[1]) / 100;
|
|
3519
|
+
const y2Match = xml.match(/y2="(-?\d+)"/);
|
|
3520
|
+
if (y2Match)
|
|
3521
|
+
unknownObj.y2 = parseInt(y2Match[1]) / 100;
|
|
3522
|
+
const x3Match = xml.match(/x3="(-?\d+)"/);
|
|
3523
|
+
if (x3Match)
|
|
3524
|
+
unknownObj.x3 = parseInt(x3Match[1]) / 100;
|
|
3525
|
+
const y3Match = xml.match(/y3="(-?\d+)"/);
|
|
3526
|
+
if (y3Match)
|
|
3527
|
+
unknownObj.y3 = parseInt(y3Match[1]) / 100;
|
|
3528
|
+
unknownObj.shapeObject = this.parseShapeObject(xml);
|
|
3529
|
+
unknownObj.drawingObject = this.parseDrawingObject(xml);
|
|
3530
|
+
return unknownObj;
|
|
3531
|
+
}
|
|
3532
|
+
static parseFormObject(xml) {
|
|
3533
|
+
const formObject = {};
|
|
3534
|
+
const nameMatch = xml.match(/\bname="([^"]*)"/);
|
|
3535
|
+
if (nameMatch)
|
|
3536
|
+
formObject.name = nameMatch[1];
|
|
3537
|
+
const foreColorMatch = xml.match(/foreColor="([^"]*)"/);
|
|
3538
|
+
if (foreColorMatch)
|
|
3539
|
+
formObject.foreColor = foreColorMatch[1];
|
|
3540
|
+
const backColorMatch = xml.match(/backColor="([^"]*)"/);
|
|
3541
|
+
if (backColorMatch)
|
|
3542
|
+
formObject.backColor = backColorMatch[1];
|
|
3543
|
+
const groupNameMatch = xml.match(/groupName="([^"]*)"/);
|
|
3544
|
+
if (groupNameMatch)
|
|
3545
|
+
formObject.groupName = groupNameMatch[1];
|
|
3546
|
+
const tabStopMatch = xml.match(/tabStop="([^"]*)"/);
|
|
3547
|
+
if (tabStopMatch) {
|
|
3548
|
+
formObject.tabStop = tabStopMatch[1] === '1' || tabStopMatch[1] === 'true';
|
|
3549
|
+
}
|
|
3550
|
+
const tabOrderMatch = xml.match(/tabOrder="(\d+)"/);
|
|
3551
|
+
if (tabOrderMatch)
|
|
3552
|
+
formObject.tabOrder = parseInt(tabOrderMatch[1]);
|
|
3553
|
+
const enabledMatch = xml.match(/enabled="([^"]*)"/);
|
|
3554
|
+
if (enabledMatch) {
|
|
3555
|
+
formObject.enabled = enabledMatch[1] === '1' || enabledMatch[1] === 'true';
|
|
3556
|
+
}
|
|
3557
|
+
const borderTypeMatch = xml.match(/borderType="(\d+)"/);
|
|
3558
|
+
if (borderTypeMatch)
|
|
3559
|
+
formObject.borderType = parseInt(borderTypeMatch[1]);
|
|
3560
|
+
const drawFrameMatch = xml.match(/drawFrame="([^"]*)"/);
|
|
3561
|
+
if (drawFrameMatch) {
|
|
3562
|
+
formObject.drawFrame = drawFrameMatch[1] === '1' || drawFrameMatch[1] === 'true';
|
|
3563
|
+
}
|
|
3564
|
+
const printableMatch = xml.match(/printable="([^"]*)"/);
|
|
3565
|
+
if (printableMatch) {
|
|
3566
|
+
formObject.printable = printableMatch[1] === '1' || printableMatch[1] === 'true';
|
|
3567
|
+
}
|
|
3568
|
+
const formCharShapeMatch = xml.match(/<(?:hp:|hc:)?formCharShape[^>]*>/i);
|
|
3569
|
+
if (formCharShapeMatch) {
|
|
3570
|
+
const fcs = formCharShapeMatch[0];
|
|
3571
|
+
formObject.formCharShape = {};
|
|
3572
|
+
const charShapeMatch = fcs.match(/charPrIDRef="(\d+)"/);
|
|
3573
|
+
if (charShapeMatch)
|
|
3574
|
+
formObject.formCharShape.charShape = parseInt(charShapeMatch[1]);
|
|
3575
|
+
const followContextMatch = fcs.match(/followContext="([^"]*)"/);
|
|
3576
|
+
if (followContextMatch) {
|
|
3577
|
+
formObject.formCharShape.followContext = followContextMatch[1] === '1' || followContextMatch[1] === 'true';
|
|
3578
|
+
}
|
|
3579
|
+
const autoSizeMatch = fcs.match(/autoSize="([^"]*)"/);
|
|
3580
|
+
if (autoSizeMatch) {
|
|
3581
|
+
formObject.formCharShape.autoSize = autoSizeMatch[1] === '1' || autoSizeMatch[1] === 'true';
|
|
3582
|
+
}
|
|
3583
|
+
const wordWrapMatch = fcs.match(/wordWrap="([^"]*)"/);
|
|
3584
|
+
if (wordWrapMatch) {
|
|
3585
|
+
formObject.formCharShape.wordWrap = wordWrapMatch[1] === '1' || wordWrapMatch[1] === 'true';
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
const buttonSetMatch = xml.match(/<(?:hp:|hc:)?buttonSet[^>]*>([\s\S]*?)<\/(?:hp:|hc:)?buttonSet>/i);
|
|
3589
|
+
if (buttonSetMatch) {
|
|
3590
|
+
formObject.buttonSet = {};
|
|
3591
|
+
const bsContent = buttonSetMatch[0];
|
|
3592
|
+
const captionMatch = bsContent.match(/caption="([^"]*)"/);
|
|
3593
|
+
if (captionMatch)
|
|
3594
|
+
formObject.buttonSet.caption = captionMatch[1];
|
|
3595
|
+
const valueMatch = bsContent.match(/\bvalue="([^"]*)"/);
|
|
3596
|
+
if (valueMatch)
|
|
3597
|
+
formObject.buttonSet.value = valueMatch[1];
|
|
3598
|
+
const radioGroupNameMatch = bsContent.match(/radioGroupName="([^"]*)"/);
|
|
3599
|
+
if (radioGroupNameMatch)
|
|
3600
|
+
formObject.buttonSet.radioGroupName = radioGroupNameMatch[1];
|
|
3601
|
+
const triStateMatch = bsContent.match(/triState="([^"]*)"/);
|
|
3602
|
+
if (triStateMatch) {
|
|
3603
|
+
formObject.buttonSet.triState = triStateMatch[1] === '1' || triStateMatch[1] === 'true';
|
|
3604
|
+
}
|
|
3605
|
+
const backStyleMatch = bsContent.match(/backStyle="([^"]*)"/);
|
|
3606
|
+
if (backStyleMatch)
|
|
3607
|
+
formObject.buttonSet.backStyle = backStyleMatch[1];
|
|
3608
|
+
}
|
|
3609
|
+
return formObject;
|
|
3610
|
+
}
|
|
3611
|
+
static parseButton(xml) {
|
|
3612
|
+
const button = {
|
|
3613
|
+
id: generateId(),
|
|
3614
|
+
};
|
|
3615
|
+
button.shapeObject = this.parseShapeObject(xml);
|
|
3616
|
+
button.formObject = this.parseFormObject(xml);
|
|
3617
|
+
return button;
|
|
3618
|
+
}
|
|
3619
|
+
static parseRadioButton(xml) {
|
|
3620
|
+
const radioButton = {
|
|
3621
|
+
id: generateId(),
|
|
3622
|
+
};
|
|
3623
|
+
radioButton.shapeObject = this.parseShapeObject(xml);
|
|
3624
|
+
radioButton.formObject = this.parseFormObject(xml);
|
|
3625
|
+
return radioButton;
|
|
3626
|
+
}
|
|
3627
|
+
static parseCheckButton(xml) {
|
|
3628
|
+
const checkButton = {
|
|
3629
|
+
id: generateId(),
|
|
3630
|
+
};
|
|
3631
|
+
checkButton.shapeObject = this.parseShapeObject(xml);
|
|
3632
|
+
checkButton.formObject = this.parseFormObject(xml);
|
|
3633
|
+
return checkButton;
|
|
3634
|
+
}
|
|
3635
|
+
static parseComboBox(xml) {
|
|
3636
|
+
const comboBox = {
|
|
3637
|
+
id: generateId(),
|
|
3638
|
+
};
|
|
3639
|
+
const listBoxRowsMatch = xml.match(/listBoxRows="(\d+)"/);
|
|
3640
|
+
if (listBoxRowsMatch)
|
|
3641
|
+
comboBox.listBoxRows = parseInt(listBoxRowsMatch[1]);
|
|
3642
|
+
const listBoxWidthMatch = xml.match(/listBoxWidth="(\d+)"/);
|
|
3643
|
+
if (listBoxWidthMatch)
|
|
3644
|
+
comboBox.listBoxWidth = parseInt(listBoxWidthMatch[1]) / 100;
|
|
3645
|
+
const textMatch = xml.match(/\btext="([^"]*)"/);
|
|
3646
|
+
if (textMatch)
|
|
3647
|
+
comboBox.text = textMatch[1];
|
|
3648
|
+
const editEnableMatch = xml.match(/editEnable="([^"]*)"/);
|
|
3649
|
+
if (editEnableMatch) {
|
|
3650
|
+
comboBox.editEnable = editEnableMatch[1] === '1' || editEnableMatch[1] === 'true';
|
|
3651
|
+
}
|
|
3652
|
+
comboBox.shapeObject = this.parseShapeObject(xml);
|
|
3653
|
+
comboBox.formObject = this.parseFormObject(xml);
|
|
3654
|
+
return comboBox;
|
|
3655
|
+
}
|
|
3656
|
+
static parseEdit(xml) {
|
|
3657
|
+
const edit = {
|
|
3658
|
+
id: generateId(),
|
|
3659
|
+
};
|
|
3660
|
+
const multiLineMatch = xml.match(/multiLine="([^"]*)"/);
|
|
3661
|
+
if (multiLineMatch) {
|
|
3662
|
+
edit.multiLine = multiLineMatch[1] === '1' || multiLineMatch[1] === 'true';
|
|
3663
|
+
}
|
|
3664
|
+
const passwordCharMatch = xml.match(/passwordChar="([^"]*)"/);
|
|
3665
|
+
if (passwordCharMatch)
|
|
3666
|
+
edit.passwordChar = passwordCharMatch[1];
|
|
3667
|
+
const maxLengthMatch = xml.match(/maxLength="(\d+)"/);
|
|
3668
|
+
if (maxLengthMatch)
|
|
3669
|
+
edit.maxLength = parseInt(maxLengthMatch[1]);
|
|
3670
|
+
const scrollBarsMatch = xml.match(/scrollBars="([^"]*)"/);
|
|
3671
|
+
if (scrollBarsMatch) {
|
|
3672
|
+
edit.scrollBars = scrollBarsMatch[1] === '1' || scrollBarsMatch[1] === 'true';
|
|
3673
|
+
}
|
|
3674
|
+
const tabKeyBehaviorMatch = xml.match(/tabKeyBehavior="([^"]*)"/);
|
|
3675
|
+
if (tabKeyBehaviorMatch)
|
|
3676
|
+
edit.tabKeyBehavior = tabKeyBehaviorMatch[1];
|
|
3677
|
+
const numberMatch = xml.match(/\bnumber="([^"]*)"/);
|
|
3678
|
+
if (numberMatch) {
|
|
3679
|
+
edit.number = numberMatch[1] === '1' || numberMatch[1] === 'true';
|
|
3680
|
+
}
|
|
3681
|
+
const readOnlyMatch = xml.match(/readOnly="([^"]*)"/);
|
|
3682
|
+
if (readOnlyMatch) {
|
|
3683
|
+
edit.readOnly = readOnlyMatch[1] === '1' || readOnlyMatch[1] === 'true';
|
|
3684
|
+
}
|
|
3685
|
+
const alignTextMatch = xml.match(/alignText="([^"]*)"/);
|
|
3686
|
+
if (alignTextMatch)
|
|
3687
|
+
edit.alignText = alignTextMatch[1];
|
|
3688
|
+
const editTextMatch = xml.match(/<(?:hp:|hc:)?editText[^>]*>([^<]*)<\/(?:hp:|hc:)?editText>/i);
|
|
3689
|
+
if (editTextMatch) {
|
|
3690
|
+
edit.text = this.decodeXmlEntities(editTextMatch[1]);
|
|
3691
|
+
}
|
|
3692
|
+
edit.shapeObject = this.parseShapeObject(xml);
|
|
3693
|
+
edit.formObject = this.parseFormObject(xml);
|
|
3694
|
+
return edit;
|
|
3695
|
+
}
|
|
3696
|
+
static parseListBox(xml) {
|
|
3697
|
+
const listBox = {
|
|
3698
|
+
id: generateId(),
|
|
3699
|
+
};
|
|
3700
|
+
const textMatch = xml.match(/\btext="([^"]*)"/);
|
|
3701
|
+
if (textMatch)
|
|
3702
|
+
listBox.text = textMatch[1];
|
|
3703
|
+
const itemHeightMatch = xml.match(/itemHeight="(\d+)"/);
|
|
3704
|
+
if (itemHeightMatch)
|
|
3705
|
+
listBox.itemHeight = parseInt(itemHeightMatch[1]) / 100;
|
|
3706
|
+
const topIndexMatch = xml.match(/topIndex="(\d+)"/);
|
|
3707
|
+
if (topIndexMatch)
|
|
3708
|
+
listBox.topIndex = parseInt(topIndexMatch[1]);
|
|
3709
|
+
listBox.shapeObject = this.parseShapeObject(xml);
|
|
3710
|
+
listBox.formObject = this.parseFormObject(xml);
|
|
3711
|
+
return listBox;
|
|
3712
|
+
}
|
|
3713
|
+
static parseScrollBar(xml) {
|
|
3714
|
+
const scrollBar = {
|
|
3715
|
+
id: generateId(),
|
|
3716
|
+
};
|
|
3717
|
+
const delayMatch = xml.match(/delay="(\d+)"/);
|
|
3718
|
+
if (delayMatch)
|
|
3719
|
+
scrollBar.delay = parseInt(delayMatch[1]);
|
|
3720
|
+
const largeChangeMatch = xml.match(/largeChange="(\d+)"/);
|
|
3721
|
+
if (largeChangeMatch)
|
|
3722
|
+
scrollBar.largeChange = parseInt(largeChangeMatch[1]);
|
|
3723
|
+
const smallChangeMatch = xml.match(/smallChange="(\d+)"/);
|
|
3724
|
+
if (smallChangeMatch)
|
|
3725
|
+
scrollBar.smallChange = parseInt(smallChangeMatch[1]);
|
|
3726
|
+
const minMatch = xml.match(/\bmin="(\d+)"/);
|
|
3727
|
+
if (minMatch)
|
|
3728
|
+
scrollBar.min = parseInt(minMatch[1]);
|
|
3729
|
+
const maxMatch = xml.match(/\bmax="(\d+)"/);
|
|
3730
|
+
if (maxMatch)
|
|
3731
|
+
scrollBar.max = parseInt(maxMatch[1]);
|
|
3732
|
+
const pageMatch = xml.match(/\bpage="(\d+)"/);
|
|
3733
|
+
if (pageMatch)
|
|
3734
|
+
scrollBar.page = parseInt(pageMatch[1]);
|
|
3735
|
+
const valueMatch = xml.match(/\bvalue="(\d+)"/);
|
|
3736
|
+
if (valueMatch)
|
|
3737
|
+
scrollBar.value = parseInt(valueMatch[1]);
|
|
3738
|
+
const typeMatch = xml.match(/\btype="([^"]*)"/);
|
|
3739
|
+
if (typeMatch)
|
|
3740
|
+
scrollBar.type = typeMatch[1];
|
|
3741
|
+
scrollBar.shapeObject = this.parseShapeObject(xml);
|
|
3742
|
+
scrollBar.formObject = this.parseFormObject(xml);
|
|
3743
|
+
return scrollBar;
|
|
3744
|
+
}
|
|
3745
|
+
static parseCompatibleDocument(xml) {
|
|
3746
|
+
const compatDocMatch = xml.match(/<hh:compatibleDocument[^>]*>([\s\S]*?)<\/hh:compatibleDocument>/i);
|
|
3747
|
+
if (!compatDocMatch)
|
|
3748
|
+
return undefined;
|
|
3749
|
+
const content = compatDocMatch[0];
|
|
3750
|
+
const compatDoc = {};
|
|
3751
|
+
const targetProgramMatch = content.match(/targetProgram="([^"]*)"/);
|
|
3752
|
+
if (targetProgramMatch) {
|
|
3753
|
+
const progMap = {
|
|
3754
|
+
'NONE': 'None', 'HWP70': 'Hwp70', 'WORD': 'Word'
|
|
3755
|
+
};
|
|
3756
|
+
compatDoc.targetProgram = progMap[targetProgramMatch[1].toUpperCase()] || 'None';
|
|
3757
|
+
}
|
|
3758
|
+
const layoutCompatMatch = content.match(/<hh:layoutCompatibility[^>]*(?:\/>|([\s\S]*?)<\/hh:layoutCompatibility>)/i);
|
|
3759
|
+
if (layoutCompatMatch) {
|
|
3760
|
+
const lcContent = layoutCompatMatch[0];
|
|
3761
|
+
const lc = {};
|
|
3762
|
+
const boolFlags = [
|
|
3763
|
+
'applyFontWeightToBold', 'useInnerUnderline', 'fixedUnderlineWidth',
|
|
3764
|
+
'doNotApplyStrikeout', 'useLowercaseStrikeout', 'extendLineheightToOffset',
|
|
3765
|
+
'treatQuotationAsLatin', 'doNotAlignWhitespaceOnRight', 'doNotAdjustWordInJustify',
|
|
3766
|
+
'baseCharUnitOnEAsian', 'baseCharUnitOfIndentOnFirstChar', 'adjustLineheightToFont',
|
|
3767
|
+
'adjustBaselineInFixedLinespacing', 'excludeOverlappingParaSpacing',
|
|
3768
|
+
'applyNextspacingOfLastPara', 'applyAtLeastToPercent100Pct',
|
|
3769
|
+
'doNotApplyAutoSpaceEAsianEng', 'doNotApplyAutoSpaceEAsianNum',
|
|
3770
|
+
'adjustParaBorderfillToSpacing', 'connectParaBorderfillOfEqualBorder',
|
|
3771
|
+
'adjustParaBorderOffsetWithBorder', 'extendLineheightToParaBorderOffset',
|
|
3772
|
+
'applyParaBorderToOutside', 'baseLinespacingOnLinegrid', 'applyCharSpacingToCharGrid',
|
|
3773
|
+
'doNotApplyGridInHeaderfooter', 'extendHeaderfooterToBody',
|
|
3774
|
+
'adjustEndnotePositionToFootnote', 'doNotApplyImageEffect', 'doNotApplyShapeComment',
|
|
3775
|
+
'doNotAdjustEmptyAnchorLine', 'overlapBothAllowOverlap', 'doNotApplyVertOffsetOfForward',
|
|
3776
|
+
'extendVertLimitToPageMargins', 'doNotHoldAnchorOfTable', 'doNotFormattingAtBeneathAnchor',
|
|
3777
|
+
'doNotApplyExtensionCharCompose'
|
|
3778
|
+
];
|
|
3779
|
+
for (const flag of boolFlags) {
|
|
3780
|
+
const regex = new RegExp(`${flag}="([^"]*)"`, 'i');
|
|
3781
|
+
const match = lcContent.match(regex);
|
|
3782
|
+
if (match) {
|
|
3783
|
+
lc[flag] = match[1] === '1' || match[1] === 'true';
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
compatDoc.layoutCompatibility = lc;
|
|
3787
|
+
}
|
|
3788
|
+
return compatDoc;
|
|
3789
|
+
}
|
|
3790
|
+
static async parseBinDataStorage(zip, content) {
|
|
3791
|
+
const binDataPath = 'Contents/content.hpf';
|
|
3792
|
+
const binDataXml = await this.readXmlFile(zip, binDataPath);
|
|
3793
|
+
if (binDataXml) {
|
|
3794
|
+
const binDataRegex = /<(?:hp:|hpf:)?binData[^>]*id="([^"]*)"[^>]*>([\s\S]*?)<\/(?:hp:|hpf:)?binData>/gi;
|
|
3795
|
+
let match;
|
|
3796
|
+
while ((match = binDataRegex.exec(binDataXml)) !== null) {
|
|
3797
|
+
const binData = {
|
|
3798
|
+
id: match[1],
|
|
3799
|
+
data: match[2].trim(),
|
|
3800
|
+
};
|
|
3801
|
+
const sizeMatch = match[0].match(/size="(\d+)"/);
|
|
3802
|
+
if (sizeMatch)
|
|
3803
|
+
binData.size = parseInt(sizeMatch[1]);
|
|
3804
|
+
const encodingMatch = match[0].match(/encoding="([^"]*)"/);
|
|
3805
|
+
if (encodingMatch && encodingMatch[1].toUpperCase() === 'BASE64') {
|
|
3806
|
+
binData.encoding = 'Base64';
|
|
3807
|
+
}
|
|
3808
|
+
const compressMatch = match[0].match(/compress="([^"]*)"/);
|
|
3809
|
+
if (compressMatch) {
|
|
3810
|
+
binData.compress = compressMatch[1] === '1' || compressMatch[1] === 'true';
|
|
3811
|
+
}
|
|
3812
|
+
content.binData.set(binData.id, binData);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
const binDataFolder = zip.folder('BinData');
|
|
3816
|
+
if (binDataFolder) {
|
|
3817
|
+
const binFiles = Object.keys(zip.files).filter((f) => f.startsWith('BinData/') && !f.endsWith('/'));
|
|
3818
|
+
for (const binPath of binFiles) {
|
|
3819
|
+
const file = zip.file(binPath);
|
|
3820
|
+
if (!file)
|
|
3821
|
+
continue;
|
|
3822
|
+
const data = await file.async('base64');
|
|
3823
|
+
const fileName = binPath.split('/').pop() || '';
|
|
3824
|
+
const fileId = fileName.replace(/\.[^.]+$/, '');
|
|
3825
|
+
if (!content.binData.has(fileId)) {
|
|
3826
|
+
content.binData.set(fileId, {
|
|
3827
|
+
id: fileId,
|
|
3828
|
+
data: data,
|
|
3829
|
+
encoding: 'Base64',
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
exports.HwpxParser = HwpxParser;
|
|
3837
|
+
HwpxParser.styles = {
|
|
3838
|
+
charShapes: new Map(),
|
|
3839
|
+
paraShapes: new Map(),
|
|
3840
|
+
fonts: new Map(),
|
|
3841
|
+
fontsByLang: new Map(),
|
|
3842
|
+
borderFills: new Map(),
|
|
3843
|
+
tabDefs: new Map(),
|
|
3844
|
+
numberings: new Map(),
|
|
3845
|
+
bullets: new Map(),
|
|
3846
|
+
styles: new Map(),
|
|
3847
|
+
memoShapes: new Map(),
|
|
3848
|
+
};
|