postext 0.3.15 → 0.3.17
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/dist/__tests__/createLayout.test.js +12 -13
- package/dist/__tests__/createLayout.test.js.map +1 -1
- package/dist/__tests__/defaults/resourceTypes.test.d.ts +2 -0
- package/dist/__tests__/defaults/resourceTypes.test.d.ts.map +1 -0
- package/dist/__tests__/defaults/resourceTypes.test.js +69 -0
- package/dist/__tests__/defaults/resourceTypes.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +54 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/parse/inlineRef.test.d.ts +2 -0
- package/dist/__tests__/parse/inlineRef.test.d.ts.map +1 -0
- package/dist/__tests__/parse/inlineRef.test.js +83 -0
- package/dist/__tests__/parse/inlineRef.test.js.map +1 -0
- package/dist/__tests__/parse/resourceDirective.test.d.ts +2 -0
- package/dist/__tests__/parse/resourceDirective.test.d.ts.map +1 -0
- package/dist/__tests__/parse/resourceDirective.test.js +55 -0
- package/dist/__tests__/parse/resourceDirective.test.js.map +1 -0
- package/dist/__tests__/pipeline/floatPlacement.test.d.ts +2 -0
- package/dist/__tests__/pipeline/floatPlacement.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/floatPlacement.test.js +141 -0
- package/dist/__tests__/pipeline/floatPlacement.test.js.map +1 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.d.ts +2 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.js +107 -0
- package/dist/__tests__/pipeline/inlineRefRender.test.js.map +1 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.d.ts +2 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.js +186 -0
- package/dist/__tests__/pipeline/resourceNumbering.test.js.map +1 -0
- package/dist/__tests__/table/model.test.d.ts +2 -0
- package/dist/__tests__/table/model.test.d.ts.map +1 -0
- package/dist/__tests__/table/model.test.js +187 -0
- package/dist/__tests__/table/model.test.js.map +1 -0
- package/dist/canvas-backend/blockRender.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.js +25 -6
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/headerFooter.d.ts +3 -2
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +63 -2
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/index.d.ts +2 -0
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +11 -0
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts +28 -0
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -0
- package/dist/canvas-backend/renderResourceBlock.js +146 -0
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -0
- package/dist/defaults/bodyText.d.ts.map +1 -1
- package/dist/defaults/bodyText.js +30 -0
- package/dist/defaults/bodyText.js.map +1 -1
- package/dist/defaults/captionStyle.d.ts +9 -0
- package/dist/defaults/captionStyle.d.ts.map +1 -0
- package/dist/defaults/captionStyle.js +74 -0
- package/dist/defaults/captionStyle.js.map +1 -0
- package/dist/defaults/debug.d.ts.map +1 -1
- package/dist/defaults/debug.js +6 -0
- package/dist/defaults/debug.js.map +1 -1
- package/dist/defaults/headerFooter.d.ts +20 -18
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +269 -165
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +29 -6
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +3 -0
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +19 -0
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/resourceTypes.d.ts +8 -0
- package/dist/defaults/resourceTypes.d.ts.map +1 -0
- package/dist/defaults/resourceTypes.js +43 -0
- package/dist/defaults/resourceTypes.js.map +1 -0
- package/dist/defaults/shared.d.ts.map +1 -1
- package/dist/defaults/shared.js +30 -0
- package/dist/defaults/shared.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts +11 -0
- package/dist/defaults/tableStyle.d.ts.map +1 -0
- package/dist/defaults/tableStyle.js +114 -0
- package/dist/defaults/tableStyle.js.map +1 -0
- package/dist/design/layout.d.ts +94 -0
- package/dist/design/layout.d.ts.map +1 -0
- package/dist/design/layout.js +642 -0
- package/dist/design/layout.js.map +1 -0
- package/dist/design/placeholders.d.ts +29 -0
- package/dist/design/placeholders.d.ts.map +1 -0
- package/dist/design/placeholders.js +126 -0
- package/dist/design/placeholders.js.map +1 -0
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +91 -1
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +1 -0
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +2 -1
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/measure/cache.js +1 -1
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/rich.d.ts +7 -0
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +30 -0
- package/dist/measure/rich.js.map +1 -1
- package/dist/numbering.d.ts +16 -0
- package/dist/numbering.d.ts.map +1 -1
- package/dist/numbering.js +28 -18
- package/dist/numbering.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +37 -9
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/inlineFormatting.d.ts +38 -0
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +84 -0
- package/dist/parse/inlineFormatting.js.map +1 -1
- package/dist/parse/sourceMapping.d.ts.map +1 -1
- package/dist/parse/sourceMapping.js +20 -0
- package/dist/parse/sourceMapping.js.map +1 -1
- package/dist/parse/types.d.ts +20 -1
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +389 -17
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildBlockKind.d.ts +14 -0
- package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
- package/dist/pipeline/buildBlockKind.js +16 -1
- package/dist/pipeline/buildBlockKind.js.map +1 -1
- package/dist/pipeline/buildHelpers.d.ts.map +1 -1
- package/dist/pipeline/buildHelpers.js +7 -1
- package/dist/pipeline/buildHelpers.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts +17 -1
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +32 -0
- package/dist/pipeline/buildMeasurement.js.map +1 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +3 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +45 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -0
- package/dist/pipeline/floatPlacement.js +68 -0
- package/dist/pipeline/floatPlacement.js.map +1 -0
- package/dist/pipeline/headerFooter.d.ts +23 -7
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +258 -100
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/placeholders.d.ts +6 -0
- package/dist/pipeline/placeholders.d.ts.map +1 -1
- package/dist/pipeline/placeholders.js +46 -0
- package/dist/pipeline/placeholders.js.map +1 -1
- package/dist/pipeline/placement.d.ts +15 -2
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +38 -3
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +58 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -0
- package/dist/pipeline/resourceLayout.js +338 -0
- package/dist/pipeline/resourceLayout.js.map +1 -0
- package/dist/pipeline/resourceNumbering.d.ts +54 -0
- package/dist/pipeline/resourceNumbering.d.ts.map +1 -0
- package/dist/pipeline/resourceNumbering.js +218 -0
- package/dist/pipeline/resourceNumbering.js.map +1 -0
- package/dist/pipeline/styles.d.ts +6 -0
- package/dist/pipeline/styles.d.ts.map +1 -1
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/styles.js.map +1 -1
- package/dist/table/model.d.ts +53 -0
- package/dist/table/model.d.ts.map +1 -0
- package/dist/table/model.js +253 -0
- package/dist/table/model.js.map +1 -0
- package/dist/types.d.ts +397 -41
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +165 -18
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { dimensionToPx } from '../units';
|
|
2
|
+
import { buildFontString, measureTextWidth } from '../measure';
|
|
3
|
+
import { hyphenateText } from '../hyphenate';
|
|
4
|
+
import { resolveDesignPlaceholders, } from './placeholders';
|
|
5
|
+
function pageMatchesParity(pageIndex, parity) {
|
|
6
|
+
if (parity === 'all')
|
|
7
|
+
return true;
|
|
8
|
+
const pageNumber = pageIndex + 1;
|
|
9
|
+
const isOdd = pageNumber % 2 === 1;
|
|
10
|
+
return parity === 'odd' ? isOdd : !isOdd;
|
|
11
|
+
}
|
|
12
|
+
function dimPx(d, dpi, baseFontSizePx) {
|
|
13
|
+
if (!d)
|
|
14
|
+
return 0;
|
|
15
|
+
return dimensionToPx(d, dpi, baseFontSizePx);
|
|
16
|
+
}
|
|
17
|
+
function resolvePadding(padding, dpi, baseFontSizePx) {
|
|
18
|
+
return {
|
|
19
|
+
top: dimPx(padding?.top, dpi, baseFontSizePx),
|
|
20
|
+
right: dimPx(padding?.right, dpi, baseFontSizePx),
|
|
21
|
+
bottom: dimPx(padding?.bottom, dpi, baseFontSizePx),
|
|
22
|
+
left: dimPx(padding?.left, dpi, baseFontSizePx),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function resolveBox(style, dpi, baseFontSizePx) {
|
|
26
|
+
if (!style)
|
|
27
|
+
return undefined;
|
|
28
|
+
return {
|
|
29
|
+
backgroundColor: style.backgroundColor?.hex,
|
|
30
|
+
borderColor: style.borderColor?.hex,
|
|
31
|
+
borderWidthPx: dimPx(style.borderWidth, dpi, baseFontSizePx),
|
|
32
|
+
borderRadiusPx: dimPx(style.borderRadius, dpi, baseFontSizePx),
|
|
33
|
+
padding: resolvePadding(style.padding, dpi, baseFontSizePx),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function colorHex(c) {
|
|
37
|
+
return c?.hex ?? '#000000';
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Anchor dependency graph
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function anchorTargetId(placement) {
|
|
43
|
+
if (placement.anchor.to === 'container')
|
|
44
|
+
return undefined;
|
|
45
|
+
return placement.anchor.to.slice(1);
|
|
46
|
+
}
|
|
47
|
+
function topoSort(elements, issues) {
|
|
48
|
+
const byId = new Map();
|
|
49
|
+
for (const el of elements)
|
|
50
|
+
byId.set(el.id, el);
|
|
51
|
+
// Detect dangling
|
|
52
|
+
for (const el of elements) {
|
|
53
|
+
const target = anchorTargetId(el.placement);
|
|
54
|
+
if (target && !byId.has(target)) {
|
|
55
|
+
issues.push({ kind: 'danglingAnchor', elementId: el.id, targetId: target });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const visited = new Set();
|
|
59
|
+
const visiting = new Set();
|
|
60
|
+
const out = [];
|
|
61
|
+
function visit(el, path) {
|
|
62
|
+
if (visited.has(el.id))
|
|
63
|
+
return;
|
|
64
|
+
if (visiting.has(el.id)) {
|
|
65
|
+
issues.push({ kind: 'cyclicAnchor', elementId: el.id });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
visiting.add(el.id);
|
|
69
|
+
const target = anchorTargetId(el.placement);
|
|
70
|
+
if (target) {
|
|
71
|
+
const dep = byId.get(target);
|
|
72
|
+
if (dep)
|
|
73
|
+
visit(dep, [...path, el.id]);
|
|
74
|
+
}
|
|
75
|
+
visiting.delete(el.id);
|
|
76
|
+
visited.add(el.id);
|
|
77
|
+
out.push(el);
|
|
78
|
+
}
|
|
79
|
+
for (const el of elements)
|
|
80
|
+
visit(el, []);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function resolveContainerAnchor(edge, ref) {
|
|
84
|
+
// Container-relative nine-point grid: pin the corresponding corner of the
|
|
85
|
+
// element to the corresponding point of the container.
|
|
86
|
+
switch (edge) {
|
|
87
|
+
case 'top-left':
|
|
88
|
+
return { anchorX: ref.x, anchorY: ref.y, pinX: 'start', pinY: 'start' };
|
|
89
|
+
case 'top':
|
|
90
|
+
return { anchorX: ref.x + ref.width / 2, anchorY: ref.y, pinX: 'middle', pinY: 'start' };
|
|
91
|
+
case 'top-right':
|
|
92
|
+
return { anchorX: ref.x + ref.width, anchorY: ref.y, pinX: 'end', pinY: 'start' };
|
|
93
|
+
case 'left':
|
|
94
|
+
return { anchorX: ref.x, anchorY: ref.y + ref.height / 2, pinX: 'start', pinY: 'middle' };
|
|
95
|
+
case 'center':
|
|
96
|
+
return { anchorX: ref.x + ref.width / 2, anchorY: ref.y + ref.height / 2, pinX: 'middle', pinY: 'middle' };
|
|
97
|
+
case 'right':
|
|
98
|
+
return { anchorX: ref.x + ref.width, anchorY: ref.y + ref.height / 2, pinX: 'end', pinY: 'middle' };
|
|
99
|
+
case 'bottom-left':
|
|
100
|
+
return { anchorX: ref.x, anchorY: ref.y + ref.height, pinX: 'start', pinY: 'end' };
|
|
101
|
+
case 'bottom':
|
|
102
|
+
return { anchorX: ref.x + ref.width / 2, anchorY: ref.y + ref.height, pinX: 'middle', pinY: 'end' };
|
|
103
|
+
case 'bottom-right':
|
|
104
|
+
return { anchorX: ref.x + ref.width, anchorY: ref.y + ref.height, pinX: 'end', pinY: 'end' };
|
|
105
|
+
default:
|
|
106
|
+
// Fallback when an element-to-element edge was set with anchor.to = 'container'.
|
|
107
|
+
return { anchorX: ref.x, anchorY: ref.y, pinX: 'start', pinY: 'start' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function resolveElementAnchor(edge, ref) {
|
|
111
|
+
switch (edge) {
|
|
112
|
+
case 'right-of':
|
|
113
|
+
return { anchorX: ref.x + ref.width, anchorY: ref.y, pinX: 'start', pinY: 'start' };
|
|
114
|
+
case 'left-of':
|
|
115
|
+
return { anchorX: ref.x, anchorY: ref.y, pinX: 'end', pinY: 'start' };
|
|
116
|
+
case 'below':
|
|
117
|
+
return { anchorX: ref.x, anchorY: ref.y + ref.height, pinX: 'start', pinY: 'start' };
|
|
118
|
+
case 'above':
|
|
119
|
+
return { anchorX: ref.x, anchorY: ref.y, pinX: 'start', pinY: 'end' };
|
|
120
|
+
case 'align-top':
|
|
121
|
+
return { anchorX: ref.x, anchorY: ref.y, pinX: 'start', pinY: 'start' };
|
|
122
|
+
case 'align-bottom':
|
|
123
|
+
return { anchorX: ref.x, anchorY: ref.y + ref.height, pinX: 'start', pinY: 'end' };
|
|
124
|
+
case 'align-left':
|
|
125
|
+
return { anchorX: ref.x, anchorY: ref.y, pinX: 'start', pinY: 'start' };
|
|
126
|
+
case 'align-right':
|
|
127
|
+
return { anchorX: ref.x + ref.width, anchorY: ref.y, pinX: 'end', pinY: 'start' };
|
|
128
|
+
default:
|
|
129
|
+
// Fallback when a container edge was set with anchor.to = '#id' — treat
|
|
130
|
+
// as align-top (match target's top-left).
|
|
131
|
+
return { anchorX: ref.x, anchorY: ref.y, pinX: 'start', pinY: 'start' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Text layout — wrap / ellipsis / clip
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
function ellipsize(text, fontString, maxWidth, mode) {
|
|
138
|
+
const ellipsis = '…';
|
|
139
|
+
if (measureTextWidth(text, fontString) <= maxWidth)
|
|
140
|
+
return text;
|
|
141
|
+
const ellipsisWidth = measureTextWidth(ellipsis, fontString);
|
|
142
|
+
if (ellipsisWidth > maxWidth)
|
|
143
|
+
return '';
|
|
144
|
+
const budget = maxWidth - ellipsisWidth;
|
|
145
|
+
if (mode === 'end') {
|
|
146
|
+
let lo = 0;
|
|
147
|
+
let hi = text.length;
|
|
148
|
+
while (lo < hi) {
|
|
149
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
150
|
+
if (measureTextWidth(text.slice(0, mid), fontString) <= budget) {
|
|
151
|
+
lo = mid;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
hi = mid - 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return text.slice(0, lo) + ellipsis;
|
|
158
|
+
}
|
|
159
|
+
if (mode === 'start') {
|
|
160
|
+
let lo = 0;
|
|
161
|
+
let hi = text.length;
|
|
162
|
+
while (lo < hi) {
|
|
163
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
164
|
+
if (measureTextWidth(text.slice(mid), fontString) <= budget) {
|
|
165
|
+
hi = mid;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
lo = mid + 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return ellipsis + text.slice(lo);
|
|
172
|
+
}
|
|
173
|
+
// middle
|
|
174
|
+
let leftLen = 0;
|
|
175
|
+
let rightLen = 0;
|
|
176
|
+
while (true) {
|
|
177
|
+
const candidate = text.slice(0, leftLen + 1) + text.slice(text.length - rightLen);
|
|
178
|
+
if (measureTextWidth(candidate, fontString) + ellipsisWidth > maxWidth)
|
|
179
|
+
break;
|
|
180
|
+
leftLen++;
|
|
181
|
+
if (leftLen + rightLen >= text.length)
|
|
182
|
+
break;
|
|
183
|
+
const candidate2 = text.slice(0, leftLen) + text.slice(text.length - (rightLen + 1));
|
|
184
|
+
if (measureTextWidth(candidate2, fontString) + ellipsisWidth > maxWidth)
|
|
185
|
+
break;
|
|
186
|
+
rightLen++;
|
|
187
|
+
if (leftLen + rightLen >= text.length)
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
return text.slice(0, leftLen) + ellipsis + text.slice(text.length - rightLen);
|
|
191
|
+
}
|
|
192
|
+
const SOFT_HYPHEN = '\u00AD';
|
|
193
|
+
function breakWordWithHyphenation(word, fontString, maxWidth, useHyphenation) {
|
|
194
|
+
// Returns a split of the word where head (with trailing hyphen if hyphenated)
|
|
195
|
+
// fits in maxWidth. Returns undefined if no split is possible.
|
|
196
|
+
if (useHyphenation) {
|
|
197
|
+
const hy = hyphenateText(word);
|
|
198
|
+
if (hy.includes(SOFT_HYPHEN)) {
|
|
199
|
+
const parts = hy.split(SOFT_HYPHEN);
|
|
200
|
+
let best;
|
|
201
|
+
for (let i = 1; i < parts.length; i++) {
|
|
202
|
+
const headRaw = parts.slice(0, i).join('');
|
|
203
|
+
const tailRaw = parts.slice(i).join('');
|
|
204
|
+
const headWithHyphen = headRaw + '-';
|
|
205
|
+
if (measureTextWidth(headWithHyphen, fontString) <= maxWidth) {
|
|
206
|
+
best = { head: headWithHyphen, tail: tailRaw };
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (best)
|
|
213
|
+
return best;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Last-resort character break so text never overflows when it can't fit.
|
|
217
|
+
let lo = 1;
|
|
218
|
+
let hi = word.length;
|
|
219
|
+
while (lo < hi) {
|
|
220
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
221
|
+
if (measureTextWidth(word.slice(0, mid), fontString) <= maxWidth) {
|
|
222
|
+
lo = mid;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
hi = mid - 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (lo < 1)
|
|
229
|
+
return undefined;
|
|
230
|
+
return { head: word.slice(0, lo), tail: word.slice(lo) };
|
|
231
|
+
}
|
|
232
|
+
function wrapToWidth(text, fontString, maxWidth, hyphenate) {
|
|
233
|
+
// Greedy word wrap; preserves existing line breaks.
|
|
234
|
+
const paragraphs = text.split('\n');
|
|
235
|
+
const out = [];
|
|
236
|
+
for (const para of paragraphs) {
|
|
237
|
+
const words = para.split(/(\s+)/);
|
|
238
|
+
let current = '';
|
|
239
|
+
for (const part of words) {
|
|
240
|
+
const test = current + part;
|
|
241
|
+
if (measureTextWidth(test, fontString) <= maxWidth || (current.length === 0 && /^\s*$/.test(part))) {
|
|
242
|
+
current = test;
|
|
243
|
+
}
|
|
244
|
+
else if (current.length === 0) {
|
|
245
|
+
// Single word doesn't fit — try hyphenation / char break.
|
|
246
|
+
let remaining = part;
|
|
247
|
+
while (remaining.length > 0) {
|
|
248
|
+
if (measureTextWidth(remaining, fontString) <= maxWidth) {
|
|
249
|
+
current = remaining;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
const split = breakWordWithHyphenation(remaining, fontString, maxWidth, hyphenate);
|
|
253
|
+
if (!split || split.head.length === 0) {
|
|
254
|
+
current = remaining;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
out.push(split.head);
|
|
258
|
+
remaining = split.tail;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
out.push(current.replace(/\s+$/, ''));
|
|
263
|
+
// New line starts with this part; if the part itself doesn't fit, split it.
|
|
264
|
+
const trimmed = part.replace(/^\s+/, '');
|
|
265
|
+
if (measureTextWidth(trimmed, fontString) <= maxWidth) {
|
|
266
|
+
current = trimmed;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
let remaining = trimmed;
|
|
270
|
+
current = '';
|
|
271
|
+
while (remaining.length > 0) {
|
|
272
|
+
if (measureTextWidth(remaining, fontString) <= maxWidth) {
|
|
273
|
+
current = remaining;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
const split = breakWordWithHyphenation(remaining, fontString, maxWidth, hyphenate);
|
|
277
|
+
if (!split || split.head.length === 0) {
|
|
278
|
+
current = remaining;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
out.push(split.head);
|
|
282
|
+
remaining = split.tail;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (current.length > 0)
|
|
288
|
+
out.push(current.replace(/\s+$/, ''));
|
|
289
|
+
else
|
|
290
|
+
out.push('');
|
|
291
|
+
}
|
|
292
|
+
return out.length > 0 ? out : [''];
|
|
293
|
+
}
|
|
294
|
+
function layoutText(text, fontString, fontSizePx, lineHeight, overflow,
|
|
295
|
+
/** When not undefined, constrains text width. Otherwise measure natural. */
|
|
296
|
+
maxContentWidth, hyphenate) {
|
|
297
|
+
const lineHeightPx = fontSizePx * lineHeight;
|
|
298
|
+
if (maxContentWidth === undefined) {
|
|
299
|
+
// Single natural line.
|
|
300
|
+
const width = measureTextWidth(text, fontString);
|
|
301
|
+
return {
|
|
302
|
+
lines: [{
|
|
303
|
+
text,
|
|
304
|
+
width,
|
|
305
|
+
topY: 0,
|
|
306
|
+
baselineY: lineHeightPx * 0.8,
|
|
307
|
+
height: lineHeightPx,
|
|
308
|
+
}],
|
|
309
|
+
contentWidth: width,
|
|
310
|
+
contentHeight: lineHeightPx,
|
|
311
|
+
needsClip: false,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (overflow === 'wrap') {
|
|
315
|
+
const lines = wrapToWidth(text, fontString, maxContentWidth, hyphenate ?? false);
|
|
316
|
+
const wrapped = lines.map((t, i) => ({
|
|
317
|
+
text: t,
|
|
318
|
+
width: measureTextWidth(t, fontString),
|
|
319
|
+
topY: i * lineHeightPx,
|
|
320
|
+
baselineY: i * lineHeightPx + lineHeightPx * 0.8,
|
|
321
|
+
height: lineHeightPx,
|
|
322
|
+
}));
|
|
323
|
+
const w = wrapped.reduce((m, l) => Math.max(m, l.width), 0);
|
|
324
|
+
return {
|
|
325
|
+
lines: wrapped,
|
|
326
|
+
contentWidth: w,
|
|
327
|
+
contentHeight: wrapped.length * lineHeightPx,
|
|
328
|
+
needsClip: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (overflow === 'clip') {
|
|
332
|
+
const natural = measureTextWidth(text, fontString);
|
|
333
|
+
return {
|
|
334
|
+
lines: [{
|
|
335
|
+
text,
|
|
336
|
+
width: natural,
|
|
337
|
+
topY: 0,
|
|
338
|
+
baselineY: lineHeightPx * 0.8,
|
|
339
|
+
height: lineHeightPx,
|
|
340
|
+
}],
|
|
341
|
+
contentWidth: Math.min(natural, maxContentWidth),
|
|
342
|
+
contentHeight: lineHeightPx,
|
|
343
|
+
needsClip: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// ellipsis-*
|
|
347
|
+
const mode = overflow === 'ellipsis-start' ? 'start'
|
|
348
|
+
: overflow === 'ellipsis-middle' ? 'middle'
|
|
349
|
+
: 'end';
|
|
350
|
+
const visible = ellipsize(text, fontString, maxContentWidth, mode);
|
|
351
|
+
const width = measureTextWidth(visible, fontString);
|
|
352
|
+
return {
|
|
353
|
+
lines: [{
|
|
354
|
+
text: visible,
|
|
355
|
+
width,
|
|
356
|
+
topY: 0,
|
|
357
|
+
baselineY: lineHeightPx * 0.8,
|
|
358
|
+
height: lineHeightPx,
|
|
359
|
+
}],
|
|
360
|
+
contentWidth: width,
|
|
361
|
+
contentHeight: lineHeightPx,
|
|
362
|
+
needsClip: false,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Size resolution helpers
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
function resolveFixedSize(size, dpi, baseFontSizePx) {
|
|
369
|
+
if (size === undefined)
|
|
370
|
+
return undefined;
|
|
371
|
+
if (size === 'auto' || size === 'fill')
|
|
372
|
+
return size;
|
|
373
|
+
return dimPx(size, dpi, baseFontSizePx);
|
|
374
|
+
}
|
|
375
|
+
// Clamp helper — returns x + w bounded by container edge when pin is 'start'
|
|
376
|
+
// (element extends to the right/down), etc.
|
|
377
|
+
function fillToContainerEdge(anchorX, pinX, container) {
|
|
378
|
+
if (pinX === 'start')
|
|
379
|
+
return container.x + container.width - anchorX;
|
|
380
|
+
if (pinX === 'end')
|
|
381
|
+
return anchorX - container.x;
|
|
382
|
+
// middle: distance to nearest edge * 2
|
|
383
|
+
const leftDist = anchorX - container.x;
|
|
384
|
+
const rightDist = container.x + container.width - anchorX;
|
|
385
|
+
return Math.min(leftDist, rightDist) * 2;
|
|
386
|
+
}
|
|
387
|
+
function fillToContainerEdgeY(anchorY, pinY, container) {
|
|
388
|
+
if (pinY === 'start')
|
|
389
|
+
return container.y + container.height - anchorY;
|
|
390
|
+
if (pinY === 'end')
|
|
391
|
+
return anchorY - container.y;
|
|
392
|
+
const topDist = anchorY - container.y;
|
|
393
|
+
const botDist = container.y + container.height - anchorY;
|
|
394
|
+
return Math.min(topDist, botDist) * 2;
|
|
395
|
+
}
|
|
396
|
+
function edgeXFromPin(anchorX, pinX, width) {
|
|
397
|
+
if (pinX === 'start')
|
|
398
|
+
return anchorX;
|
|
399
|
+
if (pinX === 'end')
|
|
400
|
+
return anchorX - width;
|
|
401
|
+
return anchorX - width / 2;
|
|
402
|
+
}
|
|
403
|
+
function edgeYFromPin(anchorY, pinY, height) {
|
|
404
|
+
if (pinY === 'start')
|
|
405
|
+
return anchorY;
|
|
406
|
+
if (pinY === 'end')
|
|
407
|
+
return anchorY - height;
|
|
408
|
+
return anchorY - height / 2;
|
|
409
|
+
}
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Main engine
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
/** Layout one design slot against its container. */
|
|
414
|
+
export function layoutDesignSlot(slot, context, pageIndex) {
|
|
415
|
+
const issues = [];
|
|
416
|
+
const containerRef = {
|
|
417
|
+
x: context.container.x,
|
|
418
|
+
y: context.container.y,
|
|
419
|
+
width: context.container.width,
|
|
420
|
+
height: context.container.height,
|
|
421
|
+
};
|
|
422
|
+
// Filter by parity first.
|
|
423
|
+
const candidates = slot.elements.filter((el) => pageMatchesParity(pageIndex, el.parity));
|
|
424
|
+
// Topologically sort (dependencies first).
|
|
425
|
+
const ordered = topoSort(candidates, issues);
|
|
426
|
+
// Precompute: resolved text contents (placeholder-expanded).
|
|
427
|
+
const textContent = new Map();
|
|
428
|
+
for (const el of ordered) {
|
|
429
|
+
if (el.kind === 'text') {
|
|
430
|
+
const { text } = resolveDesignPlaceholders(el.content, context.placeholders);
|
|
431
|
+
textContent.set(el.id, text);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const resolvedGeo = new Map();
|
|
435
|
+
const primitives = [];
|
|
436
|
+
for (const el of ordered) {
|
|
437
|
+
const target = anchorTargetId(el.placement);
|
|
438
|
+
let refGeo = containerRef;
|
|
439
|
+
let useElementEdge = false;
|
|
440
|
+
if (target) {
|
|
441
|
+
const dep = resolvedGeo.get(target);
|
|
442
|
+
if (dep) {
|
|
443
|
+
refGeo = { x: dep.x, y: dep.y, width: dep.width, height: dep.height };
|
|
444
|
+
useElementEdge = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const anchor = useElementEdge
|
|
448
|
+
? resolveElementAnchor(el.placement.anchor.edge, refGeo)
|
|
449
|
+
: resolveContainerAnchor(el.placement.anchor.edge, refGeo);
|
|
450
|
+
const offsetX = dimPx(el.placement.offset?.x, context.dpi);
|
|
451
|
+
const offsetY = dimPx(el.placement.offset?.y, context.dpi);
|
|
452
|
+
const anchorX = anchor.anchorX + offsetX;
|
|
453
|
+
const anchorY = anchor.anchorY + offsetY;
|
|
454
|
+
if (el.kind === 'text') {
|
|
455
|
+
const prim = layoutTextElement(el, textContent.get(el.id) ?? '', {
|
|
456
|
+
anchorX,
|
|
457
|
+
anchorY,
|
|
458
|
+
pinX: anchor.pinX,
|
|
459
|
+
pinY: anchor.pinY,
|
|
460
|
+
}, containerRef, context.dpi, useElementEdge);
|
|
461
|
+
resolvedGeo.set(el.id, prim);
|
|
462
|
+
primitives.push(prim);
|
|
463
|
+
}
|
|
464
|
+
else if (el.kind === 'rule') {
|
|
465
|
+
const prim = layoutRuleElement(el, {
|
|
466
|
+
anchorX,
|
|
467
|
+
anchorY,
|
|
468
|
+
pinX: anchor.pinX,
|
|
469
|
+
pinY: anchor.pinY,
|
|
470
|
+
}, containerRef, context.dpi);
|
|
471
|
+
resolvedGeo.set(el.id, prim);
|
|
472
|
+
primitives.push(prim);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const prim = layoutBoxElement(el, {
|
|
476
|
+
anchorX,
|
|
477
|
+
anchorY,
|
|
478
|
+
pinX: anchor.pinX,
|
|
479
|
+
pinY: anchor.pinY,
|
|
480
|
+
}, containerRef, context.dpi);
|
|
481
|
+
resolvedGeo.set(el.id, prim);
|
|
482
|
+
primitives.push(prim);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
container: context.container,
|
|
487
|
+
primitives,
|
|
488
|
+
issues,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function layoutTextElement(el, text, pin, container, dpi, anchoredToElement) {
|
|
492
|
+
const fontSizePx = dimPx(el.fontSize, dpi);
|
|
493
|
+
const weight = el.fontWeight === 400 ? 'normal' : String(el.fontWeight);
|
|
494
|
+
const style = el.italic ? 'italic' : 'normal';
|
|
495
|
+
const fontString = buildFontString(el.fontFamily, fontSizePx, weight, style);
|
|
496
|
+
const box = resolveBox(el.box, dpi, fontSizePx);
|
|
497
|
+
const padding = box?.padding ?? { top: 0, right: 0, bottom: 0, left: 0 };
|
|
498
|
+
const widthSize = resolveFixedSize(el.placement.size?.width, dpi, fontSizePx);
|
|
499
|
+
const heightSize = resolveFixedSize(el.placement.size?.height, dpi, fontSizePx);
|
|
500
|
+
// Determine content width budget.
|
|
501
|
+
let contentMax;
|
|
502
|
+
let elementWidth;
|
|
503
|
+
let clampToContainer = false;
|
|
504
|
+
if (widthSize === 'auto' || widthSize === undefined) {
|
|
505
|
+
// Auto width: let the text size itself, but never overflow the container.
|
|
506
|
+
// We compute the distance from the anchor to the container edge and use
|
|
507
|
+
// that as an upper bound for wrapping/ellipsis.
|
|
508
|
+
const fillW = fillToContainerEdge(pin.anchorX, pin.pinX, container);
|
|
509
|
+
contentMax = Math.max(0, fillW - padding.left - padding.right);
|
|
510
|
+
clampToContainer = true;
|
|
511
|
+
}
|
|
512
|
+
else if (widthSize === 'fill') {
|
|
513
|
+
const fillW = fillToContainerEdge(pin.anchorX, pin.pinX, container);
|
|
514
|
+
elementWidth = Math.max(0, fillW);
|
|
515
|
+
contentMax = Math.max(0, elementWidth - padding.left - padding.right);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
elementWidth = widthSize;
|
|
519
|
+
contentMax = Math.max(0, elementWidth - padding.left - padding.right);
|
|
520
|
+
}
|
|
521
|
+
const m = layoutText(text, fontString, fontSizePx, el.lineHeight, el.overflow, contentMax, el.hyphenate);
|
|
522
|
+
const contentWidth = m.contentWidth;
|
|
523
|
+
if (elementWidth === undefined)
|
|
524
|
+
elementWidth = contentWidth + padding.left + padding.right;
|
|
525
|
+
if (clampToContainer) {
|
|
526
|
+
const maxW = fillToContainerEdge(pin.anchorX, pin.pinX, container);
|
|
527
|
+
elementWidth = Math.min(elementWidth, Math.max(0, maxW));
|
|
528
|
+
}
|
|
529
|
+
let elementHeight;
|
|
530
|
+
if (heightSize === undefined || heightSize === 'auto') {
|
|
531
|
+
elementHeight = m.contentHeight + padding.top + padding.bottom;
|
|
532
|
+
}
|
|
533
|
+
else if (heightSize === 'fill') {
|
|
534
|
+
elementHeight = Math.max(0, fillToContainerEdgeY(pin.anchorY, pin.pinY, container));
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
elementHeight = heightSize;
|
|
538
|
+
}
|
|
539
|
+
const x = edgeXFromPin(pin.anchorX, pin.pinX, elementWidth);
|
|
540
|
+
const y = edgeYFromPin(pin.anchorY, pin.pinY, elementHeight);
|
|
541
|
+
// When anchored to another element with auto-sized width, wrapped lines
|
|
542
|
+
// should flow from the anchor side: right-of → left-align, left-of →
|
|
543
|
+
// right-align. This keeps the first character of every wrapped line at the
|
|
544
|
+
// same vertical gutter as the anchor, matching user intent.
|
|
545
|
+
const autoWidth = el.placement.size?.width === undefined || el.placement.size?.width === 'auto';
|
|
546
|
+
const effectiveAlign = anchoredToElement && autoWidth
|
|
547
|
+
? (pin.pinX === 'start' ? 'left' : pin.pinX === 'end' ? 'right' : el.align)
|
|
548
|
+
: el.align;
|
|
549
|
+
return {
|
|
550
|
+
kind: 'text',
|
|
551
|
+
id: el.id,
|
|
552
|
+
x,
|
|
553
|
+
y,
|
|
554
|
+
width: elementWidth,
|
|
555
|
+
height: elementHeight,
|
|
556
|
+
lines: m.lines,
|
|
557
|
+
fontString,
|
|
558
|
+
fontSizePx,
|
|
559
|
+
color: colorHex(el.color),
|
|
560
|
+
align: effectiveAlign,
|
|
561
|
+
verticalAlign: el.verticalAlign,
|
|
562
|
+
needsClip: m.needsClip || el.overflow === 'clip',
|
|
563
|
+
contentX: padding.left,
|
|
564
|
+
contentY: padding.top,
|
|
565
|
+
contentWidth: Math.max(0, elementWidth - padding.left - padding.right),
|
|
566
|
+
contentHeight: Math.max(0, elementHeight - padding.top - padding.bottom),
|
|
567
|
+
box,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function layoutRuleElement(el, pin, container, dpi) {
|
|
571
|
+
const thicknessPx = dimPx(el.thickness, dpi);
|
|
572
|
+
const widthSize = resolveFixedSize(el.placement.size?.width, dpi);
|
|
573
|
+
const heightSize = resolveFixedSize(el.placement.size?.height, dpi);
|
|
574
|
+
let elementWidth;
|
|
575
|
+
let elementHeight;
|
|
576
|
+
if (el.direction === 'horizontal') {
|
|
577
|
+
if (widthSize === undefined || widthSize === 'auto' || widthSize === 'fill') {
|
|
578
|
+
elementWidth = Math.max(0, fillToContainerEdge(pin.anchorX, pin.pinX, container));
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
elementWidth = widthSize;
|
|
582
|
+
}
|
|
583
|
+
elementHeight = typeof heightSize === 'number' ? heightSize : thicknessPx;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
if (heightSize === undefined || heightSize === 'auto' || heightSize === 'fill') {
|
|
587
|
+
elementHeight = Math.max(0, fillToContainerEdgeY(pin.anchorY, pin.pinY, container));
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
elementHeight = heightSize;
|
|
591
|
+
}
|
|
592
|
+
elementWidth = typeof widthSize === 'number' ? widthSize : thicknessPx;
|
|
593
|
+
}
|
|
594
|
+
const x = edgeXFromPin(pin.anchorX, pin.pinX, elementWidth);
|
|
595
|
+
const y = edgeYFromPin(pin.anchorY, pin.pinY, elementHeight);
|
|
596
|
+
return {
|
|
597
|
+
kind: 'rule',
|
|
598
|
+
id: el.id,
|
|
599
|
+
direction: el.direction,
|
|
600
|
+
x,
|
|
601
|
+
y,
|
|
602
|
+
width: elementWidth,
|
|
603
|
+
height: elementHeight,
|
|
604
|
+
color: colorHex(el.color),
|
|
605
|
+
thicknessPx,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function layoutBoxElement(el, pin, container, dpi) {
|
|
609
|
+
const widthSize = resolveFixedSize(el.placement.size?.width, dpi);
|
|
610
|
+
const heightSize = resolveFixedSize(el.placement.size?.height, dpi);
|
|
611
|
+
let elementWidth;
|
|
612
|
+
let elementHeight;
|
|
613
|
+
if (widthSize === undefined || widthSize === 'auto' || widthSize === 'fill') {
|
|
614
|
+
elementWidth = Math.max(0, fillToContainerEdge(pin.anchorX, pin.pinX, container));
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
elementWidth = widthSize;
|
|
618
|
+
}
|
|
619
|
+
if (heightSize === undefined || heightSize === 'auto' || heightSize === 'fill') {
|
|
620
|
+
elementHeight = Math.max(0, fillToContainerEdgeY(pin.anchorY, pin.pinY, container));
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
elementHeight = heightSize;
|
|
624
|
+
}
|
|
625
|
+
const x = edgeXFromPin(pin.anchorX, pin.pinX, elementWidth);
|
|
626
|
+
const y = edgeYFromPin(pin.anchorY, pin.pinY, elementHeight);
|
|
627
|
+
const box = resolveBox(el.style, dpi) ?? {
|
|
628
|
+
borderWidthPx: 0,
|
|
629
|
+
borderRadiusPx: 0,
|
|
630
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
kind: 'box',
|
|
634
|
+
id: el.id,
|
|
635
|
+
x,
|
|
636
|
+
y,
|
|
637
|
+
width: elementWidth,
|
|
638
|
+
height: elementHeight,
|
|
639
|
+
box,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
//# sourceMappingURL=layout.js.map
|