postext 0.3.42 → 0.3.44
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/README.md +1 -1
- package/dist/__tests__/createLayout.test.js +5 -65
- package/dist/__tests__/createLayout.test.js.map +1 -1
- package/dist/__tests__/defaults/calloutStyles.test.js +3 -0
- package/dist/__tests__/defaults/calloutStyles.test.js.map +1 -1
- package/dist/__tests__/parse/containers.test.js +1 -1
- package/dist/__tests__/parse/containers.test.js.map +1 -1
- package/dist/__tests__/parse/scripts.test.d.ts +2 -0
- package/dist/__tests__/parse/scripts.test.d.ts.map +1 -0
- package/dist/__tests__/parse/scripts.test.js +68 -0
- package/dist/__tests__/parse/scripts.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.js +163 -0
- package/dist/__tests__/pipeline/calloutColumnsLabel.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutFloat.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutFloat.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutFloat.test.js +138 -0
- package/dist/__tests__/pipeline/calloutFloat.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutPlacement.test.js +10 -6
- package/dist/__tests__/pipeline/calloutPlacement.test.js.map +1 -1
- package/dist/__tests__/pipeline/floatPlacement.test.js +3 -3
- package/dist/__tests__/pipeline/floatPlacement.test.js.map +1 -1
- package/dist/__tests__/pipeline/sideColumn.test.d.ts +2 -0
- package/dist/__tests__/pipeline/sideColumn.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/sideColumn.test.js +130 -0
- package/dist/__tests__/pipeline/sideColumn.test.js.map +1 -0
- package/dist/__tests__/pipeline/spanBlocks.test.js +11 -3
- package/dist/__tests__/pipeline/spanBlocks.test.js.map +1 -1
- package/dist/canvas-backend/blockRender.js +2 -2
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +2 -2
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/columnRule.d.ts.map +1 -1
- package/dist/columnRule.js +3 -0
- package/dist/columnRule.js.map +1 -1
- package/dist/defaults/calloutStyles.d.ts +23 -0
- package/dist/defaults/calloutStyles.d.ts.map +1 -1
- package/dist/defaults/calloutStyles.js +68 -0
- package/dist/defaults/calloutStyles.js.map +1 -1
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +2 -0
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +7 -1
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/layout.d.ts.map +1 -1
- package/dist/defaults/layout.js +12 -0
- package/dist/defaults/layout.js.map +1 -1
- package/dist/design/layout.d.ts +3 -0
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +149 -5
- package/dist/design/layout.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +8 -4
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +3 -0
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +1 -0
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/measure/font.d.ts +9 -0
- package/dist/measure/font.d.ts.map +1 -1
- package/dist/measure/font.js +17 -1
- package/dist/measure/font.js.map +1 -1
- package/dist/measure/rich.d.ts +17 -0
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +45 -4
- package/dist/measure/rich.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +1 -1
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/inlineFormatting.d.ts +5 -0
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +29 -4
- package/dist/parse/inlineFormatting.js.map +1 -1
- package/dist/parse/types.d.ts +4 -1
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +452 -85
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +4 -0
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +390 -131
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/columnBalancing.js +1 -1
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/config.d.ts +16 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +35 -2
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +21 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +12 -3
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/floatSlots.d.ts +17 -0
- package/dist/pipeline/floatSlots.d.ts.map +1 -1
- package/dist/pipeline/floatSlots.js +27 -3
- package/dist/pipeline/floatSlots.js.map +1 -1
- package/dist/pipeline/headerFooter.js +1 -1
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +13 -3
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/placement.d.ts +12 -1
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +50 -7
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +17 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +45 -11
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/types.d.ts +174 -62
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +11 -2
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/package.json +1 -1
|
@@ -97,6 +97,7 @@ export function deriveCalloutResolvedConfig(resolved, style) {
|
|
|
97
97
|
fontSize: body.fontSize,
|
|
98
98
|
lineHeight: body.lineHeight,
|
|
99
99
|
color: body.color,
|
|
100
|
+
boldColor: body.boldColor ?? body.color,
|
|
100
101
|
textAlign: body.textAlign,
|
|
101
102
|
hyphenation: { ...resolved.bodyText.hyphenation, enabled: body.hyphenation },
|
|
102
103
|
paragraphSpacing: body.paragraphSpacing,
|
|
@@ -113,6 +114,10 @@ export function deriveCalloutResolvedConfig(resolved, style) {
|
|
|
113
114
|
...l,
|
|
114
115
|
bulletChar: bulletOverride ? lists.bulletChar : l.bulletChar,
|
|
115
116
|
color: colorOverride ? lists.color : l.color,
|
|
117
|
+
// A bullet size / weight of its own sets the glyph in the box's body
|
|
118
|
+
// face at that size (the document's list face otherwise).
|
|
119
|
+
...(lists.bulletFontSize ? { fontSize: lists.bulletFontSize, fontFamily: body.fontFamily } : {}),
|
|
120
|
+
...(lists.bulletFontWeight !== undefined ? { fontWeight: lists.bulletFontWeight } : {}),
|
|
116
121
|
})),
|
|
117
122
|
},
|
|
118
123
|
orderedLists: {
|
|
@@ -141,7 +146,7 @@ export function deriveCalloutMeasureContext(ctx, style) {
|
|
|
141
146
|
orderedLevelIndentsPx: computeOrderedLevelIndentsPx(resolved, bodyStyle.fontSizePx, ctx.orderedMetrics.maxWidthByDepth),
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
|
-
const VALID_SPANS = new Set(['column', 'page']);
|
|
149
|
+
const VALID_SPANS = new Set(['column', 'page', 'side']);
|
|
145
150
|
const VALID_PLACEMENTS = new Set(['here', 'top', 'bottom', 'fixed']);
|
|
146
151
|
/** Per-instance span / placement: fence attribute when valid, else the style. */
|
|
147
152
|
export function resolveCalloutAttrs(style, attrs) {
|
|
@@ -152,7 +157,8 @@ export function resolveCalloutAttrs(style, attrs) {
|
|
|
152
157
|
? attrs.placement
|
|
153
158
|
: style.placement;
|
|
154
159
|
const title = attrs.title !== undefined ? attrs.title : style.title;
|
|
155
|
-
|
|
160
|
+
const label = attrs.label ?? '';
|
|
161
|
+
return { span, placement, title, label };
|
|
156
162
|
}
|
|
157
163
|
/** Shift a resolved resource block's caption/table geometry by `(ox, oy)`. */
|
|
158
164
|
function offsetResourceBlock(rb, ox, oy) {
|
|
@@ -242,7 +248,7 @@ export function layoutCallout(input) {
|
|
|
242
248
|
const bodyStyle = derivedCtx.bodyStyle;
|
|
243
249
|
const em = bodyStyle.fontSizePx;
|
|
244
250
|
const px = (d) => dimensionToPx(d, dpi, em);
|
|
245
|
-
const { span, placement, title: rawTitle } = resolveCalloutAttrs(style, attrs);
|
|
251
|
+
const { span, placement, title: rawTitle, label: labelText } = resolveCalloutAttrs(style, attrs);
|
|
246
252
|
const isAuto = style.width === 'auto';
|
|
247
253
|
// --- Marker column (outside the box, on its left) --------------------------
|
|
248
254
|
const hasMarker = iconPresent(style.marker);
|
|
@@ -266,6 +272,7 @@ export function layoutCallout(input) {
|
|
|
266
272
|
const topStripe = stripeOn && style.stripe.side === 'top';
|
|
267
273
|
const hasIcon = !input.continuation && iconPresent(style.icon);
|
|
268
274
|
const iconSize = hasIcon ? px(style.icon.size) : 0;
|
|
275
|
+
const iconBoxW = hasIcon && style.icon.width ? Math.max(iconSize, px(style.icon.width)) : iconSize;
|
|
269
276
|
const gapPx = px(style.titleStyle.gap);
|
|
270
277
|
// The icon takes its own column only when there is no side stripe to sit
|
|
271
278
|
// over. A continuation that opens inside a paragraph (`lineFrom`) keeps
|
|
@@ -273,8 +280,9 @@ export function layoutCallout(input) {
|
|
|
273
280
|
// so the text must wrap the same way — line `lineFrom` on, nothing lost
|
|
274
281
|
// or doubled.
|
|
275
282
|
const openingMidRun = !!input.continuation && (input.lineFrom ?? 0) > 0;
|
|
276
|
-
const
|
|
277
|
-
const
|
|
283
|
+
const cornerIcon = style.icon.position === 'corner';
|
|
284
|
+
const iconColumnKept = (hasIcon || (openingMidRun && iconPresent(style.icon))) && !sideStripe && !cornerIcon;
|
|
285
|
+
const iconColumn = iconColumnKept ? iconBoxW + gapPx : 0;
|
|
278
286
|
const innerX = (stripeLeft ? stripeW : 0) + padL + iconColumn;
|
|
279
287
|
const innerTop = (topStripe ? stripeW : 0) + padT;
|
|
280
288
|
// --- Title -----------------------------------------------------------------
|
|
@@ -284,13 +292,16 @@ export function layoutCallout(input) {
|
|
|
284
292
|
const titleFontPx = dimensionToPx(style.titleStyle.fontSize, dpi, em);
|
|
285
293
|
const titleFont = buildFontString(style.titleStyle.fontFamily, titleFontPx, style.titleStyle.fontWeight.toString(), style.titleStyle.italic ? 'italic' : 'normal');
|
|
286
294
|
const titleLineHeight = titleFontPx * 1.2;
|
|
295
|
+
const titleTrackingPx = Math.max(0, dimensionToPx(style.titleStyle.letterSpacing, dpi, titleFontPx));
|
|
296
|
+
const titleIndentPx = Math.max(0, dimensionToPx(style.titleStyle.indent, dpi, titleFontPx));
|
|
287
297
|
const hasTitle = !input.continuation && titleText.trim().length > 0;
|
|
298
|
+
const titleWidthOf = (t) => measureTextWidth(t, titleFont) + titleTrackingPx * t.length;
|
|
288
299
|
// Box width: `fill` uses the given width less the marker column; `auto`
|
|
289
300
|
// shrink-wraps the title.
|
|
290
301
|
let boxWidth = input.width - markerColumn;
|
|
291
302
|
let innerWidth;
|
|
292
303
|
if (isAuto) {
|
|
293
|
-
const titleW = hasTitle ?
|
|
304
|
+
const titleW = hasTitle ? titleWidthOf(titleText) + titleIndentPx : 0;
|
|
294
305
|
innerWidth = Math.max(1, titleW);
|
|
295
306
|
boxWidth = innerX + innerWidth + padR + (stripeRight ? stripeW : 0);
|
|
296
307
|
}
|
|
@@ -301,14 +312,18 @@ export function layoutCallout(input) {
|
|
|
301
312
|
const overlayBlocks = [];
|
|
302
313
|
let titleBlock;
|
|
303
314
|
if (hasTitle) {
|
|
304
|
-
const
|
|
315
|
+
const titleW = Math.max(1, innerWidth - titleIndentPx);
|
|
316
|
+
const measured = measureBlock(titleText, titleFont, titleW, titleLineHeight, {
|
|
317
|
+
textAlign: 'left',
|
|
318
|
+
...(titleTrackingPx > 0 ? { letterSpacingPx: titleTrackingPx } : {}),
|
|
319
|
+
});
|
|
305
320
|
const lines = measured.lines.length > 0
|
|
306
321
|
? measured.lines
|
|
307
|
-
: [{ text: titleText, bbox: createBoundingBox(0, 0,
|
|
322
|
+
: [{ text: titleText, bbox: createBoundingBox(0, 0, titleW, titleLineHeight), baseline: titleLineHeight * 0.8, hyphenated: false }];
|
|
308
323
|
const titleHeight = lines.length * titleLineHeight;
|
|
309
324
|
titleBlock = {
|
|
310
325
|
kind: 'text',
|
|
311
|
-
bbox: createBoundingBox(innerX, cursorY,
|
|
326
|
+
bbox: createBoundingBox(innerX + titleIndentPx, cursorY, titleW, titleHeight),
|
|
312
327
|
fontString: titleFont,
|
|
313
328
|
color: style.titleStyle.color.hex,
|
|
314
329
|
lines: lines.map((ln, i) => ({
|
|
@@ -318,137 +333,286 @@ export function layoutCallout(input) {
|
|
|
318
333
|
width: ln.bbox.width,
|
|
319
334
|
})),
|
|
320
335
|
clip: false,
|
|
336
|
+
...(titleTrackingPx > 0 ? { letterSpacingPx: titleTrackingPx } : {}),
|
|
321
337
|
};
|
|
322
338
|
cursorY += titleHeight;
|
|
323
339
|
}
|
|
324
340
|
// --- Children --------------------------------------------------------------
|
|
341
|
+
// The children stack under the title; a `:::columns{count=N}` group among
|
|
342
|
+
// them lays its own children out in N columns of equal width, cut at
|
|
343
|
+
// the line boundaries that level the columns best, and takes the height
|
|
344
|
+
// of the tallest column.
|
|
325
345
|
const childBlocks = [];
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
346
|
+
const columnGapPx = px(style.columnGap);
|
|
347
|
+
const isFirstRealOf = (k) => children.slice(0, k).every((c) => c.type === 'directive' || isMarkerBlock(c));
|
|
348
|
+
const isLastRealOf = (k) => children.slice(k + 1).every((c) => c.type === 'directive' || isMarkerBlock(c));
|
|
349
|
+
/** Lay out one child at `width` from `x`, appending it to `into` and
|
|
350
|
+
* advancing the stack. */
|
|
351
|
+
const placeChild = (raw, k, width, x, st, into) => {
|
|
352
|
+
const blockIdx = childStartIdx + k;
|
|
353
|
+
const measuredBlock = measureContentBlock(raw, blockIdx, width, derivedCtx, {
|
|
354
|
+
styleOverride: input.paragraphStyleFor?.(blockIdx),
|
|
355
|
+
});
|
|
356
|
+
if (!measuredBlock)
|
|
357
|
+
return undefined;
|
|
358
|
+
const { kind, measured, prefixLen, absoluteSourceMap, mathDisplayRender, resourceBlock } = measuredBlock;
|
|
359
|
+
const { style: bs, vdtType, headingLevel, numberPrefix, listBullet, listDepth, listKind, bulletXOffsetInColumn, strikethroughText } = kind;
|
|
360
|
+
// Spacing above: margin collapsing, list-item spacing inside a run.
|
|
361
|
+
let spacing;
|
|
362
|
+
if (st.first) {
|
|
363
|
+
spacing = st.prevMarginBottom;
|
|
364
|
+
}
|
|
365
|
+
else if (vdtType === 'listItem' && st.prevWasListItem) {
|
|
366
|
+
spacing = listBullet ? listBullet.itemSpacingPx : 0;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
spacing = Math.max(st.prevMarginBottom, bs.marginTopPx);
|
|
370
|
+
}
|
|
371
|
+
st.cursorY += spacing;
|
|
372
|
+
const blk = createVDTBlock(input.nextChildId(), vdtType, bs.fontString, bs.color, bs.textAlign);
|
|
373
|
+
applyStyleAttrs(blk, bs);
|
|
374
|
+
blk.contentIndex = blockIdx;
|
|
375
|
+
blk.containerId = containerId;
|
|
376
|
+
blk.dirty = false;
|
|
377
|
+
blk.snappedToGrid = false;
|
|
378
|
+
blk.headingLevel = headingLevel;
|
|
379
|
+
if (numberPrefix)
|
|
380
|
+
blk.numberPrefix = numberPrefix;
|
|
381
|
+
blk.sourceMap = absoluteSourceMap;
|
|
382
|
+
blk.plainPrefixLen = prefixLen;
|
|
383
|
+
// Line-level cut of the first / last child (split boxes): the kept
|
|
384
|
+
// run of lines, and whether this child opens after its first line
|
|
385
|
+
// (no bullet then — the head carries it).
|
|
386
|
+
const isTextRun = !(vdtType === 'resource' && resourceBlock) && !(vdtType === 'mathDisplay' && mathDisplayRender);
|
|
387
|
+
const isFirstReal = isFirstRealOf(k);
|
|
388
|
+
const isLastReal = isLastRealOf(k);
|
|
389
|
+
const lineFrom = isTextRun && isFirstReal ? Math.max(0, input.lineFrom ?? 0) : 0;
|
|
390
|
+
const lineTo = isTextRun && isLastReal && input.lineTo !== undefined
|
|
391
|
+
? Math.max(lineFrom + 1, Math.min(input.lineTo, measured.lines.length))
|
|
392
|
+
: measured.lines.length;
|
|
393
|
+
const keptLines = isTextRun ? measured.lines.slice(Math.min(lineFrom, measured.lines.length - 1), lineTo) : measured.lines;
|
|
394
|
+
const openedMidRun = lineFrom > 0;
|
|
395
|
+
let height;
|
|
396
|
+
if (vdtType === 'resource' && resourceBlock) {
|
|
397
|
+
height = measured.totalHeight;
|
|
398
|
+
blk.resourceBlock = resourceBlock;
|
|
399
|
+
blk.lines = [{
|
|
400
|
+
text: '',
|
|
401
|
+
bbox: createBoundingBox(0, 0, resourceBlock.bodyRect.width, height),
|
|
402
|
+
baseline: 0,
|
|
403
|
+
hyphenated: false,
|
|
404
|
+
segments: [],
|
|
405
|
+
isLastLine: true,
|
|
406
|
+
}];
|
|
407
|
+
blk.sourceStart = raw.sourceStart + (input.bodyOffset ?? ctx.bodyOffset);
|
|
408
|
+
blk.sourceEnd = raw.sourceEnd + (input.bodyOffset ?? ctx.bodyOffset);
|
|
409
|
+
}
|
|
410
|
+
else if (vdtType === 'mathDisplay' && mathDisplayRender) {
|
|
411
|
+
blk.mathRender = mathDisplayRender;
|
|
412
|
+
blk.tex = raw.tex;
|
|
413
|
+
const mathLine = { ...measured.lines[0], bbox: { ...measured.lines[0].bbox, y: 0 } };
|
|
414
|
+
blk.lines = [mathLine];
|
|
415
|
+
height = mathLine.bbox.height;
|
|
416
|
+
blk.sourceStart = raw.sourceStart + ctx.bodyOffset;
|
|
417
|
+
blk.sourceEnd = raw.sourceEnd + ctx.bodyOffset;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
blk.lines = resetLinePositions(keptLines, bs.lineHeightPx);
|
|
421
|
+
height = blk.lines.length * bs.lineHeightPx;
|
|
422
|
+
blk.sourceStart = keptLines[0].sourceStart ?? raw.sourceStart + ctx.bodyOffset;
|
|
423
|
+
blk.sourceEnd = keptLines[keptLines.length - 1].sourceEnd ?? raw.sourceEnd + ctx.bodyOffset;
|
|
424
|
+
}
|
|
425
|
+
// Relocate to the inner rect (box-relative).
|
|
426
|
+
blk.bbox = createBoundingBox(x, st.cursorY, width, height);
|
|
427
|
+
for (const line of blk.lines) {
|
|
428
|
+
line.bbox.x += x;
|
|
429
|
+
line.bbox.y += st.cursorY;
|
|
430
|
+
line.baseline += st.cursorY;
|
|
431
|
+
}
|
|
432
|
+
if (blk.resourceBlock)
|
|
433
|
+
offsetResourceBlock(blk.resourceBlock, x, st.cursorY);
|
|
434
|
+
if (listBullet && !openedMidRun) {
|
|
435
|
+
blk.listDepth = listDepth;
|
|
436
|
+
blk.listKind = listKind;
|
|
437
|
+
blk.bulletText = listBullet.bulletText;
|
|
438
|
+
blk.bulletFontString = listBullet.bulletFontString;
|
|
439
|
+
blk.bulletColor = listBullet.bulletColor;
|
|
440
|
+
blk.bulletOffsetX = x + bulletXOffsetInColumn;
|
|
441
|
+
if (listBullet.separatorText !== undefined) {
|
|
442
|
+
blk.separatorText = listBullet.separatorText;
|
|
443
|
+
blk.separatorFontString = listBullet.separatorFontString;
|
|
444
|
+
blk.separatorColor = listBullet.separatorColor;
|
|
445
|
+
blk.separatorX = blk.bulletOffsetX + (listBullet.separatorOffsetPx ?? 0);
|
|
446
|
+
}
|
|
447
|
+
if (strikethroughText)
|
|
448
|
+
blk.strikethroughText = true;
|
|
449
|
+
const firstLine = blk.lines[0];
|
|
450
|
+
if (firstLine) {
|
|
451
|
+
blk.bulletY = firstLine.baseline - listBullet.textFontSizePx * 0.3 + listBullet.verticalOffsetPx;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else if (listBullet) {
|
|
455
|
+
blk.listDepth = listDepth;
|
|
456
|
+
blk.listKind = listKind;
|
|
457
|
+
}
|
|
458
|
+
into.push(blk);
|
|
459
|
+
st.cursorY += height;
|
|
460
|
+
st.prevMarginBottom = bs.marginBottomPx;
|
|
461
|
+
st.prevWasListItem = vdtType === 'listItem';
|
|
462
|
+
st.first = false;
|
|
463
|
+
return blk;
|
|
464
|
+
};
|
|
465
|
+
/** The tail of a text block cut at line `l`: a block of its own with the
|
|
466
|
+
* lines from `l` on (no bullet — the head keeps it), at y = 0. */
|
|
467
|
+
const tailOf = (blk, l) => {
|
|
468
|
+
const lh = blk.lines[0]?.bbox.height ?? 0;
|
|
469
|
+
const tail = { ...blk, id: input.nextChildId(), lines: resetLinePositions(blk.lines.slice(l), lh) };
|
|
470
|
+
delete tail.bulletText;
|
|
471
|
+
delete tail.bulletFontString;
|
|
472
|
+
delete tail.bulletColor;
|
|
473
|
+
delete tail.bulletOffsetX;
|
|
474
|
+
delete tail.bulletY;
|
|
475
|
+
delete tail.separatorText;
|
|
476
|
+
delete tail.separatorFontString;
|
|
477
|
+
delete tail.separatorColor;
|
|
478
|
+
delete tail.separatorX;
|
|
479
|
+
tail.bbox = createBoundingBox(blk.bbox.x, 0, blk.bbox.width, tail.lines.length * lh);
|
|
480
|
+
tail.sourceStart = tail.lines[0]?.sourceStart ?? blk.sourceStart;
|
|
481
|
+
return tail;
|
|
482
|
+
};
|
|
483
|
+
/** A `:::columns{count=N}` group: children `k0 … k1 - 1` in N columns. */
|
|
484
|
+
const placeColumnsGroup = (k0, k1, count, st, breaks) => {
|
|
485
|
+
const cols = Math.max(1, Math.min(6, count));
|
|
486
|
+
const colW = Math.max(1, (innerWidth - columnGapPx * (cols - 1)) / cols);
|
|
487
|
+
// Spacing above the group: as a block with no margin of its own.
|
|
488
|
+
const spacing = st.first ? st.prevMarginBottom : st.prevMarginBottom;
|
|
489
|
+
const groupTop = st.cursorY + spacing;
|
|
490
|
+
const gst = { cursorY: 0, prevMarginBottom: 0, prevWasListItem: false, first: true };
|
|
491
|
+
const stack = [];
|
|
492
|
+
for (let k = k0; k < k1; k++) {
|
|
331
493
|
const raw = children[k];
|
|
332
|
-
const blockIdx = childStartIdx + k;
|
|
333
494
|
if (raw.type === 'directive' || isMarkerBlock(raw))
|
|
334
495
|
continue;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
spacing = listBullet ? listBullet.itemSpacingPx : 0;
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
spacing = Math.max(prevMarginBottom, bs.marginTopPx);
|
|
496
|
+
placeChild(raw, k, colW, innerX, gst, stack);
|
|
497
|
+
}
|
|
498
|
+
if (stack.length === 0)
|
|
499
|
+
return;
|
|
500
|
+
const total = gst.cursorY;
|
|
501
|
+
const candidates = [];
|
|
502
|
+
stack.forEach((blk, i) => {
|
|
503
|
+
if (i > 0)
|
|
504
|
+
candidates.push({ y: blk.bbox.y, block: i, line: 0 });
|
|
505
|
+
const cuttable = blk.type !== 'resource' && blk.type !== 'mathDisplay' && blk.lines.length > 1 && !blk.mathRender;
|
|
506
|
+
if (cuttable) {
|
|
507
|
+
for (let l = 1; l < blk.lines.length; l++)
|
|
508
|
+
candidates.push({ y: blk.lines[l].bbox.y, block: i, line: l });
|
|
352
509
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const isTextRun = !(vdtType === 'resource' && resourceBlock) && !(vdtType === 'mathDisplay' && mathDisplayRender);
|
|
369
|
-
const isFirstReal = childBlocks.length === 0;
|
|
370
|
-
const isLastReal = children.slice(k + 1).every((c) => c.type === 'directive' || isMarkerBlock(c));
|
|
371
|
-
const lineFrom = isTextRun && isFirstReal ? Math.max(0, input.lineFrom ?? 0) : 0;
|
|
372
|
-
const lineTo = isTextRun && isLastReal && input.lineTo !== undefined
|
|
373
|
-
? Math.max(lineFrom + 1, Math.min(input.lineTo, measured.lines.length))
|
|
374
|
-
: measured.lines.length;
|
|
375
|
-
const keptLines = isTextRun ? measured.lines.slice(Math.min(lineFrom, measured.lines.length - 1), lineTo) : measured.lines;
|
|
376
|
-
const openedMidRun = lineFrom > 0;
|
|
377
|
-
let height;
|
|
378
|
-
if (vdtType === 'resource' && resourceBlock) {
|
|
379
|
-
height = measured.totalHeight;
|
|
380
|
-
blk.resourceBlock = resourceBlock;
|
|
381
|
-
blk.lines = [{
|
|
382
|
-
text: '',
|
|
383
|
-
bbox: createBoundingBox(0, 0, resourceBlock.bodyRect.width, height),
|
|
384
|
-
baseline: 0,
|
|
385
|
-
hyphenated: false,
|
|
386
|
-
segments: [],
|
|
387
|
-
isLastLine: true,
|
|
388
|
-
}];
|
|
389
|
-
blk.sourceStart = raw.sourceStart + (input.bodyOffset ?? ctx.bodyOffset);
|
|
390
|
-
blk.sourceEnd = raw.sourceEnd + (input.bodyOffset ?? ctx.bodyOffset);
|
|
510
|
+
});
|
|
511
|
+
const cuts = [];
|
|
512
|
+
let prevY = 0;
|
|
513
|
+
if (breaks && breaks.length > 0) {
|
|
514
|
+
// Explicit column starts (`breaks="3"`: the third child opens the
|
|
515
|
+
// second column): block-level cuts, no balancing.
|
|
516
|
+
for (const b of breaks.slice(0, cols - 1)) {
|
|
517
|
+
const i = b - 1;
|
|
518
|
+
if (i <= 0 || i >= stack.length)
|
|
519
|
+
continue;
|
|
520
|
+
const y = stack[i].bbox.y;
|
|
521
|
+
if (y <= prevY + 0.01)
|
|
522
|
+
continue;
|
|
523
|
+
cuts.push({ y, block: i, line: 0 });
|
|
524
|
+
prevY = y;
|
|
391
525
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
for (let c = 1; c < cols; c++) {
|
|
529
|
+
const target = (total * c) / cols;
|
|
530
|
+
let best;
|
|
531
|
+
for (const cand of candidates) {
|
|
532
|
+
if (cand.y <= prevY + 0.01)
|
|
533
|
+
continue;
|
|
534
|
+
if (!best || Math.abs(cand.y - target) < Math.abs(best.y - target))
|
|
535
|
+
best = cand;
|
|
536
|
+
}
|
|
537
|
+
if (!best)
|
|
538
|
+
break;
|
|
539
|
+
cuts.push(best);
|
|
540
|
+
prevY = best.y;
|
|
400
541
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
542
|
+
}
|
|
543
|
+
// Split the stack at the cuts into columns of blocks; a cut inside a
|
|
544
|
+
// text block leaves its head behind and opens the next column with the
|
|
545
|
+
// tail (a block of its own, bullet-less).
|
|
546
|
+
const columns = [[]];
|
|
547
|
+
const columnStarts = [0];
|
|
548
|
+
let cutIdx = 0;
|
|
549
|
+
for (let i = 0; i < stack.length; i++) {
|
|
550
|
+
let blk = stack[i];
|
|
551
|
+
while (cutIdx < cuts.length && cuts[cutIdx].block === i && cuts[cutIdx].line === 0) {
|
|
552
|
+
columns.push([]);
|
|
553
|
+
columnStarts.push(cuts[cutIdx].y);
|
|
554
|
+
cutIdx++;
|
|
406
555
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
line
|
|
411
|
-
|
|
412
|
-
|
|
556
|
+
let consumed = 0;
|
|
557
|
+
while (cutIdx < cuts.length && cuts[cutIdx].block === i && cuts[cutIdx].line > 0) {
|
|
558
|
+
const cut = cuts[cutIdx];
|
|
559
|
+
const l = cut.line - consumed;
|
|
560
|
+
cutIdx++;
|
|
561
|
+
if (l <= 0 || l >= blk.lines.length)
|
|
562
|
+
continue;
|
|
563
|
+
const lh = blk.lines[0]?.bbox.height ?? 0;
|
|
564
|
+
const tail = tailOf(blk, l);
|
|
565
|
+
blk.lines = blk.lines.slice(0, l);
|
|
566
|
+
blk.bbox = createBoundingBox(blk.bbox.x, blk.bbox.y, blk.bbox.width, l * lh);
|
|
567
|
+
columns[columns.length - 1].push(blk);
|
|
568
|
+
offsetBlock(tail, 0, cut.y);
|
|
569
|
+
columns.push([]);
|
|
570
|
+
columnStarts.push(cut.y);
|
|
571
|
+
consumed += l;
|
|
572
|
+
blk = tail;
|
|
413
573
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
blk
|
|
422
|
-
blk
|
|
423
|
-
|
|
424
|
-
blk.separatorText = listBullet.separatorText;
|
|
425
|
-
blk.separatorFontString = listBullet.separatorFontString;
|
|
426
|
-
blk.separatorColor = listBullet.separatorColor;
|
|
427
|
-
blk.separatorX = blk.bulletOffsetX + (listBullet.separatorOffsetPx ?? 0);
|
|
428
|
-
}
|
|
429
|
-
if (strikethroughText)
|
|
430
|
-
blk.strikethroughText = true;
|
|
431
|
-
const firstLine = blk.lines[0];
|
|
432
|
-
if (firstLine) {
|
|
433
|
-
blk.bulletY = firstLine.baseline - listBullet.textFontSizePx * 0.3 + listBullet.verticalOffsetPx;
|
|
434
|
-
}
|
|
574
|
+
columns[columns.length - 1].push(blk);
|
|
575
|
+
}
|
|
576
|
+
let groupHeight = 0;
|
|
577
|
+
columns.forEach((blocks, c) => {
|
|
578
|
+
const shiftX = c * (colW + columnGapPx);
|
|
579
|
+
const shiftY = groupTop - (columnStarts[c] ?? 0);
|
|
580
|
+
for (const blk of blocks) {
|
|
581
|
+
offsetBlock(blk, shiftX, shiftY);
|
|
582
|
+
childBlocks.push(blk);
|
|
583
|
+
groupHeight = Math.max(groupHeight, blk.bbox.y + blk.bbox.height - groupTop);
|
|
435
584
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
585
|
+
});
|
|
586
|
+
st.cursorY = groupTop + groupHeight;
|
|
587
|
+
st.prevMarginBottom = 0;
|
|
588
|
+
st.prevWasListItem = false;
|
|
589
|
+
st.first = false;
|
|
590
|
+
};
|
|
591
|
+
const st = { cursorY, prevMarginBottom: hasTitle ? gapPx : 0, prevWasListItem: false, first: true };
|
|
592
|
+
if (!isAuto) {
|
|
593
|
+
for (let k = 0; k < children.length; k++) {
|
|
594
|
+
const raw = children[k];
|
|
595
|
+
if (raw.type === 'containerStart' && raw.containerName === 'columns') {
|
|
596
|
+
let e = k + 1;
|
|
597
|
+
while (e < children.length && !(children[e].type === 'containerEnd' && children[e].containerName === 'columns' && children[e].containerId === raw.containerId))
|
|
598
|
+
e++;
|
|
599
|
+
const count = Number.parseInt(raw.containerAttrs?.count ?? '2', 10);
|
|
600
|
+
const breaks = (raw.containerAttrs?.breaks ?? '').split(',').map((v) => Number.parseInt(v.trim(), 10)).filter((v) => Number.isFinite(v) && v > 1);
|
|
601
|
+
placeColumnsGroup(k + 1, e, Number.isFinite(count) ? count : 2, st, breaks.length > 0 ? breaks : undefined);
|
|
602
|
+
k = e;
|
|
603
|
+
continue;
|
|
439
604
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
prevWasListItem = vdtType === 'listItem';
|
|
444
|
-
first = false;
|
|
605
|
+
if (raw.type === 'directive' || isMarkerBlock(raw))
|
|
606
|
+
continue;
|
|
607
|
+
placeChild(raw, k, innerWidth, innerX, st, childBlocks);
|
|
445
608
|
}
|
|
446
609
|
}
|
|
610
|
+
cursorY = st.cursorY;
|
|
447
611
|
// An icon taller than the content grows the inner area to fit it; with
|
|
448
612
|
// `align: 'center'` the title and children are centred on the icon.
|
|
449
613
|
let contentBottom = cursorY;
|
|
450
614
|
const contentH = contentBottom - innerTop;
|
|
451
|
-
if (hasIcon && iconSize > contentH) {
|
|
615
|
+
if (hasIcon && !cornerIcon && iconSize > contentH) {
|
|
452
616
|
const extra = iconSize - contentH;
|
|
453
617
|
if (style.icon.align === 'center') {
|
|
454
618
|
if (titleBlock)
|
|
@@ -491,13 +655,22 @@ export function layoutCallout(input) {
|
|
|
491
655
|
if (hasIcon) {
|
|
492
656
|
// Over a side stripe the icon is centred on the stripe; otherwise it
|
|
493
657
|
// sits in its own column left of the content.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
658
|
+
// A corner badge hangs on the top-right corner, half of it past the
|
|
659
|
+
// border, flush with the top edge.
|
|
660
|
+
const cornerRight = style.icon.cornerSide === 'right'
|
|
661
|
+
|| (style.icon.cornerSide === 'outer' && !input.mirrored)
|
|
662
|
+
|| (style.icon.cornerSide === 'inner' && !!input.mirrored);
|
|
663
|
+
const iconX = cornerIcon
|
|
664
|
+
? (cornerRight ? boxWidth - iconSize / 2 : -iconSize / 2)
|
|
665
|
+
: sideStripe
|
|
666
|
+
? (stripeLeft ? (stripeW - iconSize) / 2 : boxWidth - stripeW + (stripeW - iconSize) / 2)
|
|
667
|
+
: innerX - iconColumn;
|
|
668
|
+
const iconY = cornerIcon
|
|
669
|
+
? 0
|
|
670
|
+
: style.icon.align === 'center'
|
|
671
|
+
? innerTop + (contentBottom - innerTop - iconSize) / 2
|
|
672
|
+
: innerTop;
|
|
673
|
+
const built = buildIconBlock(style.icon, iconX, iconY, iconSize, ctx, iconBoxW);
|
|
501
674
|
if (built) {
|
|
502
675
|
overlayBlocks.push(built.block);
|
|
503
676
|
iconFileId = built.fileId;
|
|
@@ -506,6 +679,72 @@ export function layoutCallout(input) {
|
|
|
506
679
|
}
|
|
507
680
|
if (titleBlock)
|
|
508
681
|
overlayBlocks.push(titleBlock);
|
|
682
|
+
// --- Label tab ---------------------------------------------------------------
|
|
683
|
+
// The fence's `label` on a tab hugging a top corner, rising `offset`
|
|
684
|
+
// above the box; an icon beside it and a rule along the top edge.
|
|
685
|
+
let labelRisePx = 0;
|
|
686
|
+
if (style.label && !input.continuation && labelText.trim().length > 0) {
|
|
687
|
+
const lb = style.label;
|
|
688
|
+
const lbFontPx = dimensionToPx(lb.fontSize, dpi, em);
|
|
689
|
+
const lbFont = buildFontString(lb.fontFamily, lbFontPx, lb.fontWeight.toString());
|
|
690
|
+
const padX = dimensionToPx(lb.paddingX, dpi, lbFontPx);
|
|
691
|
+
const tabH = dimensionToPx(lb.height, dpi, lbFontPx);
|
|
692
|
+
const rise = px(lb.offset);
|
|
693
|
+
const inset = px(lb.inset);
|
|
694
|
+
const textW = measureTextWidth(labelText, lbFont);
|
|
695
|
+
const tabW = textW + 2 * padX;
|
|
696
|
+
const onRight = lb.position === 'top-right';
|
|
697
|
+
const tabX = onRight ? boxWidth - inset - tabW : inset;
|
|
698
|
+
const tabY = -rise;
|
|
699
|
+
labelRisePx = Math.max(0, rise);
|
|
700
|
+
overlayBlocks.push({
|
|
701
|
+
kind: 'box',
|
|
702
|
+
bbox: createBoundingBox(tabX, tabY, tabW, tabH),
|
|
703
|
+
box: { backgroundColor: lb.background.hex, borderWidthPx: 0, borderRadiusPx: 0 },
|
|
704
|
+
});
|
|
705
|
+
overlayBlocks.push({
|
|
706
|
+
kind: 'text',
|
|
707
|
+
bbox: createBoundingBox(tabX, tabY, tabW, tabH),
|
|
708
|
+
fontString: lbFont,
|
|
709
|
+
color: lb.color.hex,
|
|
710
|
+
lines: [{ text: labelText, xOffset: padX, baselineY: tabY + tabH / 2 + lbFontPx * 0.36, width: textW }],
|
|
711
|
+
clip: false,
|
|
712
|
+
});
|
|
713
|
+
let edgeX = onRight ? tabX : tabX + tabW;
|
|
714
|
+
if (lb.icon.resourceId) {
|
|
715
|
+
const iconW = px(lb.icon.width);
|
|
716
|
+
const gap = px(lb.icon.gap);
|
|
717
|
+
const iconX = onRight ? tabX - gap - iconW : tabX + tabW + gap;
|
|
718
|
+
const resource = ctx.resourceById.get(lb.icon.resourceId);
|
|
719
|
+
const fileId = resource?.bitmap?.fileId ?? resource?.svg?.fileId;
|
|
720
|
+
if (fileId) {
|
|
721
|
+
const dims = resource?.bitmap ?? resource?.svg;
|
|
722
|
+
let w = iconW;
|
|
723
|
+
let h = tabH;
|
|
724
|
+
if (dims?.width && dims?.height) {
|
|
725
|
+
const k = Math.min(iconW / dims.width, tabH / dims.height);
|
|
726
|
+
w = dims.width * k;
|
|
727
|
+
h = dims.height * k;
|
|
728
|
+
}
|
|
729
|
+
overlayBlocks.push({ kind: 'image', bbox: createBoundingBox(onRight ? iconX + iconW - w : iconX, tabY + (tabH - h) / 2, w, h), fileId });
|
|
730
|
+
edgeX = onRight ? iconX + iconW - w : iconX + w;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (lb.rule.enabled) {
|
|
734
|
+
const ruleW = px(lb.rule.width);
|
|
735
|
+
const x0 = onRight ? 0 : edgeX;
|
|
736
|
+
const len = onRight ? edgeX : boxWidth - edgeX;
|
|
737
|
+
if (len > 0) {
|
|
738
|
+
overlayBlocks.push({
|
|
739
|
+
kind: 'rule',
|
|
740
|
+
bbox: createBoundingBox(x0, -ruleW / 2, len, ruleW),
|
|
741
|
+
color: lb.rule.color.hex,
|
|
742
|
+
thicknessPx: ruleW,
|
|
743
|
+
direction: 'horizontal',
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
509
748
|
// --- Marker column + frame -----------------------------------------------------
|
|
510
749
|
// The frame is as tall as the tallest of box, marker and rule; the three
|
|
511
750
|
// are centred on each other (`marker.align: 'center'`) or top-aligned.
|
|
@@ -546,13 +785,26 @@ export function layoutCallout(input) {
|
|
|
546
785
|
});
|
|
547
786
|
}
|
|
548
787
|
}
|
|
788
|
+
// The label tab rises above the box inside the block: the box starts
|
|
789
|
+
// `labelRisePx` down, so the tab keeps its room at a column head too,
|
|
790
|
+
// where the top margin is dropped.
|
|
791
|
+
if (labelRisePx > 0) {
|
|
792
|
+
for (const b of overlayBlocks)
|
|
793
|
+
offsetDesignBlock(b, 0, labelRisePx);
|
|
794
|
+
for (const b of markerBlocks)
|
|
795
|
+
offsetDesignBlock(b, 0, labelRisePx);
|
|
796
|
+
for (const blk of childBlocks)
|
|
797
|
+
offsetBlock(blk, 0, labelRisePx);
|
|
798
|
+
innerRect.y += labelRisePx;
|
|
799
|
+
}
|
|
800
|
+
const frameHeight = totalHeight + labelRisePx;
|
|
549
801
|
const overlay = {
|
|
550
|
-
bbox: createBoundingBox(0, 0, width,
|
|
802
|
+
bbox: createBoundingBox(0, 0, width, frameHeight),
|
|
551
803
|
blocks: [...markerBlocks, ...overlayBlocks],
|
|
552
804
|
};
|
|
553
805
|
// --- Frame block -------------------------------------------------------------
|
|
554
806
|
const frame = createVDTBlock(frameId, 'callout', bodyStyle.fontString, bodyStyle.color, bodyStyle.textAlign);
|
|
555
|
-
frame.bbox = createBoundingBox(0, 0, width,
|
|
807
|
+
frame.bbox = createBoundingBox(0, 0, width, frameHeight);
|
|
556
808
|
frame.lines = [];
|
|
557
809
|
frame.dirty = false;
|
|
558
810
|
frame.snappedToGrid = false;
|
|
@@ -573,7 +825,7 @@ export function layoutCallout(input) {
|
|
|
573
825
|
frame,
|
|
574
826
|
children: childBlocks,
|
|
575
827
|
width,
|
|
576
|
-
totalHeight,
|
|
828
|
+
totalHeight: frameHeight,
|
|
577
829
|
marginTopPx: px(style.marginTop),
|
|
578
830
|
marginBottomPx: px(style.marginBottom),
|
|
579
831
|
};
|
|
@@ -599,15 +851,22 @@ function fitInSquare(x, y, size, w, h) {
|
|
|
599
851
|
/** The design block for an icon spec drawn in the square `(x, y, size)`:
|
|
600
852
|
* an image block for a resource (aspect-fitted; `undefined` when the
|
|
601
853
|
* resource is missing), a centred text block for a glyph. */
|
|
602
|
-
function buildIconBlock(spec, x, y, size, ctx) {
|
|
854
|
+
function buildIconBlock(spec, x, y, size, ctx, boxWidth = size) {
|
|
603
855
|
if (spec.kind === 'resource') {
|
|
604
856
|
const resource = ctx.resourceById.get(spec.resourceId);
|
|
605
857
|
const fileId = resource?.bitmap?.fileId ?? resource?.svg?.fileId;
|
|
606
858
|
if (!fileId)
|
|
607
859
|
return undefined;
|
|
608
860
|
const dims = resource?.bitmap ?? resource?.svg;
|
|
861
|
+
let bbox = fitInSquare(x, y, size, dims?.width, dims?.height);
|
|
862
|
+
if (boxWidth > size && dims?.width && dims?.height) {
|
|
863
|
+
const k = Math.min(boxWidth / dims.width, size / dims.height);
|
|
864
|
+
const fw = dims.width * k;
|
|
865
|
+
const fh = dims.height * k;
|
|
866
|
+
bbox = createBoundingBox(x, y + (size - fh) / 2, fw, fh);
|
|
867
|
+
}
|
|
609
868
|
return {
|
|
610
|
-
block: { kind: 'image', bbox
|
|
869
|
+
block: { kind: 'image', bbox, fileId },
|
|
611
870
|
fileId,
|
|
612
871
|
format: resource?.bitmap?.format,
|
|
613
872
|
};
|