hwp-convert 1.0.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/CHANGELOG.md +185 -0
- package/LICENSE +25 -0
- package/NOTICE +23 -0
- package/README.md +338 -0
- package/dist/browser/hwp-convert.browser.mjs +20677 -0
- package/dist/browser/hwp-convert.browser.mjs.map +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +267 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/errors.d.ts +9 -0
- package/dist/lib/errors.js +18 -0
- package/dist/lib/hwp/binData.d.ts +15 -0
- package/dist/lib/hwp/binData.js +64 -0
- package/dist/lib/hwp/bodyText.d.ts +31 -0
- package/dist/lib/hwp/bodyText.js +208 -0
- package/dist/lib/hwp/byteReader.d.ts +40 -0
- package/dist/lib/hwp/byteReader.js +116 -0
- package/dist/lib/hwp/cfbReader.d.ts +44 -0
- package/dist/lib/hwp/cfbReader.js +134 -0
- package/dist/lib/hwp/control.d.ts +17 -0
- package/dist/lib/hwp/control.js +290 -0
- package/dist/lib/hwp/converter.d.ts +22 -0
- package/dist/lib/hwp/converter.js +41 -0
- package/dist/lib/hwp/docInfo.d.ts +26 -0
- package/dist/lib/hwp/docInfo.js +396 -0
- package/dist/lib/hwp/fileHeader.d.ts +42 -0
- package/dist/lib/hwp/fileHeader.js +66 -0
- package/dist/lib/hwp/htmlReader.d.ts +17 -0
- package/dist/lib/hwp/htmlReader.js +602 -0
- package/dist/lib/hwp/hwpxBuilder.d.ts +19 -0
- package/dist/lib/hwp/hwpxBuilder.js +633 -0
- package/dist/lib/hwp/index.d.ts +68 -0
- package/dist/lib/hwp/index.js +149 -0
- package/dist/lib/hwp/mdReader.d.ts +16 -0
- package/dist/lib/hwp/mdReader.js +485 -0
- package/dist/lib/hwp/mdWriter.d.ts +23 -0
- package/dist/lib/hwp/mdWriter.js +182 -0
- package/dist/lib/hwp/owpml.d.ts +33 -0
- package/dist/lib/hwp/owpml.js +86 -0
- package/dist/lib/hwp/record.d.ts +24 -0
- package/dist/lib/hwp/record.js +59 -0
- package/dist/lib/hwp/tags.d.ts +115 -0
- package/dist/lib/hwp/tags.js +217 -0
- package/dist/lib/hwp/types.d.ts +214 -0
- package/dist/lib/hwp/types.js +5 -0
- package/dist/lib/hwpxReader.d.ts +60 -0
- package/dist/lib/hwpxReader.js +1104 -0
- package/dist/lib/types.d.ts +47 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/writer.d.ts +19 -0
- package/dist/lib/writer.js +149 -0
- package/package.json +94 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HwpDocument IR → HWPX(OWPML) 패키지 빌더 (스타일 보존 포함).
|
|
3
|
+
*
|
|
4
|
+
* - DocInfo 의 fontFaces / charShapes / paraShapes / styles 를 header.xml refList 로 매핑
|
|
5
|
+
* - paragraph: paraPrIDRef = paraShapeId, styleIDRef = styleId
|
|
6
|
+
* - run: charPrIDRef = charShapeId
|
|
7
|
+
* - 표/이미지 + BinData 패키징 + manifest 등록
|
|
8
|
+
*
|
|
9
|
+
* 1차 포팅 한계: BorderFill/Numbering/TabDef 는 paraShape 의 참조 ID 만 보존하고
|
|
10
|
+
* 실제 정의는 default(0) 로 둠. 추후 단계에서 정의 자체도 옮길 예정.
|
|
11
|
+
*/
|
|
12
|
+
import JSZip from "jszip";
|
|
13
|
+
import { detectImageMime } from "./binData.js";
|
|
14
|
+
import { MIMETYPE, OWPML_NS, DEFAULT_LINESEG, SEC_PR_XML, makeParaId, escapeXml, } from "./owpml.js";
|
|
15
|
+
const NS_OPF = "http://www.idpf.org/2007/opf/";
|
|
16
|
+
const NS_DC = "http://purl.org/dc/elements/1.1/";
|
|
17
|
+
const NS_OASIS_CONTAINER = "urn:oasis:names:tc:opendocument:xmlns:container";
|
|
18
|
+
const NS_OASIS_MANIFEST = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0";
|
|
19
|
+
const LANG_NAMES = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
|
|
20
|
+
export async function buildHwpxFromDocument(doc, options) {
|
|
21
|
+
const zip = new JSZip();
|
|
22
|
+
zip.file("mimetype", MIMETYPE, { compression: "STORE" });
|
|
23
|
+
// BinData 매니페스트 항목 사전 구성
|
|
24
|
+
const binEntries = [];
|
|
25
|
+
for (const [storageId, { data, extension }] of doc.binData) {
|
|
26
|
+
const ext = extension.toLowerCase();
|
|
27
|
+
binEntries.push({
|
|
28
|
+
id: `image${storageId}`,
|
|
29
|
+
href: `BinData/image${storageId}.${ext}`,
|
|
30
|
+
mediaType: detectImageMime(ext),
|
|
31
|
+
data,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// META-INF/container.xml
|
|
35
|
+
zip.file("META-INF/container.xml", `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
36
|
+
`<container xmlns="${NS_OASIS_CONTAINER}">` +
|
|
37
|
+
`<rootfiles>` +
|
|
38
|
+
`<rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>` +
|
|
39
|
+
`</rootfiles>` +
|
|
40
|
+
`</container>`);
|
|
41
|
+
// META-INF/manifest.xml
|
|
42
|
+
const manifestEntries = [
|
|
43
|
+
`<manifest:file-entry manifest:full-path="/" manifest:media-type="application/hwpml-package+xml"/>`,
|
|
44
|
+
`<manifest:file-entry manifest:full-path="version.xml" manifest:media-type="application/xml"/>`,
|
|
45
|
+
`<manifest:file-entry manifest:full-path="settings.xml" manifest:media-type="application/xml"/>`,
|
|
46
|
+
`<manifest:file-entry manifest:full-path="Contents/content.hpf" manifest:media-type="application/hwpml-package+xml"/>`,
|
|
47
|
+
`<manifest:file-entry manifest:full-path="Contents/header.xml" manifest:media-type="application/xml"/>`,
|
|
48
|
+
];
|
|
49
|
+
for (let i = 0; i < doc.sections.length; i++) {
|
|
50
|
+
manifestEntries.push(`<manifest:file-entry manifest:full-path="Contents/section${i}.xml" manifest:media-type="application/xml"/>`);
|
|
51
|
+
}
|
|
52
|
+
for (const e of binEntries) {
|
|
53
|
+
manifestEntries.push(`<manifest:file-entry manifest:full-path="${e.href}" manifest:media-type="${e.mediaType}"/>`);
|
|
54
|
+
}
|
|
55
|
+
zip.file("META-INF/manifest.xml", `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
56
|
+
`<manifest:manifest xmlns:manifest="${NS_OASIS_MANIFEST}">` +
|
|
57
|
+
manifestEntries.join("") +
|
|
58
|
+
`</manifest:manifest>`);
|
|
59
|
+
// version.xml
|
|
60
|
+
zip.file("version.xml", `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
61
|
+
`<ha:HCFVersion xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app" ha:targetApplication="WORDPROCESSOR" ha:major="${doc.header.version.major}" ha:minor="${doc.header.version.minor}" ha:micro="${doc.header.version.build}" ha:buildNumber="${doc.header.version.revision}"/>`);
|
|
62
|
+
// settings.xml
|
|
63
|
+
zip.file("settings.xml", `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
64
|
+
`<ha:HWPApplicationSetting xmlns:ha="http://www.hancom.co.kr/hwpml/2011/app">` +
|
|
65
|
+
`<ha:CaretPosition ha:listIDRef="0" ha:paraIDRef="0" ha:pos="0"/>` +
|
|
66
|
+
`</ha:HWPApplicationSetting>`);
|
|
67
|
+
// OPF 매니페스트 + spine
|
|
68
|
+
const opfManifest = [
|
|
69
|
+
`<opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>`,
|
|
70
|
+
];
|
|
71
|
+
for (let i = 0; i < doc.sections.length; i++) {
|
|
72
|
+
opfManifest.push(`<opf:item id="section${i}" href="Contents/section${i}.xml" media-type="application/xml"/>`);
|
|
73
|
+
}
|
|
74
|
+
for (const e of binEntries) {
|
|
75
|
+
opfManifest.push(`<opf:item id="${e.id}" href="${e.href}" media-type="${e.mediaType}" isEmbeded="1"/>`);
|
|
76
|
+
}
|
|
77
|
+
const spineRefs = `<opf:itemref idref="header" linear="yes"/>` +
|
|
78
|
+
doc.sections.map((_, i) => `<opf:itemref idref="section${i}" linear="yes"/>`).join("");
|
|
79
|
+
zip.file("Contents/content.hpf", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n` +
|
|
80
|
+
`<opf:package ${OWPML_NS} version="" unique-identifier="" id="">` +
|
|
81
|
+
`<opf:metadata>` +
|
|
82
|
+
`<dc:title>${escapeXml(options?.title ?? "")}</dc:title>` +
|
|
83
|
+
`<dc:creator>${escapeXml(options?.creator ?? "")}</dc:creator>` +
|
|
84
|
+
`<dc:format>application/hwpml-package+xml</dc:format>` +
|
|
85
|
+
`</opf:metadata>` +
|
|
86
|
+
`<opf:manifest>` +
|
|
87
|
+
opfManifest.join("") +
|
|
88
|
+
`</opf:manifest>` +
|
|
89
|
+
`<opf:spine>` +
|
|
90
|
+
spineRefs +
|
|
91
|
+
`</opf:spine>` +
|
|
92
|
+
`</opf:package>`);
|
|
93
|
+
// header.xml — DocInfo 기반 풀 빌드
|
|
94
|
+
zip.file("Contents/header.xml", buildHeaderXmlFromDocInfo(doc.docInfo, doc.sections.length));
|
|
95
|
+
// 섹션
|
|
96
|
+
for (let i = 0; i < doc.sections.length; i++) {
|
|
97
|
+
zip.file(`Contents/section${i}.xml`, buildSectionXml(doc.sections[i].paragraphs, binEntries));
|
|
98
|
+
}
|
|
99
|
+
// BinData
|
|
100
|
+
for (const e of binEntries) {
|
|
101
|
+
zip.file(e.href, e.data);
|
|
102
|
+
}
|
|
103
|
+
// Preview/PrvText.txt — 다른 HWP 뷰어 호환을 위한 평문 미리보기
|
|
104
|
+
zip.file("Preview/PrvText.txt", buildPrvText(doc));
|
|
105
|
+
return await zip.generateAsync({ type: "uint8array" });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 한컴 HWP/HWPX 의 Preview/PrvText.txt 형식을 따른 미리보기 평문 생성.
|
|
109
|
+
* - 셀은 "<셀텍스트 >" 로 감싸 행 단위로 나열
|
|
110
|
+
* - 행 사이는 \r\n
|
|
111
|
+
* - 일반 문단은 그대로
|
|
112
|
+
*/
|
|
113
|
+
function buildPrvText(doc) {
|
|
114
|
+
const lines = [];
|
|
115
|
+
for (const section of doc.sections) {
|
|
116
|
+
for (const para of section.paragraphs) {
|
|
117
|
+
collectPrvLines(para, lines);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// 약 2KB 까지만 보존 (Hancom 제한)
|
|
121
|
+
return lines.join("\r\n").slice(0, 2000);
|
|
122
|
+
}
|
|
123
|
+
function collectPrvLines(para, lines) {
|
|
124
|
+
if (para.text.length > 0) {
|
|
125
|
+
lines.push(para.text);
|
|
126
|
+
}
|
|
127
|
+
for (const ctrl of para.controls) {
|
|
128
|
+
if (ctrl.kind === "table") {
|
|
129
|
+
// 행별로 셀을 < ... > 로 감싸 join
|
|
130
|
+
const rows = Array.from({ length: ctrl.rowCount }, () => []);
|
|
131
|
+
for (const cell of ctrl.cells) {
|
|
132
|
+
if (cell.row >= 0 && cell.row < ctrl.rowCount)
|
|
133
|
+
rows[cell.row].push(cell);
|
|
134
|
+
}
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
row.sort((a, b) => a.col - b.col);
|
|
137
|
+
const cellTexts = row.map((cell) => {
|
|
138
|
+
const inner = cell.paragraphs
|
|
139
|
+
.map((q) => {
|
|
140
|
+
const buf = [];
|
|
141
|
+
collectPrvLines(q, buf);
|
|
142
|
+
return buf.join(" ");
|
|
143
|
+
})
|
|
144
|
+
.join(" ");
|
|
145
|
+
return `<${inner} >`;
|
|
146
|
+
});
|
|
147
|
+
if (cellTexts.length > 0)
|
|
148
|
+
lines.push(cellTexts.join(""));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (ctrl.kind === "header" ||
|
|
152
|
+
ctrl.kind === "footer" ||
|
|
153
|
+
ctrl.kind === "footnote") {
|
|
154
|
+
for (const q of ctrl.paragraphs)
|
|
155
|
+
collectPrvLines(q, lines);
|
|
156
|
+
}
|
|
157
|
+
else if (ctrl.kind === "equation" && ctrl.script.length > 0) {
|
|
158
|
+
lines.push(ctrl.script);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ============================================================
|
|
163
|
+
// header.xml 빌드 (DocInfo → refList)
|
|
164
|
+
// ============================================================
|
|
165
|
+
function buildHeaderXmlFromDocInfo(docInfo, secCnt) {
|
|
166
|
+
const fontfacesXml = buildFontfacesXml(docInfo.fontFaces);
|
|
167
|
+
const borderFillsXml = buildBorderFillsXml(docInfo.borderFills);
|
|
168
|
+
const charPropsXml = buildCharPropertiesXml(docInfo.charShapes);
|
|
169
|
+
const tabDefsXml = buildTabDefsXml(docInfo.tabDefs);
|
|
170
|
+
const numberingsXml = buildNumberingsXml(docInfo.numberings);
|
|
171
|
+
const bulletsXml = buildBulletsXml(docInfo.bullets);
|
|
172
|
+
const paraPropsXml = buildParaPropertiesXml(docInfo.paraShapes);
|
|
173
|
+
const stylesXml = buildStylesXml(docInfo.styles);
|
|
174
|
+
return (`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n` +
|
|
175
|
+
`<hh:head ${OWPML_NS} version="1.5" secCnt="${Math.max(1, secCnt)}">` +
|
|
176
|
+
`<hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>` +
|
|
177
|
+
`<hh:refList>` +
|
|
178
|
+
fontfacesXml +
|
|
179
|
+
borderFillsXml +
|
|
180
|
+
charPropsXml +
|
|
181
|
+
tabDefsXml +
|
|
182
|
+
numberingsXml +
|
|
183
|
+
bulletsXml +
|
|
184
|
+
paraPropsXml +
|
|
185
|
+
stylesXml +
|
|
186
|
+
`</hh:refList>` +
|
|
187
|
+
`</hh:head>`);
|
|
188
|
+
}
|
|
189
|
+
function buildBorderFillsXml(borderFills) {
|
|
190
|
+
const cnt = Math.max(1, borderFills.length);
|
|
191
|
+
const items = [];
|
|
192
|
+
for (let i = 0; i < cnt; i++) {
|
|
193
|
+
items.push(buildSingleBorderFillXml(i, borderFills[i]));
|
|
194
|
+
}
|
|
195
|
+
return `<hh:borderFills itemCnt="${cnt}">${items.join("")}</hh:borderFills>`;
|
|
196
|
+
}
|
|
197
|
+
/** HWP 너비 인덱스 → mm 매핑 (HWP 5.0 스펙) */
|
|
198
|
+
const BORDER_WIDTH_MM = [
|
|
199
|
+
"0.1", "0.12", "0.15", "0.2", "0.25", "0.3", "0.4", "0.5",
|
|
200
|
+
"0.6", "0.7", "1.0", "1.5", "2.0", "3.0", "4.0", "5.0",
|
|
201
|
+
];
|
|
202
|
+
const BORDER_LINE_TYPE_NAMES = [
|
|
203
|
+
"NONE", "SOLID", "DASH", "DOT", "DASH_DOT", "DASH_DOT_DOT", "LONG_DASH", "CIRCLE",
|
|
204
|
+
"DOUBLE", "THIN_THICK_DOUBLE", "THICK_THIN_DOUBLE", "THIN_THICK_THIN_TRIPLE",
|
|
205
|
+
"WAVE", "DOUBLE_WAVE", "THICK_3D", "THICK_3D_REVERSE", "THIN_3D", "THIN_3D_REVERSE",
|
|
206
|
+
];
|
|
207
|
+
function lineTypeName(idx) {
|
|
208
|
+
return BORDER_LINE_TYPE_NAMES[idx] ?? "SOLID";
|
|
209
|
+
}
|
|
210
|
+
function widthMm(idx) {
|
|
211
|
+
return (BORDER_WIDTH_MM[idx] ?? "0.1") + " mm";
|
|
212
|
+
}
|
|
213
|
+
function buildBorderXml(tagName, line) {
|
|
214
|
+
if (!line) {
|
|
215
|
+
return `<hh:${tagName} type="SOLID" width="0.1 mm" color="#000000"/>`;
|
|
216
|
+
}
|
|
217
|
+
return `<hh:${tagName} type="${lineTypeName(line.lineType)}" width="${widthMm(line.widthIndex)}" color="${colorBgrToHex(line.color)}"/>`;
|
|
218
|
+
}
|
|
219
|
+
function buildSingleBorderFillXml(id, bf) {
|
|
220
|
+
const left = buildBorderXml("leftBorder", bf?.borders?.[0]);
|
|
221
|
+
const right = buildBorderXml("rightBorder", bf?.borders?.[1]);
|
|
222
|
+
const top = buildBorderXml("topBorder", bf?.borders?.[2]);
|
|
223
|
+
const bottom = buildBorderXml("bottomBorder", bf?.borders?.[3]);
|
|
224
|
+
// BorderFill attr u16 비트필드:
|
|
225
|
+
// bit 0: 3D, bit 1: 그림자
|
|
226
|
+
// bit 2..4 (0x1C): slash 대각선 모양 — 0=NONE, 그 외=present
|
|
227
|
+
// bit 5..7 (0xE0): backSlash 대각선 모양
|
|
228
|
+
const attr = bf?.attr ?? 0;
|
|
229
|
+
const slashKind = (attr >>> 2) & 0x07;
|
|
230
|
+
const backSlashKind = (attr >>> 5) & 0x07;
|
|
231
|
+
const diagWidth = widthMm(bf?.diagonal?.widthIndex ?? 0);
|
|
232
|
+
const diagColor = colorBgrToHex(bf?.diagonal?.color ?? 0);
|
|
233
|
+
const slashType = slashKind !== 0 ? "SOLID" : "NONE";
|
|
234
|
+
const backSlashType = backSlashKind !== 0 ? "SOLID" : "NONE";
|
|
235
|
+
// <hh:diagonal> 의 type 은 둘 중 하나라도 있으면 SOLID
|
|
236
|
+
const hasDiag = slashKind !== 0 || backSlashKind !== 0;
|
|
237
|
+
const diagonalEl = `<hh:diagonal type="${hasDiag ? "SOLID" : "NONE"}" width="${diagWidth}" color="${diagColor}"/>`;
|
|
238
|
+
const fillEl = bf?.fill
|
|
239
|
+
? `<hh:fillBrush>` +
|
|
240
|
+
`<hh:winBrush faceColor="${colorBgrToHex(bf.fill.backgroundColor)}" hatchColor="${colorBgrToHex(bf.fill.patternColor)}" hatchStyle="${bf.fill.patternType < 0 ? "NONE" : "HORIZONTAL"}" alpha="0"/>` +
|
|
241
|
+
`</hh:fillBrush>`
|
|
242
|
+
: "";
|
|
243
|
+
return (`<hh:borderFill id="${id}" threeD="${(attr & 0x01) !== 0 ? 1 : 0}" shadow="${(attr & 0x02) !== 0 ? 1 : 0}" centerLine="NONE" breakCellSeparateLine="0">` +
|
|
244
|
+
`<hh:slash type="${slashType}" Crooked="0" isCounter="0"/>` +
|
|
245
|
+
`<hh:backSlash type="${backSlashType}" Crooked="0" isCounter="0"/>` +
|
|
246
|
+
left +
|
|
247
|
+
right +
|
|
248
|
+
top +
|
|
249
|
+
bottom +
|
|
250
|
+
diagonalEl +
|
|
251
|
+
fillEl +
|
|
252
|
+
`</hh:borderFill>`);
|
|
253
|
+
}
|
|
254
|
+
function buildTabDefsXml(tabDefs) {
|
|
255
|
+
const cnt = Math.max(1, tabDefs.length);
|
|
256
|
+
const items = [];
|
|
257
|
+
for (let i = 0; i < cnt; i++) {
|
|
258
|
+
const td = tabDefs[i];
|
|
259
|
+
const al = td?.autoTabLeft ?? true ? 1 : 0;
|
|
260
|
+
const ar = td?.autoTabRight ?? true ? 1 : 0;
|
|
261
|
+
items.push(`<hh:tabPr id="${i}" autoTabLeft="${al}" autoTabRight="${ar}">` +
|
|
262
|
+
`<hh:items itemCnt="0"/>` +
|
|
263
|
+
`</hh:tabPr>`);
|
|
264
|
+
}
|
|
265
|
+
return `<hh:tabProperties itemCnt="${cnt}">${items.join("")}</hh:tabProperties>`;
|
|
266
|
+
}
|
|
267
|
+
function buildNumberingsXml(numberings) {
|
|
268
|
+
if (numberings.length === 0) {
|
|
269
|
+
return (`<hh:numberings itemCnt="1">` +
|
|
270
|
+
`<hh:numbering id="0" start="1">` +
|
|
271
|
+
Array.from({ length: 7 })
|
|
272
|
+
.map((_, level) => `<hh:paraHead level="${level + 1}" start="1" numFormat="^${level + 1}." textOffsetType="PERCENT" textOffset="50" numberingChar="false" charPrIDRef="0">` +
|
|
273
|
+
`<hh:autoNumberFormat type="DIGIT" userChar="" prefixChar="" suffixChar="."/>` +
|
|
274
|
+
`</hh:paraHead>`)
|
|
275
|
+
.join("") +
|
|
276
|
+
`</hh:numbering>` +
|
|
277
|
+
`</hh:numberings>`);
|
|
278
|
+
}
|
|
279
|
+
const items = numberings
|
|
280
|
+
.map((n, idx) => `<hh:numbering id="${idx}" start="${n.startNumber}">` +
|
|
281
|
+
n.levelFormats
|
|
282
|
+
.map((fmt, level) => `<hh:paraHead level="${level + 1}" start="1" numFormat="${escapeXml(fmt || "^" + (level + 1) + ".")}" textOffsetType="PERCENT" textOffset="50" numberingChar="false" charPrIDRef="0">` +
|
|
283
|
+
`<hh:autoNumberFormat type="DIGIT" userChar="" prefixChar="" suffixChar="."/>` +
|
|
284
|
+
`</hh:paraHead>`)
|
|
285
|
+
.join("") +
|
|
286
|
+
`</hh:numbering>`)
|
|
287
|
+
.join("");
|
|
288
|
+
return `<hh:numberings itemCnt="${numberings.length}">${items}</hh:numberings>`;
|
|
289
|
+
}
|
|
290
|
+
function buildBulletsXml(bullets) {
|
|
291
|
+
if (bullets.length === 0) {
|
|
292
|
+
return (`<hh:bullets itemCnt="1">` +
|
|
293
|
+
`<hh:bullet id="0" char="●" imageBullet="0" checkedChar="0">` +
|
|
294
|
+
`<hh:img bright="0" contrast="0" effect="REAL_PIC" binaryItemIDRef="0"/>` +
|
|
295
|
+
`</hh:bullet>` +
|
|
296
|
+
`</hh:bullets>`);
|
|
297
|
+
}
|
|
298
|
+
const items = bullets
|
|
299
|
+
.map((b, idx) => `<hh:bullet id="${idx}" char="${escapeXml(b.bulletChar)}" imageBullet="0" checkedChar="0">` +
|
|
300
|
+
`<hh:img bright="0" contrast="0" effect="REAL_PIC" binaryItemIDRef="0"/>` +
|
|
301
|
+
`</hh:bullet>`)
|
|
302
|
+
.join("");
|
|
303
|
+
return `<hh:bullets itemCnt="${bullets.length}">${items}</hh:bullets>`;
|
|
304
|
+
}
|
|
305
|
+
function buildFontfacesXml(fontFaces) {
|
|
306
|
+
// 7개 언어 그룹 — 비어있어도 lang 속성은 넣어둔다.
|
|
307
|
+
// 그룹 안 폰트가 0개면 단일 fallback "바탕"
|
|
308
|
+
const groups = [];
|
|
309
|
+
for (let li = 0; li < 7; li++) {
|
|
310
|
+
const fonts = fontFaces[li] ?? [];
|
|
311
|
+
const lang = LANG_NAMES[li];
|
|
312
|
+
const list = fonts.length > 0
|
|
313
|
+
? fonts.map((f, idx) => buildFontXml(idx, f)).join("")
|
|
314
|
+
: `<hh:font id="0" face="바탕" type="TTF" isEmbedded="0"/>`;
|
|
315
|
+
const cnt = fonts.length > 0 ? fonts.length : 1;
|
|
316
|
+
groups.push(`<hh:fontface lang="${lang}" fontCnt="${cnt}">${list}</hh:fontface>`);
|
|
317
|
+
}
|
|
318
|
+
return `<hh:fontfaces itemCnt="${groups.length}">${groups.join("")}</hh:fontfaces>`;
|
|
319
|
+
}
|
|
320
|
+
function buildFontXml(id, f) {
|
|
321
|
+
const subAttrs = f.substituteName ? ` type="UNKNOWN" face="${escapeXml(f.substituteName)}"` : "";
|
|
322
|
+
const sub = f.substituteName ? `<hh:substFont${subAttrs}/>` : "";
|
|
323
|
+
return `<hh:font id="${id}" face="${escapeXml(f.name)}" type="TTF" isEmbedded="0">${sub}</hh:font>`;
|
|
324
|
+
}
|
|
325
|
+
function buildCharPropertiesXml(charShapes) {
|
|
326
|
+
if (charShapes.length === 0) {
|
|
327
|
+
// 최소 1개 fallback
|
|
328
|
+
return (`<hh:charProperties itemCnt="1">` +
|
|
329
|
+
`<hh:charPr id="0" height="1000" textColor="#000000" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="0">` +
|
|
330
|
+
defaultFontGroupXml() +
|
|
331
|
+
`</hh:charPr>` +
|
|
332
|
+
`</hh:charProperties>`);
|
|
333
|
+
}
|
|
334
|
+
const items = charShapes.map((cs, idx) => buildCharPrXml(idx, cs)).join("");
|
|
335
|
+
return `<hh:charProperties itemCnt="${charShapes.length}">${items}</hh:charProperties>`;
|
|
336
|
+
}
|
|
337
|
+
function buildCharPrXml(id, cs) {
|
|
338
|
+
const ids = cs.faceNameIds;
|
|
339
|
+
const fontRef = `<hh:fontRef hangul="${ids.hangul}" latin="${ids.latin}" hanja="${ids.hanja}" japanese="${ids.japanese}" other="${ids.other}" symbol="${ids.symbol}" user="${ids.user}"/>`;
|
|
340
|
+
const ratio = `<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>`;
|
|
341
|
+
const spacing = `<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>`;
|
|
342
|
+
const relSz = `<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>`;
|
|
343
|
+
const offset = `<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>`;
|
|
344
|
+
const italic = cs.italic ? `<hh:italic/>` : "";
|
|
345
|
+
const bold = cs.bold ? `<hh:bold/>` : "";
|
|
346
|
+
const underline = cs.underline
|
|
347
|
+
? `<hh:underline type="BOTTOM" shape="SOLID" color="${colorBgrToHex(cs.underlineColor)}"/>`
|
|
348
|
+
: "";
|
|
349
|
+
const strikeout = cs.strikeout
|
|
350
|
+
? `<hh:strikeout shape="SOLID" color="${colorBgrToHex(cs.textColor)}"/>`
|
|
351
|
+
: "";
|
|
352
|
+
const textColor = colorBgrToHex(cs.textColor);
|
|
353
|
+
const shadeColor = cs.shadeColor === 0xffffff || cs.shadeColor === 0 ? "none" : colorBgrToHex(cs.shadeColor);
|
|
354
|
+
return (`<hh:charPr id="${id}" height="${cs.baseSize}" textColor="${textColor}" shadeColor="${shadeColor}" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="0">` +
|
|
355
|
+
fontRef +
|
|
356
|
+
ratio +
|
|
357
|
+
spacing +
|
|
358
|
+
relSz +
|
|
359
|
+
offset +
|
|
360
|
+
italic +
|
|
361
|
+
bold +
|
|
362
|
+
underline +
|
|
363
|
+
strikeout +
|
|
364
|
+
`</hh:charPr>`);
|
|
365
|
+
}
|
|
366
|
+
function defaultFontGroupXml() {
|
|
367
|
+
return (`<hh:fontRef hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>` +
|
|
368
|
+
`<hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>` +
|
|
369
|
+
`<hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>` +
|
|
370
|
+
`<hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>` +
|
|
371
|
+
`<hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>`);
|
|
372
|
+
}
|
|
373
|
+
function buildParaPropertiesXml(paraShapes) {
|
|
374
|
+
if (paraShapes.length === 0) {
|
|
375
|
+
return (`<hh:paraProperties itemCnt="1">` +
|
|
376
|
+
`<hh:paraPr id="0" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="0" suppressLineNumbers="0" checked="0">` +
|
|
377
|
+
`<hh:align horizontal="JUSTIFY" vertical="BASELINE"/>` +
|
|
378
|
+
`<hh:heading type="NONE" idRef="0" level="0"/>` +
|
|
379
|
+
`<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="KEEP_WORD" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>` +
|
|
380
|
+
`<hh:margin><hh:intent value="0"/><hh:left value="0"/><hh:right value="0"/><hh:prev value="0"/><hh:next value="0"/></hh:margin>` +
|
|
381
|
+
`<hh:lineSpacing type="PERCENT" value="160"/>` +
|
|
382
|
+
`</hh:paraPr>` +
|
|
383
|
+
`</hh:paraProperties>`);
|
|
384
|
+
}
|
|
385
|
+
const items = paraShapes.map((ps, idx) => buildParaPrXml(idx, ps)).join("");
|
|
386
|
+
return `<hh:paraProperties itemCnt="${paraShapes.length}">${items}</hh:paraProperties>`;
|
|
387
|
+
}
|
|
388
|
+
function buildParaPrXml(id, ps) {
|
|
389
|
+
const align = alignToOwpml(ps.alignment);
|
|
390
|
+
return (`<hh:paraPr id="${id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="0" suppressLineNumbers="0" checked="0">` +
|
|
391
|
+
`<hh:align horizontal="${align}" vertical="BASELINE"/>` +
|
|
392
|
+
`<hh:heading type="NONE" idRef="0" level="0"/>` +
|
|
393
|
+
`<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="KEEP_WORD" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>` +
|
|
394
|
+
`<hh:margin>` +
|
|
395
|
+
`<hh:intent value="${ps.indent}"/>` +
|
|
396
|
+
`<hh:left value="${ps.leftMargin}"/>` +
|
|
397
|
+
`<hh:right value="${ps.rightMargin}"/>` +
|
|
398
|
+
`<hh:prev value="${ps.prevSpacing}"/>` +
|
|
399
|
+
`<hh:next value="${ps.nextSpacing}"/>` +
|
|
400
|
+
`</hh:margin>` +
|
|
401
|
+
`<hh:lineSpacing type="PERCENT" value="${Math.max(0, ps.lineSpacing)}"/>` +
|
|
402
|
+
`</hh:paraPr>`);
|
|
403
|
+
}
|
|
404
|
+
function buildStylesXml(styles) {
|
|
405
|
+
if (styles.length === 0) {
|
|
406
|
+
return (`<hh:styles itemCnt="1">` +
|
|
407
|
+
`<hh:style id="0" type="PARA" name="바탕글" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langID="1042" lockForm="0"/>` +
|
|
408
|
+
`</hh:styles>`);
|
|
409
|
+
}
|
|
410
|
+
const items = styles
|
|
411
|
+
.map((s, idx) => `<hh:style id="${idx}" type="PARA" name="${escapeXml(s.name || "Style" + idx)}" engName="${escapeXml(s.engName ?? "")}" paraPrIDRef="${s.paraShapeId}" charPrIDRef="${s.charShapeId}" nextStyleIDRef="${idx}" langID="1042" lockForm="0"/>`)
|
|
412
|
+
.join("");
|
|
413
|
+
return `<hh:styles itemCnt="${styles.length}">${items}</hh:styles>`;
|
|
414
|
+
}
|
|
415
|
+
// ============================================================
|
|
416
|
+
// 색상 / 정렬 변환
|
|
417
|
+
// ============================================================
|
|
418
|
+
/** HWP ColorRef (u32 LE 의 0xAABBGGRR 형식) → "#RRGGBB" */
|
|
419
|
+
export function colorBgrToHex(color) {
|
|
420
|
+
const r = color & 0xff;
|
|
421
|
+
const g = (color >>> 8) & 0xff;
|
|
422
|
+
const b = (color >>> 16) & 0xff;
|
|
423
|
+
return "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0").toUpperCase()).join("");
|
|
424
|
+
}
|
|
425
|
+
function alignToOwpml(a) {
|
|
426
|
+
switch (a) {
|
|
427
|
+
case "left":
|
|
428
|
+
return "LEFT";
|
|
429
|
+
case "right":
|
|
430
|
+
return "RIGHT";
|
|
431
|
+
case "center":
|
|
432
|
+
return "CENTER";
|
|
433
|
+
case "justify":
|
|
434
|
+
return "JUSTIFY";
|
|
435
|
+
case "distribute":
|
|
436
|
+
return "DISTRIBUTE";
|
|
437
|
+
case "distributeSpace":
|
|
438
|
+
return "DISTRIBUTE_SPACE";
|
|
439
|
+
default:
|
|
440
|
+
return "JUSTIFY";
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ============================================================
|
|
444
|
+
// section.xml 빌드
|
|
445
|
+
// ============================================================
|
|
446
|
+
function buildSectionXml(paragraphs, binEntries) {
|
|
447
|
+
// 본 문단 + 머리말/꼬리말/각주 인라인 보강
|
|
448
|
+
const parts = [];
|
|
449
|
+
// 섹션 첫 문단에 secPr(페이지 설정) — 한컴이 섹션을 구성하는 데 필수
|
|
450
|
+
parts.push(`<hp:p id="${makeParaId()}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
|
|
451
|
+
`<hp:run charPrIDRef="0">${SEC_PR_XML}</hp:run>` +
|
|
452
|
+
`<hp:run charPrIDRef="0"><hp:t/></hp:run>` +
|
|
453
|
+
DEFAULT_LINESEG +
|
|
454
|
+
`</hp:p>`);
|
|
455
|
+
for (const p of paragraphs) {
|
|
456
|
+
parts.push(buildParagraphXml(p, binEntries));
|
|
457
|
+
// 같은 paragraph 안의 header/footer/footnote 컨트롤이 가진 paragraphs 도 본문 흐름에 평탄 출력
|
|
458
|
+
for (const ctrl of p.controls) {
|
|
459
|
+
if (ctrl.kind === "header" ||
|
|
460
|
+
ctrl.kind === "footer" ||
|
|
461
|
+
ctrl.kind === "footnote") {
|
|
462
|
+
for (const subPara of ctrl.paragraphs) {
|
|
463
|
+
parts.push(buildParagraphXml(subPara, binEntries));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return (`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n` +
|
|
469
|
+
`<hs:sec ${OWPML_NS}>` +
|
|
470
|
+
parts.join("") +
|
|
471
|
+
`</hs:sec>`);
|
|
472
|
+
}
|
|
473
|
+
function buildParagraphXml(p, binEntries) {
|
|
474
|
+
const parts = [];
|
|
475
|
+
// 텍스트 run 들 (charShape 별로 분리됨)
|
|
476
|
+
if (p.runs.length > 0) {
|
|
477
|
+
for (const run of p.runs) {
|
|
478
|
+
parts.push(buildRunXml(run));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else if (p.text.length > 0) {
|
|
482
|
+
parts.push(buildRunXml({ charShapeId: 0, text: p.text }));
|
|
483
|
+
}
|
|
484
|
+
// 컨트롤 (표/그림/...)
|
|
485
|
+
for (const ctrl of p.controls) {
|
|
486
|
+
const xml = buildControlXml(ctrl, binEntries);
|
|
487
|
+
if (xml)
|
|
488
|
+
parts.push(xml);
|
|
489
|
+
}
|
|
490
|
+
if (parts.length === 0) {
|
|
491
|
+
parts.push(`<hp:run charPrIDRef="0"/>`);
|
|
492
|
+
}
|
|
493
|
+
return (`<hp:p id="${makeParaId()}" paraPrIDRef="${p.paraShapeId}" styleIDRef="${p.styleId}" pageBreak="0" columnBreak="0" merged="0">` +
|
|
494
|
+
parts.join("") +
|
|
495
|
+
DEFAULT_LINESEG +
|
|
496
|
+
`</hp:p>`);
|
|
497
|
+
}
|
|
498
|
+
function buildRunXml(run) {
|
|
499
|
+
return `<hp:run charPrIDRef="${run.charShapeId}"><hp:t>${escapeXml(run.text)}</hp:t></hp:run>`;
|
|
500
|
+
}
|
|
501
|
+
// 이미지 표시 기본 크기(HWPUNIT). 원본 픽셀을 모르므로 고정값 — 한컴이 비율 보정.
|
|
502
|
+
const PIC_WIDTH = 40000;
|
|
503
|
+
const PIC_HEIGHT = 30000;
|
|
504
|
+
/**
|
|
505
|
+
* 한컴 정상 hp:pic 구조(etc/hwpjs_image_test 기준).
|
|
506
|
+
* orgSz=curSz 1:1, 단위행렬 — 한글이 실제 크기를 재계산한다.
|
|
507
|
+
*/
|
|
508
|
+
function buildPicXml(entry) {
|
|
509
|
+
const w = PIC_WIDTH;
|
|
510
|
+
const h = PIC_HEIGHT;
|
|
511
|
+
return (`<hp:pic id="${makeParaId()}" zOrder="0" numberingType="PICTURE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" ` +
|
|
512
|
+
`lock="0" dropcapstyle="None" href="" groupLevel="0" instid="${makeParaId()}" reverse="0">` +
|
|
513
|
+
`<hp:offset x="0" y="0"/>` +
|
|
514
|
+
`<hp:orgSz width="${w}" height="${h}"/>` +
|
|
515
|
+
`<hp:curSz width="${w}" height="${h}"/>` +
|
|
516
|
+
`<hp:flip horizontal="0" vertical="0"/>` +
|
|
517
|
+
`<hp:rotationInfo angle="0" centerX="${(w / 2) | 0}" centerY="${(h / 2) | 0}" rotateimage="1"/>` +
|
|
518
|
+
`<hp:renderingInfo>` +
|
|
519
|
+
`<hc:transMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>` +
|
|
520
|
+
`<hc:scaMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>` +
|
|
521
|
+
`<hc:rotMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>` +
|
|
522
|
+
`</hp:renderingInfo>` +
|
|
523
|
+
`<hc:img binaryItemIDRef="${entry.id}" bright="0" contrast="0" effect="REAL_PIC" alpha="0"/>` +
|
|
524
|
+
`<hp:imgRect><hc:pt0 x="0" y="0"/><hc:pt1 x="${w}" y="0"/><hc:pt2 x="${w}" y="${h}"/><hc:pt3 x="0" y="${h}"/></hp:imgRect>` +
|
|
525
|
+
`<hp:imgClip left="0" right="${w}" top="0" bottom="${h}"/>` +
|
|
526
|
+
`<hp:inMargin left="0" right="0" top="0" bottom="0"/>` +
|
|
527
|
+
`<hp:imgDim dimwidth="${w}" dimheight="${h}"/>` +
|
|
528
|
+
`<hp:effects/>` +
|
|
529
|
+
`<hp:sz width="${w}" widthRelTo="ABSOLUTE" height="${h}" heightRelTo="ABSOLUTE" protect="0"/>` +
|
|
530
|
+
`<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" ` +
|
|
531
|
+
`vertRelTo="PARA" horzRelTo="COLUMN" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/>` +
|
|
532
|
+
`<hp:outMargin left="0" right="0" top="0" bottom="0"/>` +
|
|
533
|
+
`</hp:pic>`);
|
|
534
|
+
}
|
|
535
|
+
function buildControlXml(ctrl, binEntries) {
|
|
536
|
+
switch (ctrl.kind) {
|
|
537
|
+
case "table":
|
|
538
|
+
return `<hp:run charPrIDRef="0">${buildTableXml(ctrl, binEntries)}</hp:run>`;
|
|
539
|
+
case "picture": {
|
|
540
|
+
const entry = binEntries.find((b) => b.id === `image${ctrl.binDataId}`);
|
|
541
|
+
if (!entry)
|
|
542
|
+
return "";
|
|
543
|
+
return `<hp:run charPrIDRef="0">${buildPicXml(entry)}</hp:run>`;
|
|
544
|
+
}
|
|
545
|
+
case "shape": {
|
|
546
|
+
// 도형: 1차 포팅에서는 placeholder. line 은 좌표만 보존.
|
|
547
|
+
const tag = ctrl.shapeType === "line"
|
|
548
|
+
? "line"
|
|
549
|
+
: ctrl.shapeType === "rectangle"
|
|
550
|
+
? "rect"
|
|
551
|
+
: ctrl.shapeType === "ellipse"
|
|
552
|
+
? "ellipse"
|
|
553
|
+
: ctrl.shapeType === "arc"
|
|
554
|
+
? "arc"
|
|
555
|
+
: ctrl.shapeType === "polygon"
|
|
556
|
+
? "polygon"
|
|
557
|
+
: "curve";
|
|
558
|
+
const coords = ctrl.shapeType === "line" && ctrl.x1 !== undefined
|
|
559
|
+
? `<hc:startPt x="${ctrl.x1}" y="${ctrl.y1 ?? 0}"/><hc:endPt x="${ctrl.x2 ?? 0}" y="${ctrl.y2 ?? 0}"/>`
|
|
560
|
+
: "";
|
|
561
|
+
return `<hp:run charPrIDRef="0"><hp:${tag}>${coords}</hp:${tag}></hp:run>`;
|
|
562
|
+
}
|
|
563
|
+
case "equation": {
|
|
564
|
+
if (ctrl.script.length === 0)
|
|
565
|
+
return "";
|
|
566
|
+
return (`<hp:run charPrIDRef="0">` +
|
|
567
|
+
`<hp:equation>` +
|
|
568
|
+
`<hp:script>${escapeXml(ctrl.script)}</hp:script>` +
|
|
569
|
+
`</hp:equation>` +
|
|
570
|
+
`</hp:run>`);
|
|
571
|
+
}
|
|
572
|
+
case "header":
|
|
573
|
+
case "footer":
|
|
574
|
+
case "footnote":
|
|
575
|
+
case "field":
|
|
576
|
+
case "unknown":
|
|
577
|
+
return "";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// 본문 가용 폭(HWPUNIT) — SEC_PR_XML 의 pagePr(width 59528, 좌우 margin 8504) 기준.
|
|
581
|
+
const TABLE_BODY_WIDTH = 42520;
|
|
582
|
+
const DEFAULT_ROW_HEIGHT = 2000; // 한글이 실제 높이를 재계산하므로 추정값으로 충분
|
|
583
|
+
function buildTableXml(t, binEntries) {
|
|
584
|
+
const colCount = Math.max(1, t.colCount);
|
|
585
|
+
const rowCount = Math.max(1, t.rowCount);
|
|
586
|
+
const cellW = Math.max(1, Math.floor(TABLE_BODY_WIDTH / colCount));
|
|
587
|
+
const tableW = cellW * colCount;
|
|
588
|
+
const tableH = DEFAULT_ROW_HEIGHT * rowCount;
|
|
589
|
+
const rows = Array.from({ length: t.rowCount }, () => []);
|
|
590
|
+
for (const cell of t.cells) {
|
|
591
|
+
if (cell.row >= 0 && cell.row < t.rowCount)
|
|
592
|
+
rows[cell.row].push(cell);
|
|
593
|
+
}
|
|
594
|
+
for (const row of rows)
|
|
595
|
+
row.sort((a, b) => a.col - b.col);
|
|
596
|
+
const subListAttrs = `id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" ` +
|
|
597
|
+
`linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0"`;
|
|
598
|
+
const trXml = rows
|
|
599
|
+
.map((row) => {
|
|
600
|
+
const tcXml = row
|
|
601
|
+
.map((cell) => {
|
|
602
|
+
const cellInner = cell.paragraphs
|
|
603
|
+
.map((q) => buildParagraphXml(q, binEntries))
|
|
604
|
+
.join("");
|
|
605
|
+
const colSpan = Math.max(1, cell.colSpan);
|
|
606
|
+
const rowSpan = Math.max(1, cell.rowSpan);
|
|
607
|
+
const cw = cellW * colSpan;
|
|
608
|
+
const ch = DEFAULT_ROW_HEIGHT * rowSpan;
|
|
609
|
+
const inner = cellInner ||
|
|
610
|
+
`<hp:p id="${makeParaId()}" paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0"/>${DEFAULT_LINESEG}</hp:p>`;
|
|
611
|
+
return (`<hp:tc name="" header="0" hasMargin="0" protect="0" editable="0" dirty="0" borderFillIDRef="0">` +
|
|
612
|
+
`<hp:subList ${subListAttrs}>${inner}</hp:subList>` +
|
|
613
|
+
`<hp:cellAddr colAddr="${cell.col}" rowAddr="${cell.row}"/>` +
|
|
614
|
+
`<hp:cellSpan colSpan="${colSpan}" rowSpan="${rowSpan}"/>` +
|
|
615
|
+
`<hp:cellSz width="${cw}" height="${ch}"/>` +
|
|
616
|
+
`<hp:cellMargin left="510" right="510" top="141" bottom="141"/>` +
|
|
617
|
+
`</hp:tc>`);
|
|
618
|
+
})
|
|
619
|
+
.join("");
|
|
620
|
+
return `<hp:tr>${tcXml}</hp:tr>`;
|
|
621
|
+
})
|
|
622
|
+
.join("");
|
|
623
|
+
return (`<hp:tbl id="${makeParaId()}" zOrder="0" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" ` +
|
|
624
|
+
`lock="0" dropcapstyle="None" pageBreak="CELL" repeatHeader="1" rowCnt="${rowCount}" colCnt="${colCount}" ` +
|
|
625
|
+
`cellSpacing="0" borderFillIDRef="0" noAdjust="0">` +
|
|
626
|
+
`<hp:sz width="${tableW}" widthRelTo="ABSOLUTE" height="${tableH}" heightRelTo="ABSOLUTE" protect="0"/>` +
|
|
627
|
+
`<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" ` +
|
|
628
|
+
`vertRelTo="PARA" horzRelTo="COLUMN" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/>` +
|
|
629
|
+
`<hp:outMargin left="283" right="283" top="283" bottom="283"/>` +
|
|
630
|
+
`<hp:inMargin left="510" right="510" top="141" bottom="141"/>` +
|
|
631
|
+
trXml +
|
|
632
|
+
`</hp:tbl>`);
|
|
633
|
+
}
|