hwpkit-dev 0.0.3 → 0.0.5
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/ .npmignore +1 -0
- package/README.md +8 -8
- package/dist/index.d.mts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +573 -230
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +573 -230
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/decoders/hwp/HwpScanner.ts +174 -57
- package/src/decoders/hwpx/HwpxDecoder.ts +23 -12
- package/src/encoders/docx/DocxEncoder.ts +49 -4
- package/src/encoders/hwp/HwpEncoder.ts +309 -163
- package/src/encoders/hwpx/HwpxEncoder.ts +249 -103
- package/src/model/doc-props.ts +5 -5
- package/src/model/doc-tree.ts +2 -2
- package/test-styling.ts +0 -210
|
@@ -172,6 +172,31 @@ const KIND_MAP: Record<string, string> = {
|
|
|
172
172
|
dash_dot_dot: "DASH_DOT_DOT",
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* 테두리 선 굵기 mm 값을 한글 표준 규격 리스트 중 가장 가까운 값으로 매핑(양자화)합니다.
|
|
177
|
+
*/
|
|
178
|
+
function quantizeBorderWidth(pt: number): string {
|
|
179
|
+
const mm = pt * 0.3528;
|
|
180
|
+
const standardWidths = [0.1, 0.12, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0];
|
|
181
|
+
let closest = standardWidths[0];
|
|
182
|
+
let minDiff = Math.abs(mm - closest);
|
|
183
|
+
for (let i = 1; i < standardWidths.length; i++) {
|
|
184
|
+
const diff = Math.abs(mm - standardWidths[i]);
|
|
185
|
+
if (diff < minDiff) {
|
|
186
|
+
minDiff = diff;
|
|
187
|
+
closest = standardWidths[i];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
let str = closest.toFixed(2);
|
|
191
|
+
if (str.endsWith("0")) {
|
|
192
|
+
str = str.slice(0, -1);
|
|
193
|
+
}
|
|
194
|
+
if (str.endsWith(".0")) {
|
|
195
|
+
str = str.slice(0, -2);
|
|
196
|
+
}
|
|
197
|
+
return `${str} mm`;
|
|
198
|
+
}
|
|
199
|
+
|
|
175
200
|
class BorderFillBank {
|
|
176
201
|
private fills: { id: number; xml: string }[] = [];
|
|
177
202
|
private keyMap = new Map<string, number>();
|
|
@@ -190,7 +215,7 @@ class BorderFillBank {
|
|
|
190
215
|
const type =
|
|
191
216
|
s && s.kind !== "none" ? (KIND_MAP[s.kind] ?? "SOLID") : "NONE";
|
|
192
217
|
const w =
|
|
193
|
-
s && s.kind !== "none" ?
|
|
218
|
+
s && s.kind !== "none" ? quantizeBorderWidth(s.pt) : "0.12 mm";
|
|
194
219
|
const c = s
|
|
195
220
|
? s.color.startsWith("#")
|
|
196
221
|
? s.color
|
|
@@ -346,7 +371,6 @@ interface CharPrDef {
|
|
|
346
371
|
latinId: number; // LATIN 그룹 폰트 ID
|
|
347
372
|
bg?: string;
|
|
348
373
|
}
|
|
349
|
-
|
|
350
374
|
interface ParaPrDef {
|
|
351
375
|
id: number;
|
|
352
376
|
align: string;
|
|
@@ -356,10 +380,12 @@ interface ParaPrDef {
|
|
|
356
380
|
prevHwp: number;
|
|
357
381
|
nextHwp: number;
|
|
358
382
|
lineSpacing: number;
|
|
383
|
+
lineSpacingFixed?: number; // HWPUNIT, type=FIXED
|
|
359
384
|
listType?: string;
|
|
360
385
|
listLevel?: number;
|
|
386
|
+
verAlign?: string;
|
|
387
|
+
lineWrap?: string;
|
|
361
388
|
}
|
|
362
|
-
|
|
363
389
|
interface StyleEntry {
|
|
364
390
|
id: number;
|
|
365
391
|
name: string;
|
|
@@ -383,7 +409,7 @@ function charPrKey(p: TextProps): string {
|
|
|
383
409
|
* null/undefined는 0 으로 처리하여 일관성 유지
|
|
384
410
|
*/
|
|
385
411
|
function paraPrKey(p: ParaProps): string {
|
|
386
|
-
return `${p.align ?? "left"}|${p.listOrd ?? ""}|${p.listLv ?? 0}|${p.indentPt ?? 0}|${p.firstLineIndentPt ?? 0}|${p.spaceBefore ?? 0}|${p.spaceAfter ?? 0}|${p.lineHeight ?? 0}|${p.styleId ?? ""}`;
|
|
412
|
+
return `${p.align ?? "left"}|${p.verAlign ?? "baseline"}|${p.lineWrap ?? "break"}|${p.listOrd ?? ""}|${p.listLv ?? 0}|${p.indentPt ?? 0}|${p.leftMargin ?? 0}|${p.indentRightPt ?? 0}|${p.firstLineIndentPt ?? 0}|${p.spaceBefore ?? 0}|${p.spaceAfter ?? 0}|${p.lineHeight ?? 0}|${p.lineHeightFixed ?? 0}|${p.styleId ?? ""}`;
|
|
387
413
|
}
|
|
388
414
|
|
|
389
415
|
// ─── 인코딩 컨텍스트 ─────────────────────────────────────────
|
|
@@ -430,21 +456,51 @@ function registerCharPr(props: TextProps, ctx: HwpxCtx): number {
|
|
|
430
456
|
return id;
|
|
431
457
|
}
|
|
432
458
|
|
|
459
|
+
const ALIGN_MAP: Record<string, string> = {
|
|
460
|
+
left: "LEFT",
|
|
461
|
+
center: "CENTER",
|
|
462
|
+
right: "RIGHT",
|
|
463
|
+
justify: "JUSTIFY",
|
|
464
|
+
distribute: "DISTRIBUTE",
|
|
465
|
+
distribute_space: "DISTRIBUTE_SPACE",
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const V_ALIGN_MAP: Record<string, string> = {
|
|
469
|
+
baseline: "BASELINE",
|
|
470
|
+
top: "TOP",
|
|
471
|
+
center: "CENTER",
|
|
472
|
+
bottom: "BOTTOM",
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const LINE_WRAP_MAP: Record<string, string> = {
|
|
476
|
+
break: "BREAK",
|
|
477
|
+
squeeze: "SQUEEZE",
|
|
478
|
+
keep: "KEEP",
|
|
479
|
+
};
|
|
480
|
+
|
|
433
481
|
function registerParaPr(props: ParaProps, ctx: HwpxCtx): number {
|
|
434
482
|
const key = paraPrKey(props);
|
|
435
483
|
const existing = ctx.paraPrMap.get(key);
|
|
436
484
|
if (existing !== undefined) return existing;
|
|
437
485
|
|
|
438
486
|
const id = ctx.paraPrs.length;
|
|
487
|
+
|
|
488
|
+
const alignStr = props.align ? (ALIGN_MAP[props.align] ?? "LEFT") : "LEFT";
|
|
489
|
+
const verAlignStr = props.verAlign ? (V_ALIGN_MAP[props.verAlign] ?? "BASELINE") : "BASELINE";
|
|
490
|
+
const lineWrapStr = props.lineWrap ? (LINE_WRAP_MAP[props.lineWrap] ?? "BREAK") : "BREAK";
|
|
491
|
+
|
|
439
492
|
const def: ParaPrDef = {
|
|
440
493
|
id,
|
|
441
|
-
align:
|
|
442
|
-
|
|
494
|
+
align: alignStr,
|
|
495
|
+
verAlign: verAlignStr,
|
|
496
|
+
lineWrap: lineWrapStr,
|
|
497
|
+
leftHwp: Metric.ptToHwp(props.leftMargin ?? 0),
|
|
443
498
|
rightHwp: Metric.ptToHwp(props.indentRightPt ?? 0),
|
|
444
499
|
intentHwp: Metric.ptToHwp(props.firstLineIndentPt ?? 0),
|
|
445
500
|
prevHwp: Metric.ptToHwp(props.spaceBefore ?? 0),
|
|
446
501
|
nextHwp: Metric.ptToHwp(props.spaceAfter ?? 0),
|
|
447
|
-
lineSpacing: props.lineHeight ? Math.round(props.lineHeight * 100) : 160,
|
|
502
|
+
lineSpacing: props.lineHeightFixed ? 0 : (props.lineHeight ? Math.round(props.lineHeight * 100) : 160),
|
|
503
|
+
lineSpacingFixed: props.lineHeightFixed ? Metric.ptToHwp(props.lineHeightFixed) : undefined,
|
|
448
504
|
};
|
|
449
505
|
if (props.listOrd !== undefined) {
|
|
450
506
|
def.listType = props.listOrd ? "DIGIT" : "BULLET";
|
|
@@ -570,8 +626,8 @@ export class HwpxEncoder extends BaseEncoder {
|
|
|
570
626
|
const sheet = doc.kids[0];
|
|
571
627
|
const dims = normalizeDims(sheet?.dims ?? A4);
|
|
572
628
|
|
|
573
|
-
const safeML = dims.ml
|
|
574
|
-
const safeMR = dims.mr
|
|
629
|
+
const safeML = (dims.ml !== undefined && dims.ml >= 0) ? dims.ml : 70.87;
|
|
630
|
+
const safeMR = (dims.mr !== undefined && dims.mr >= 0) ? dims.mr : 70.87;
|
|
575
631
|
const availableWidth = Math.round(
|
|
576
632
|
Metric.ptToHwp(dims.wPt) -
|
|
577
633
|
Metric.ptToHwp(safeML) -
|
|
@@ -638,9 +694,9 @@ export class HwpxEncoder extends BaseEncoder {
|
|
|
638
694
|
mime: "application/xml",
|
|
639
695
|
},
|
|
640
696
|
{
|
|
641
|
-
name: "META-INF/
|
|
642
|
-
data: this.stringToBytes(
|
|
643
|
-
mime: "application/
|
|
697
|
+
name: "META-INF/manifest.xml",
|
|
698
|
+
data: this.stringToBytes(MANIFEST_XML),
|
|
699
|
+
mime: "application/xml",
|
|
644
700
|
},
|
|
645
701
|
{
|
|
646
702
|
name: "Contents/content.hpf",
|
|
@@ -691,12 +747,14 @@ export class HwpxEncoder extends BaseEncoder {
|
|
|
691
747
|
|
|
692
748
|
// ─── 상수 XML ────────────────────────────────────────────────
|
|
693
749
|
|
|
750
|
+
// namespace: 실제 HWP가 기대하는 2011 버전 네임스페이스 사용 (owpml.org/2024는 열리지 않음)
|
|
694
751
|
const VERSION_XML =
|
|
695
752
|
`<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>` +
|
|
696
|
-
`<hv:HCFVersion xmlns:hv="http://www.
|
|
697
|
-
`
|
|
698
|
-
`os="1" xmlVersion="1.4" application="Hancom Office Hangul" appVersion="
|
|
753
|
+
`<hv:HCFVersion xmlns:hv="http://www.hancom.co.kr/hwpml/2011/version" ` +
|
|
754
|
+
`tagetApplication="WORDPROCESSOR" major="5" minor="0" micro="5" buildNumber="0" ` +
|
|
755
|
+
`os="1" xmlVersion="1.4" application="Hancom Office Hangul" appVersion="9, 6, 1, 10097"/>`;
|
|
699
756
|
|
|
757
|
+
// container.rdf rootfile 항목 제거 — 실제 HWPX 파일 구조와 일치
|
|
700
758
|
const CONTAINER_XML =
|
|
701
759
|
`<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>` +
|
|
702
760
|
`<ocf:container xmlns:ocf="urn:oasis:names:tc:opendocument:xmlns:container" ` +
|
|
@@ -704,19 +762,9 @@ const CONTAINER_XML =
|
|
|
704
762
|
`<ocf:rootfiles>` +
|
|
705
763
|
`<ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>` +
|
|
706
764
|
`<ocf:rootfile full-path="Preview/PrvText.txt" media-type="text/plain"/>` +
|
|
707
|
-
`<ocf:rootfile full-path="META-INF/container.rdf" media-type="application/rdf+xml"/>` +
|
|
708
765
|
`</ocf:rootfiles></ocf:container>`;
|
|
709
766
|
|
|
710
|
-
|
|
711
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>` +
|
|
712
|
-
`<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">` +
|
|
713
|
-
`<rdf:Description rdf:about=""><ns0:hasPart xmlns:ns0="http://www.hancom.co.kr/hwpml/2016/meta/pkg#" rdf:resource="Contents/header.xml"/></rdf:Description>` +
|
|
714
|
-
`<rdf:Description rdf:about="Contents/header.xml"><rdf:type rdf:resource="http://www.hancom.co.kr/hwpml/2016/meta/pkg#HeaderFile"/></rdf:Description>` +
|
|
715
|
-
`<rdf:Description rdf:about=""><ns0:hasPart xmlns:ns0="http://www.hancom.co.kr/hwpml/2016/meta/pkg#" rdf:resource="Contents/section0.xml"/></rdf:Description>` +
|
|
716
|
-
`<rdf:Description rdf:about="Contents/section0.xml"><rdf:type rdf:resource="http://www.hancom.co.kr/hwpml/2016/meta/pkg#SectionFile"/></rdf:Description>` +
|
|
717
|
-
`<rdf:Description rdf:about=""><rdf:type rdf:resource="http://www.hancom.co.kr/hwpml/2016/meta/pkg#Document"/></rdf:Description>` +
|
|
718
|
-
`</rdf:RDF>`;
|
|
719
|
-
|
|
767
|
+
// HWPX 파일은 container.rdf 대신 manifest.xml(빈 ODF 매니페스트) 사용
|
|
720
768
|
const MANIFEST_XML =
|
|
721
769
|
`<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>` +
|
|
722
770
|
`<odf:manifest xmlns:odf="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"/>`;
|
|
@@ -817,6 +865,73 @@ function buildBulletsXml(): string {
|
|
|
817
865
|
|
|
818
866
|
// ─── header.xml ──────────────────────────────────────────────
|
|
819
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Contents/header.xml 용 전역 구역 설정 리스트(secPrList)를 생성합니다.
|
|
870
|
+
*/
|
|
871
|
+
/**
|
|
872
|
+
* 페이지 여백 (margin) 과 헤더/푸터 영역 (zone) 을 계산합니다.
|
|
873
|
+
* HWPX spec 에서는 pagePr > margin 의 header/footer 가 헤더/푸터 영역의 높이 (zone height) 입니다.
|
|
874
|
+
* - headerZone: 용지 상단에서 헤더 영역 상단까지의 거리 (headerPt 가 없으면 0)
|
|
875
|
+
* - footerZone: 용지 하단에서 푸터 영역 하단까지의 거리 (footerPt 가 없으면 0)
|
|
876
|
+
* - mt/mb: 용지 상단/하단에서 본문 영역 상단/하단까지의 거리
|
|
877
|
+
*/
|
|
878
|
+
function buildHeaderSecPrListXml(dims: PageDims): string {
|
|
879
|
+
const wHwp = Metric.ptToHwp(dims.wPt);
|
|
880
|
+
const hHwp = Metric.ptToHwp(dims.hPt);
|
|
881
|
+
const ml = Metric.ptToHwp(dims.ml);
|
|
882
|
+
const mr = Metric.ptToHwp(dims.mr);
|
|
883
|
+
const mt = Metric.ptToHwp(dims.mt);
|
|
884
|
+
const mb = Metric.ptToHwp(dims.mb);
|
|
885
|
+
|
|
886
|
+
// 헤더/푸터 영역 높이 계산 (HWPX 는 zone height 를 직접 지정)
|
|
887
|
+
// headerPt 가 설정되어 있으면 그 값을 zone height 로 사용, 없으면 0 으로 설정
|
|
888
|
+
const headerZone = dims.headerPt !== undefined && dims.headerPt > 0
|
|
889
|
+
? Metric.ptToHwp(dims.headerPt)
|
|
890
|
+
: 0;
|
|
891
|
+
const footerZone = dims.footerPt !== undefined && dims.footerPt > 0
|
|
892
|
+
? Metric.ptToHwp(dims.footerPt)
|
|
893
|
+
: 0;
|
|
894
|
+
|
|
895
|
+
const pageBorderFill =
|
|
896
|
+
`<hh:pageBorderFill type="BOTH" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER">` +
|
|
897
|
+
`<hh:offset left="1417" right="1417" top="1417" bottom="1417"/>` +
|
|
898
|
+
`</hh:pageBorderFill>` +
|
|
899
|
+
`<hh:pageBorderFill type="EVEN" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER">` +
|
|
900
|
+
`<hh:offset left="1417" right="1417" top="1417" bottom="1417"/>` +
|
|
901
|
+
`</hh:pageBorderFill>` +
|
|
902
|
+
`<hh:pageBorderFill type="ODD" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER">` +
|
|
903
|
+
`<hh:offset left="1417" right="1417" top="1417" bottom="1417"/>` +
|
|
904
|
+
`</hh:pageBorderFill>`;
|
|
905
|
+
|
|
906
|
+
return (
|
|
907
|
+
`<hh:secPrList itemCnt="1">` +
|
|
908
|
+
`<hh:secPr id="0" textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" outlineShapeIDRef="0" memoShapeIDRef="0" textVerticalWidthHead="0" masterPageCnt="0">` +
|
|
909
|
+
`<hh:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/>` +
|
|
910
|
+
`<hh:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/>` +
|
|
911
|
+
`<hh:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/>` +
|
|
912
|
+
`<hh:lineNumberShape restartType="0" countBy="0" distance="0" startNumber="0"/>` +
|
|
913
|
+
`<hh:pagePr landscape="WIDELY" width="${wHwp}" height="${hHwp}" gutterType="LEFT_ONLY">` +
|
|
914
|
+
`<hh:margin header="${headerZone}" footer="${footerZone}" gutter="0" left="${ml}" right="${mr}" top="${mt}" bottom="${mb}"/>` +
|
|
915
|
+
`</hh:pagePr>` +
|
|
916
|
+
`<hh:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/>` +
|
|
917
|
+
`<hh:footNotePr><hh:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar="" supscript="1"/>` +
|
|
918
|
+
`<hh:noteLine length="-1" type="SOLID" width="0.25 mm" color="#000000"/>` +
|
|
919
|
+
`<hh:noteSpacing betweenNotes="283" belowLine="0" aboveLine="1000"/>` +
|
|
920
|
+
`<hh:numbering type="CONTINUOUS" newNum="1"/>` +
|
|
921
|
+
`<hh:placement place="EACH_COLUMN" beneathText="0"/>` +
|
|
922
|
+
`</hh:footNotePr>` +
|
|
923
|
+
`<hh:endNotePr><hh:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar="" supscript="1"/>` +
|
|
924
|
+
`<hh:noteLine length="-1" type="SOLID" width="0.25 mm" color="#000000"/>` +
|
|
925
|
+
`<hh:noteSpacing betweenNotes="0" belowLine="0" aboveLine="1000"/>` +
|
|
926
|
+
`<hh:numbering type="CONTINUOUS" newNum="1"/>` +
|
|
927
|
+
`<hh:placement place="END_OF_DOCUMENT" beneathText="0"/>` +
|
|
928
|
+
`</hh:endNotePr>` +
|
|
929
|
+
pageBorderFill +
|
|
930
|
+
`</hh:secPr>` +
|
|
931
|
+
`</hh:secPrList>`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
820
935
|
function buildHeaderXml(dims: PageDims, meta: DocMeta, ctx: HwpxCtx): string {
|
|
821
936
|
// 언어별 폰트 (LangFontBank → XML)
|
|
822
937
|
const fontFacesXml = ctx.fontBank.toXml();
|
|
@@ -846,14 +961,18 @@ function buildHeaderXml(dims: PageDims, meta: DocMeta, ctx: HwpxCtx): string {
|
|
|
846
961
|
`</hh:charPr>`;
|
|
847
962
|
}
|
|
848
963
|
|
|
849
|
-
// paraPr 목록
|
|
964
|
+
// paraPr 목록 (동적 정렬 및 줄바꿈 적용)
|
|
850
965
|
let paraPrXml = "";
|
|
851
966
|
for (const pp of ctx.paraPrs) {
|
|
967
|
+
const ver = pp.verAlign ?? "BASELINE";
|
|
968
|
+
const wrap = pp.lineWrap ?? "BREAK";
|
|
969
|
+
const lsType = pp.lineSpacingFixed !== undefined ? "FIXED" : "PERCENT";
|
|
970
|
+
const lsValue = pp.lineSpacingFixed !== undefined ? pp.lineSpacingFixed : pp.lineSpacing;
|
|
852
971
|
paraPrXml +=
|
|
853
972
|
`<hh:paraPr id="${pp.id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="0" suppressLineNumbers="0" checked="0">` +
|
|
854
|
-
`<hh:align horizontal="${pp.align}" vertical="
|
|
973
|
+
`<hh:align horizontal="${pp.align}" vertical="${ver}"/>` +
|
|
855
974
|
`<hh:heading type="NONE" idRef="0" level="0"/>` +
|
|
856
|
-
`<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="
|
|
975
|
+
`<hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="KEEP_WORD" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="${wrap}"/>` +
|
|
857
976
|
`<hh:autoSpacing eAsianEng="0" eAsianNum="0"/>` +
|
|
858
977
|
`<hh:margin>` +
|
|
859
978
|
`<hc:intent value="${pp.intentHwp}" unit="HWPUNIT"/>` +
|
|
@@ -862,7 +981,7 @@ function buildHeaderXml(dims: PageDims, meta: DocMeta, ctx: HwpxCtx): string {
|
|
|
862
981
|
`<hc:prev value="${pp.prevHwp}" unit="HWPUNIT"/>` +
|
|
863
982
|
`<hc:next value="${pp.nextHwp}" unit="HWPUNIT"/>` +
|
|
864
983
|
`</hh:margin>` +
|
|
865
|
-
`<hh:lineSpacing type="
|
|
984
|
+
`<hh:lineSpacing type="${lsType}" value="${lsValue}" unit="HWPUNIT"/>` +
|
|
866
985
|
`<hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>` +
|
|
867
986
|
`</hh:paraPr>`;
|
|
868
987
|
}
|
|
@@ -896,45 +1015,8 @@ function buildHeaderXml(dims: PageDims, meta: DocMeta, ctx: HwpxCtx): string {
|
|
|
896
1015
|
`<hh:paraProperties itemCnt="${ctx.paraPrs.length}">${paraPrXml}</hh:paraProperties>` +
|
|
897
1016
|
stylesXml +
|
|
898
1017
|
`</hh:refList>` +
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
<hh:applyFontWeightToBold />
|
|
902
|
-
<hh:useInnerUnderline />
|
|
903
|
-
<hh:useLowercaseStrikeout />
|
|
904
|
-
<hh:extendLineheightToOffset />
|
|
905
|
-
<hh:treatQuotationAsLatin />
|
|
906
|
-
<hh:doNotAlignWhitespaceOnRight />
|
|
907
|
-
<hh:doNotAdjustWordInJustify />
|
|
908
|
-
<hh:baseCharUnitOnEAsian />
|
|
909
|
-
<hh:baseCharUnitOfIndentOnFirstChar />
|
|
910
|
-
<hh:adjustLineheightToFont />
|
|
911
|
-
<hh:adjustBaselineInFixedLinespacing />
|
|
912
|
-
<hh:applyPrevspacingBeneathObject />
|
|
913
|
-
<hh:applyNextspacingOfLastPara />
|
|
914
|
-
<hh:adjustParaBorderfillToSpacing />
|
|
915
|
-
<hh:connectParaBorderfillOfEqualBorder />
|
|
916
|
-
<hh:adjustParaBorderOffsetWithBorder />
|
|
917
|
-
<hh:extendLineheightToParaBorderOffset />
|
|
918
|
-
<hh:applyParaBorderToOutside />
|
|
919
|
-
<hh:applyMinColumnWidthTo1mm />
|
|
920
|
-
<hh:applyTabPosBasedOnSegment />
|
|
921
|
-
<hh:breakTabOverLine />
|
|
922
|
-
<hh:adjustVertPosOfLine />
|
|
923
|
-
<hh:doNotAlignLastForbidden />
|
|
924
|
-
<hh:adjustMarginFromAdjustLineheight />
|
|
925
|
-
<hh:baseLineSpacingOnLineGrid />
|
|
926
|
-
<hh:applyCharSpacingToCharGrid />
|
|
927
|
-
<hh:doNotApplyGridInHeaderFooter />
|
|
928
|
-
<hh:applyExtendHeaderFooterEachSection />
|
|
929
|
-
<hh:doNotApplyLinegridAtNoLinespacing />
|
|
930
|
-
<hh:doNotAdjustEmptyAnchorLine />
|
|
931
|
-
<hh:overlapBothAllowOverlap />
|
|
932
|
-
<hh:extendVertLimitToPageMargins />
|
|
933
|
-
<hh:doNotHoldAnchorOfTable />
|
|
934
|
-
<hh:doNotFormattingAtBeneathAnchor />
|
|
935
|
-
<hh:adjustBaselineOfObjectToBottom />
|
|
936
|
-
</hh:layoutCompatibility>
|
|
937
|
-
</hh:compatibleDocument>` +
|
|
1018
|
+
buildHeaderSecPrListXml(dims) +
|
|
1019
|
+
`<hh:compatibleDocument targetProgram="HWP201X"><hh:layoutCompatibility/></hh:compatibleDocument>` +
|
|
938
1020
|
`<hh:docOption><hh:linkinfo path="" pageInherit="0" footnoteInherit="0"/></hh:docOption>` +
|
|
939
1021
|
`<hh:trackchageConfig flags="56"/>` +
|
|
940
1022
|
`</hh:head>`
|
|
@@ -956,8 +1038,8 @@ function buildHeaderFooterRunXml(
|
|
|
956
1038
|
const availW = ctx.availableWidth;
|
|
957
1039
|
const mtHwp = Metric.ptToHwp(dims.mt);
|
|
958
1040
|
const mbHwp = Metric.ptToHwp(dims.mb);
|
|
959
|
-
const headerZoneH = dims.headerPt ?
|
|
960
|
-
const footerZoneH = dims.footerPt ?
|
|
1041
|
+
const headerZoneH = dims.headerPt ? Metric.ptToHwp(dims.headerPt) : 4252; // 기본값 15mm
|
|
1042
|
+
const footerZoneH = dims.footerPt ? Metric.ptToHwp(dims.footerPt) : 4252; // 기본값 15mm
|
|
961
1043
|
|
|
962
1044
|
let inner = "";
|
|
963
1045
|
|
|
@@ -967,6 +1049,7 @@ function buildHeaderFooterRunXml(
|
|
|
967
1049
|
|
|
968
1050
|
// 2. 헤더들 생성
|
|
969
1051
|
for (const [type, paras] of Object.entries(headers)) {
|
|
1052
|
+
if (!Array.isArray(paras) || paras.length === 0) continue;
|
|
970
1053
|
const applyPageType = type === "even" ? "EVEN" : (type === "default" || type === "first" ? "BOTH" : "ODD");
|
|
971
1054
|
const savedId = ctx.nextElementId;
|
|
972
1055
|
ctx.nextElementId = 0;
|
|
@@ -986,6 +1069,7 @@ function buildHeaderFooterRunXml(
|
|
|
986
1069
|
|
|
987
1070
|
// 3. 푸터들 생성
|
|
988
1071
|
for (const [type, paras] of Object.entries(footers)) {
|
|
1072
|
+
if (!Array.isArray(paras) || paras.length === 0) continue;
|
|
989
1073
|
const applyPageType = type === "even" ? "EVEN" : (type === "default" || type === "first" ? "BOTH" : "ODD");
|
|
990
1074
|
const savedId = ctx.nextElementId;
|
|
991
1075
|
ctx.nextElementId = 0;
|
|
@@ -1028,11 +1112,12 @@ function buildSectionXml(
|
|
|
1028
1112
|
for (let i = 0; i < kids.length; i++) {
|
|
1029
1113
|
const kid = kids[i];
|
|
1030
1114
|
const isFirst = i === 0;
|
|
1031
|
-
|
|
1115
|
+
// secPr은 hs:sec 하위에 직접 기입하므로 문단 내에는 기입하지 않습니다.
|
|
1116
|
+
const curSecPr = "";
|
|
1032
1117
|
const curHfRun = isFirst ? hfRunXml : "";
|
|
1033
1118
|
|
|
1034
1119
|
if (kid.tag === "para") {
|
|
1035
|
-
const { xml, nextVertPos } = encodeParaPositioned(
|
|
1120
|
+
const { xml, nextVertPos, hasPageBreak } = encodeParaPositioned(
|
|
1036
1121
|
kid,
|
|
1037
1122
|
ctx,
|
|
1038
1123
|
vertPos,
|
|
@@ -1041,9 +1126,9 @@ function buildSectionXml(
|
|
|
1041
1126
|
curHfRun,
|
|
1042
1127
|
);
|
|
1043
1128
|
contentXml += xml;
|
|
1044
|
-
vertPos = nextVertPos;
|
|
1129
|
+
vertPos = nextVertPos; // 페이지 브레이크 발생 시에도 누적 절대 좌표 유지
|
|
1045
1130
|
} else if (kid.tag === "grid") {
|
|
1046
|
-
const { xml, nextVertPos } = encodeGridPositioned(
|
|
1131
|
+
const { xml, nextVertPos, hasPageBreak } = encodeGridPositioned(
|
|
1047
1132
|
kid,
|
|
1048
1133
|
ctx,
|
|
1049
1134
|
vertPos,
|
|
@@ -1051,7 +1136,7 @@ function buildSectionXml(
|
|
|
1051
1136
|
curHfRun,
|
|
1052
1137
|
);
|
|
1053
1138
|
contentXml += xml;
|
|
1054
|
-
vertPos = nextVertPos;
|
|
1139
|
+
vertPos = nextVertPos; // 페이지 브레이크 발생 시에도 누적 절대 좌표 유지
|
|
1055
1140
|
}
|
|
1056
1141
|
}
|
|
1057
1142
|
|
|
@@ -1062,14 +1147,14 @@ function buildSectionXml(
|
|
|
1062
1147
|
const { xml: linesegXml } = buildLinesegarray(" ", 0, fs, vs / (fs / 100), availWidth);
|
|
1063
1148
|
contentXml =
|
|
1064
1149
|
`<hp:p id="${ctx.nextElementId++}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0" paraTcId="0">` +
|
|
1065
|
-
secPrXml +
|
|
1066
1150
|
hfRunXml +
|
|
1067
1151
|
`<hp:run charPrIDRef="0" charTcId="0"><hp:t xml:space="preserve"> </hp:t></hp:run>` +
|
|
1068
1152
|
linesegXml +
|
|
1069
1153
|
`</hp:p>`;
|
|
1070
1154
|
}
|
|
1071
1155
|
|
|
1072
|
-
|
|
1156
|
+
// hs:sec 바로 아래 직계 첫 자식으로 secPrXml 기입!
|
|
1157
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?><hs:sec ${NS} xmlns:hwpunitchar="http://www.hancom.co.kr/hwpml/2016/HwpUnitChar">${secPrXml}${contentXml}</hs:sec>`;
|
|
1073
1158
|
}
|
|
1074
1159
|
|
|
1075
1160
|
function buildSecPrXml(dims: PageDims): string {
|
|
@@ -1081,12 +1166,8 @@ function buildSecPrXml(dims: PageDims): string {
|
|
|
1081
1166
|
const mb = Metric.ptToHwp(dims.mb);
|
|
1082
1167
|
// HWPX margin header/footer = header/footer ZONE HEIGHT (not distance from paper edge)
|
|
1083
1168
|
// = top_hwp - header_from_top_hwp (and bottom_hwp - footer_from_bottom_hwp)
|
|
1084
|
-
const headerZone = dims.headerPt
|
|
1085
|
-
|
|
1086
|
-
: 0;
|
|
1087
|
-
const footerZone = dims.footerPt
|
|
1088
|
-
? Math.max(0, mb - Metric.ptToHwp(dims.footerPt))
|
|
1089
|
-
: 0;
|
|
1169
|
+
const headerZone = dims.headerPt ? Metric.ptToHwp(dims.headerPt) : 0;
|
|
1170
|
+
const footerZone = dims.footerPt ? Metric.ptToHwp(dims.footerPt) : 0;
|
|
1090
1171
|
|
|
1091
1172
|
const pageBorderFill =
|
|
1092
1173
|
`<hp:pageBorderFill type="BOTH" borderFillIDRef="1" textBorder="PAPER" headerInside="0" footerInside="0" fillArea="PAPER">` +
|
|
@@ -1105,7 +1186,7 @@ function buildSecPrXml(dims: PageDims): string {
|
|
|
1105
1186
|
`<hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/>` +
|
|
1106
1187
|
`<hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/>` +
|
|
1107
1188
|
`<hp:lineNumberShape restartType="0" countBy="0" distance="0" startNumber="0"/>` +
|
|
1108
|
-
`<hp:pagePr landscape="
|
|
1189
|
+
`<hp:pagePr landscape="WIDELY" width="${wHwp}" height="${hHwp}" gutterType="LEFT_ONLY">` +
|
|
1109
1190
|
`<hp:margin header="${headerZone}" footer="${footerZone}" gutter="0" left="${ml}" right="${mr}" top="${mt}" bottom="${mb}"/>` +
|
|
1110
1191
|
`</hp:pagePr>` +
|
|
1111
1192
|
`<hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/>` +
|
|
@@ -1140,18 +1221,67 @@ function buildLinesegarray(
|
|
|
1140
1221
|
const spacing = vertsizeLine - fontSize;
|
|
1141
1222
|
const baseline = Math.round(fontSize * 0.83);
|
|
1142
1223
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1224
|
+
if (text.length === 0) {
|
|
1225
|
+
const xml = `<hp:linesegarray>` +
|
|
1226
|
+
`<hp:lineseg textpos="0" vertpos="${vertPosStart}" vertsize="${vertsizeLine}" ` +
|
|
1227
|
+
`textheight="${fontSize}" baseline="${baseline}" spacing="${spacing}" ` +
|
|
1228
|
+
`horzpos="0" horzsize="${horzSize}" flags="${LINESEG_FLAGS_FIRST}"/>` +
|
|
1229
|
+
`</hp:linesegarray>`;
|
|
1230
|
+
return { xml, totalHeight: vertsizeLine };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// 문자 단위 정밀 가로 폭 계산 및 자동 줄바꿈 알고리즘 (개행 문자 지원)
|
|
1234
|
+
const lines: { startPos: number; width: number }[] = [];
|
|
1235
|
+
let currentLineWidth = 0;
|
|
1236
|
+
let lineStartIdx = 0;
|
|
1237
|
+
|
|
1238
|
+
for (let i = 0; i < text.length; i++) {
|
|
1239
|
+
const charCode = text.charCodeAt(i);
|
|
1240
|
+
|
|
1241
|
+
// 개행 문자 (\n 또는 \r) 감지 시, 현재 줄 세그먼트를 무조건 마감하고 새로운 줄 시작
|
|
1242
|
+
if (charCode === 10 || charCode === 13) {
|
|
1243
|
+
lines.push({ startPos: lineStartIdx, width: currentLineWidth });
|
|
1244
|
+
lineStartIdx = i + 1;
|
|
1245
|
+
currentLineWidth = 0;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
let charW = fontSize * 0.55; // 기본값 (영문 소문자, 숫자 등)
|
|
1250
|
+
|
|
1251
|
+
if (charCode >= 0xac00 && charCode <= 0xd7a3) {
|
|
1252
|
+
charW = fontSize; // 한글
|
|
1253
|
+
} else if (charCode >= 0x3130 && charCode <= 0x318f) {
|
|
1254
|
+
charW = fontSize; // 한글 자모
|
|
1255
|
+
} else if (charCode >= 0x4e00 && charCode <= 0x9fff) {
|
|
1256
|
+
charW = fontSize; // 한자
|
|
1257
|
+
} else if (charCode >= 65 && charCode <= 90) {
|
|
1258
|
+
charW = fontSize * 0.65; // 영문 대문자
|
|
1259
|
+
} else if (charCode === 32) {
|
|
1260
|
+
charW = fontSize * 0.32; // 공백
|
|
1261
|
+
} else if (charCode > 255) {
|
|
1262
|
+
charW = fontSize; // 기타 전각 문자
|
|
1263
|
+
} else {
|
|
1264
|
+
charW = fontSize * 0.42; // 기타 특수기호
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (currentLineWidth + charW > horzSize && i > lineStartIdx) {
|
|
1268
|
+
lines.push({ startPos: lineStartIdx, width: currentLineWidth });
|
|
1269
|
+
lineStartIdx = i;
|
|
1270
|
+
currentLineWidth = charW;
|
|
1271
|
+
} else {
|
|
1272
|
+
currentLineWidth += charW;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
lines.push({ startPos: lineStartIdx, width: currentLineWidth });
|
|
1148
1276
|
|
|
1149
|
-
|
|
1277
|
+
const lineCount = lines.length;
|
|
1150
1278
|
const linesegParts: string[] = [];
|
|
1279
|
+
|
|
1151
1280
|
for (let i = 0; i < lineCount; i++) {
|
|
1152
1281
|
const flags = i === 0 ? LINESEG_FLAGS_FIRST : LINESEG_FLAGS_OTHER;
|
|
1282
|
+
const textpos = lines[i].startPos;
|
|
1153
1283
|
linesegParts.push(
|
|
1154
|
-
`<hp:lineseg textpos="${
|
|
1284
|
+
`<hp:lineseg textpos="${textpos}" ` +
|
|
1155
1285
|
`vertpos="${vertPosStart + i * vertsizeLine}" ` +
|
|
1156
1286
|
`vertsize="${vertsizeLine}" textheight="${fontSize}" ` +
|
|
1157
1287
|
`baseline="${baseline}" spacing="${spacing}" ` +
|
|
@@ -1172,7 +1302,13 @@ function extractParaText(para: ParaNode): string {
|
|
|
1172
1302
|
const walk = (kids: any[]) => {
|
|
1173
1303
|
for (const k of kids) {
|
|
1174
1304
|
if (k.tag === "span") {
|
|
1175
|
-
for (const c of k.kids)
|
|
1305
|
+
for (const c of k.kids) {
|
|
1306
|
+
if (c.tag === "txt") {
|
|
1307
|
+
text += c.content;
|
|
1308
|
+
} else if (c.tag === "br") {
|
|
1309
|
+
text += "\n";
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1176
1312
|
} else if (k.tag === "link") {
|
|
1177
1313
|
walk(k.kids);
|
|
1178
1314
|
}
|
|
@@ -1201,7 +1337,7 @@ function encodeParaPositioned(
|
|
|
1201
1337
|
secPr = "",
|
|
1202
1338
|
availWidth?: number,
|
|
1203
1339
|
hfRun = "",
|
|
1204
|
-
): { xml: string; nextVertPos: number } {
|
|
1340
|
+
): { xml: string; nextVertPos: number; hasPageBreak: boolean } {
|
|
1205
1341
|
// ✅ 표(Grid)를 포함하는 단락인지 확인
|
|
1206
1342
|
const gridKid = para.kids.find((k): k is GridNode => k.tag === "grid");
|
|
1207
1343
|
if (gridKid) {
|
|
@@ -1268,7 +1404,7 @@ function encodeParaPositioned(
|
|
|
1268
1404
|
linesegXml +
|
|
1269
1405
|
`</hp:p>`;
|
|
1270
1406
|
|
|
1271
|
-
return { xml, nextVertPos: vertPos + totalHeight };
|
|
1407
|
+
return { xml, nextVertPos: vertPos + totalHeight, hasPageBreak };
|
|
1272
1408
|
}
|
|
1273
1409
|
|
|
1274
1410
|
/** ✅ 가이드 준수: 표를 포함하는 단락 인코딩 */
|
|
@@ -1279,7 +1415,7 @@ function encodeTablePara(
|
|
|
1279
1415
|
vertPos: number,
|
|
1280
1416
|
secPr: string,
|
|
1281
1417
|
hfRun: string,
|
|
1282
|
-
): { xml: string; nextVertPos: number } {
|
|
1418
|
+
): { xml: string; nextVertPos: number; hasPageBreak: boolean } {
|
|
1283
1419
|
const paraPrId = ctx.paraPrMap.get(paraPrKey(para.props)) ?? 0;
|
|
1284
1420
|
|
|
1285
1421
|
// 표 알맹이 생성 (기존 로직 재사용)
|
|
@@ -1298,16 +1434,23 @@ function encodeTablePara(
|
|
|
1298
1434
|
`horzpos="0" horzsize="${ctx.availableWidth}" flags="1441792"/>` +
|
|
1299
1435
|
`</hp:linesegarray>`;
|
|
1300
1436
|
|
|
1437
|
+
const hasPageBreak = para.kids.some(
|
|
1438
|
+
(k) => k.tag === "span" && k.kids.some((c) => c.tag === "pb"),
|
|
1439
|
+
);
|
|
1440
|
+
|
|
1441
|
+
const runId = ctx.nextElementId++;
|
|
1301
1442
|
const xml =
|
|
1302
1443
|
`<hp:p id="${ctx.nextElementId++}" paraPrIDRef="${paraPrId}" styleIDRef="0" ` +
|
|
1303
|
-
`pageBreak="0" columnBreak="0" merged="0" paraTcId="0">` +
|
|
1444
|
+
`pageBreak="${hasPageBreak ? 1 : 0}" columnBreak="0" merged="0" paraTcId="0">` +
|
|
1304
1445
|
secPr +
|
|
1446
|
+
`<hp:run id="${runId}" charPrIDRef="0" charTcId="0">` +
|
|
1305
1447
|
gridXml +
|
|
1448
|
+
`</hp:run>` +
|
|
1306
1449
|
hfRun +
|
|
1307
1450
|
linesegXml +
|
|
1308
1451
|
`</hp:p>`;
|
|
1309
1452
|
|
|
1310
|
-
return { xml, nextVertPos: vertPos + totalHeight };
|
|
1453
|
+
return { xml, nextVertPos: vertPos + totalHeight, hasPageBreak };
|
|
1311
1454
|
}
|
|
1312
1455
|
|
|
1313
1456
|
function encodeCodeBlockPositioned(
|
|
@@ -1318,7 +1461,7 @@ function encodeCodeBlockPositioned(
|
|
|
1318
1461
|
fontSize: number,
|
|
1319
1462
|
spacing: number,
|
|
1320
1463
|
vertSize: number,
|
|
1321
|
-
): { xml: string; nextVertPos: number } {
|
|
1464
|
+
): { xml: string; nextVertPos: number; hasPageBreak: boolean } {
|
|
1322
1465
|
const codeBfId = ctx.borderFillBank.addUniform(
|
|
1323
1466
|
{ kind: "solid", pt: 0.5, color: "aaaaaa" },
|
|
1324
1467
|
"f4f4f4",
|
|
@@ -1358,7 +1501,7 @@ function encodeCodeBlockPositioned(
|
|
|
1358
1501
|
linesegXml +
|
|
1359
1502
|
`</hp:p>`;
|
|
1360
1503
|
|
|
1361
|
-
return { xml, nextVertPos: vertPos + totalHeight };
|
|
1504
|
+
return { xml, nextVertPos: vertPos + totalHeight, hasPageBreak: false };
|
|
1362
1505
|
}
|
|
1363
1506
|
|
|
1364
1507
|
function encodeParaKids(kids: ParaNode["kids"], ctx: HwpxCtx): string {
|
|
@@ -1421,7 +1564,7 @@ function encodeRunInner(span: SpanNode): string {
|
|
|
1421
1564
|
const content = esc(kid.content);
|
|
1422
1565
|
if (content) xml += `<hp:t xml:space="preserve">${content}</hp:t>`;
|
|
1423
1566
|
} else if (kid.tag === "br") {
|
|
1424
|
-
xml += `<hp:
|
|
1567
|
+
xml += `<hp:br/>`;
|
|
1425
1568
|
} else if (kid.tag === "pagenum") {
|
|
1426
1569
|
const fmt = (kid as any).format === "roman" ? "ROMAN_LOWER"
|
|
1427
1570
|
: (kid as any).format === "romanCaps" ? "ROMAN_UPPER" : "DIGIT";
|
|
@@ -1569,7 +1712,7 @@ function encodeGridPositioned(
|
|
|
1569
1712
|
vertPos: number,
|
|
1570
1713
|
secPr = "",
|
|
1571
1714
|
hfRun = "",
|
|
1572
|
-
): { xml: string; nextVertPos: number } {
|
|
1715
|
+
): { xml: string; nextVertPos: number; hasPageBreak: boolean } {
|
|
1573
1716
|
const { xml: gridXml, height: tblHeight } = buildGridXml(grid, ctx);
|
|
1574
1717
|
const totalHeight = Math.max(1600, tblHeight);
|
|
1575
1718
|
const fontSize = 1000;
|
|
@@ -1583,15 +1726,18 @@ function encodeGridPositioned(
|
|
|
1583
1726
|
`horzpos="0" horzsize="${ctx.availableWidth}" flags="${LINESEG_FLAGS_FIRST}"/>` +
|
|
1584
1727
|
`</hp:linesegarray>`;
|
|
1585
1728
|
|
|
1729
|
+
const runId = ctx.nextElementId++;
|
|
1586
1730
|
const xml =
|
|
1587
1731
|
`<hp:p id="${ctx.nextElementId++}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0" paraTcId="0">` +
|
|
1588
1732
|
secPr +
|
|
1589
1733
|
hfRun +
|
|
1734
|
+
`<hp:run id="${runId}" charPrIDRef="0" charTcId="0">` +
|
|
1590
1735
|
gridXml +
|
|
1736
|
+
`</hp:run>` +
|
|
1591
1737
|
linesegXml +
|
|
1592
1738
|
`</hp:p>`;
|
|
1593
1739
|
|
|
1594
|
-
return { xml, nextVertPos: vertPos + totalHeight };
|
|
1740
|
+
return { xml, nextVertPos: vertPos + totalHeight, hasPageBreak: false };
|
|
1595
1741
|
}
|
|
1596
1742
|
function buildGridXml(
|
|
1597
1743
|
grid: GridNode,
|