postext 0.3.43 → 0.3.45
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/escapes.test.d.ts +2 -0
- package/dist/__tests__/parse/escapes.test.d.ts.map +1 -0
- package/dist/__tests__/parse/escapes.test.js +28 -0
- package/dist/__tests__/parse/escapes.test.js.map +1 -0
- 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 +185 -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 +7 -4
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +48 -7
- 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 +396 -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,14 @@ 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
|
-
|
|
283
|
+
const cornerIcon = style.icon.position === 'corner';
|
|
284
|
+
// A corner badge hangs on the top-right corner — or the left one, for an
|
|
285
|
+
// `'outer'` badge on a verso of mirrored margins (`'inner'` on a recto).
|
|
286
|
+
const cornerRight = style.icon.cornerSide === 'right'
|
|
287
|
+
|| (style.icon.cornerSide === 'outer' && !input.mirrored)
|
|
288
|
+
|| (style.icon.cornerSide === 'inner' && !!input.mirrored);
|
|
289
|
+
const iconColumnKept = (hasIcon || (openingMidRun && iconPresent(style.icon))) && !sideStripe && !cornerIcon;
|
|
290
|
+
const iconColumn = iconColumnKept ? iconBoxW + gapPx : 0;
|
|
278
291
|
const innerX = (stripeLeft ? stripeW : 0) + padL + iconColumn;
|
|
279
292
|
const innerTop = (topStripe ? stripeW : 0) + padT;
|
|
280
293
|
// --- Title -----------------------------------------------------------------
|
|
@@ -284,13 +297,20 @@ export function layoutCallout(input) {
|
|
|
284
297
|
const titleFontPx = dimensionToPx(style.titleStyle.fontSize, dpi, em);
|
|
285
298
|
const titleFont = buildFontString(style.titleStyle.fontFamily, titleFontPx, style.titleStyle.fontWeight.toString(), style.titleStyle.italic ? 'italic' : 'normal');
|
|
286
299
|
const titleLineHeight = titleFontPx * 1.2;
|
|
300
|
+
const titleTrackingPx = Math.max(0, dimensionToPx(style.titleStyle.letterSpacing, dpi, titleFontPx));
|
|
301
|
+
// A corner badge on the left hangs half over the title's side: the title
|
|
302
|
+
// starts past it (its inner half plus the icon gap), at least — a
|
|
303
|
+
// configured `indent` only adds beyond that room.
|
|
304
|
+
const cornerLeftRoom = hasIcon && cornerIcon && !cornerRight ? Math.max(0, iconSize / 2 + gapPx - padL) : 0;
|
|
305
|
+
const titleIndentPx = Math.max(cornerLeftRoom, dimensionToPx(style.titleStyle.indent, dpi, titleFontPx));
|
|
287
306
|
const hasTitle = !input.continuation && titleText.trim().length > 0;
|
|
307
|
+
const titleWidthOf = (t) => measureTextWidth(t, titleFont) + titleTrackingPx * t.length;
|
|
288
308
|
// Box width: `fill` uses the given width less the marker column; `auto`
|
|
289
309
|
// shrink-wraps the title.
|
|
290
310
|
let boxWidth = input.width - markerColumn;
|
|
291
311
|
let innerWidth;
|
|
292
312
|
if (isAuto) {
|
|
293
|
-
const titleW = hasTitle ?
|
|
313
|
+
const titleW = hasTitle ? titleWidthOf(titleText) + titleIndentPx : 0;
|
|
294
314
|
innerWidth = Math.max(1, titleW);
|
|
295
315
|
boxWidth = innerX + innerWidth + padR + (stripeRight ? stripeW : 0);
|
|
296
316
|
}
|
|
@@ -301,14 +321,18 @@ export function layoutCallout(input) {
|
|
|
301
321
|
const overlayBlocks = [];
|
|
302
322
|
let titleBlock;
|
|
303
323
|
if (hasTitle) {
|
|
304
|
-
const
|
|
324
|
+
const titleW = Math.max(1, innerWidth - titleIndentPx);
|
|
325
|
+
const measured = measureBlock(titleText, titleFont, titleW, titleLineHeight, {
|
|
326
|
+
textAlign: 'left',
|
|
327
|
+
...(titleTrackingPx > 0 ? { letterSpacingPx: titleTrackingPx } : {}),
|
|
328
|
+
});
|
|
305
329
|
const lines = measured.lines.length > 0
|
|
306
330
|
? measured.lines
|
|
307
|
-
: [{ text: titleText, bbox: createBoundingBox(0, 0,
|
|
331
|
+
: [{ text: titleText, bbox: createBoundingBox(0, 0, titleW, titleLineHeight), baseline: titleLineHeight * 0.8, hyphenated: false }];
|
|
308
332
|
const titleHeight = lines.length * titleLineHeight;
|
|
309
333
|
titleBlock = {
|
|
310
334
|
kind: 'text',
|
|
311
|
-
bbox: createBoundingBox(innerX, cursorY,
|
|
335
|
+
bbox: createBoundingBox(innerX + titleIndentPx, cursorY, titleW, titleHeight),
|
|
312
336
|
fontString: titleFont,
|
|
313
337
|
color: style.titleStyle.color.hex,
|
|
314
338
|
lines: lines.map((ln, i) => ({
|
|
@@ -318,137 +342,286 @@ export function layoutCallout(input) {
|
|
|
318
342
|
width: ln.bbox.width,
|
|
319
343
|
})),
|
|
320
344
|
clip: false,
|
|
345
|
+
...(titleTrackingPx > 0 ? { letterSpacingPx: titleTrackingPx } : {}),
|
|
321
346
|
};
|
|
322
347
|
cursorY += titleHeight;
|
|
323
348
|
}
|
|
324
349
|
// --- Children --------------------------------------------------------------
|
|
350
|
+
// The children stack under the title; a `:::columns{count=N}` group among
|
|
351
|
+
// them lays its own children out in N columns of equal width, cut at
|
|
352
|
+
// the line boundaries that level the columns best, and takes the height
|
|
353
|
+
// of the tallest column.
|
|
325
354
|
const childBlocks = [];
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
355
|
+
const columnGapPx = px(style.columnGap);
|
|
356
|
+
const isFirstRealOf = (k) => children.slice(0, k).every((c) => c.type === 'directive' || isMarkerBlock(c));
|
|
357
|
+
const isLastRealOf = (k) => children.slice(k + 1).every((c) => c.type === 'directive' || isMarkerBlock(c));
|
|
358
|
+
/** Lay out one child at `width` from `x`, appending it to `into` and
|
|
359
|
+
* advancing the stack. */
|
|
360
|
+
const placeChild = (raw, k, width, x, st, into) => {
|
|
361
|
+
const blockIdx = childStartIdx + k;
|
|
362
|
+
const measuredBlock = measureContentBlock(raw, blockIdx, width, derivedCtx, {
|
|
363
|
+
styleOverride: input.paragraphStyleFor?.(blockIdx),
|
|
364
|
+
});
|
|
365
|
+
if (!measuredBlock)
|
|
366
|
+
return undefined;
|
|
367
|
+
const { kind, measured, prefixLen, absoluteSourceMap, mathDisplayRender, resourceBlock } = measuredBlock;
|
|
368
|
+
const { style: bs, vdtType, headingLevel, numberPrefix, listBullet, listDepth, listKind, bulletXOffsetInColumn, strikethroughText } = kind;
|
|
369
|
+
// Spacing above: margin collapsing, list-item spacing inside a run.
|
|
370
|
+
let spacing;
|
|
371
|
+
if (st.first) {
|
|
372
|
+
spacing = st.prevMarginBottom;
|
|
373
|
+
}
|
|
374
|
+
else if (vdtType === 'listItem' && st.prevWasListItem) {
|
|
375
|
+
spacing = listBullet ? listBullet.itemSpacingPx : 0;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
spacing = Math.max(st.prevMarginBottom, bs.marginTopPx);
|
|
379
|
+
}
|
|
380
|
+
st.cursorY += spacing;
|
|
381
|
+
const blk = createVDTBlock(input.nextChildId(), vdtType, bs.fontString, bs.color, bs.textAlign);
|
|
382
|
+
applyStyleAttrs(blk, bs);
|
|
383
|
+
blk.contentIndex = blockIdx;
|
|
384
|
+
blk.containerId = containerId;
|
|
385
|
+
blk.dirty = false;
|
|
386
|
+
blk.snappedToGrid = false;
|
|
387
|
+
blk.headingLevel = headingLevel;
|
|
388
|
+
if (numberPrefix)
|
|
389
|
+
blk.numberPrefix = numberPrefix;
|
|
390
|
+
blk.sourceMap = absoluteSourceMap;
|
|
391
|
+
blk.plainPrefixLen = prefixLen;
|
|
392
|
+
// Line-level cut of the first / last child (split boxes): the kept
|
|
393
|
+
// run of lines, and whether this child opens after its first line
|
|
394
|
+
// (no bullet then — the head carries it).
|
|
395
|
+
const isTextRun = !(vdtType === 'resource' && resourceBlock) && !(vdtType === 'mathDisplay' && mathDisplayRender);
|
|
396
|
+
const isFirstReal = isFirstRealOf(k);
|
|
397
|
+
const isLastReal = isLastRealOf(k);
|
|
398
|
+
const lineFrom = isTextRun && isFirstReal ? Math.max(0, input.lineFrom ?? 0) : 0;
|
|
399
|
+
const lineTo = isTextRun && isLastReal && input.lineTo !== undefined
|
|
400
|
+
? Math.max(lineFrom + 1, Math.min(input.lineTo, measured.lines.length))
|
|
401
|
+
: measured.lines.length;
|
|
402
|
+
const keptLines = isTextRun ? measured.lines.slice(Math.min(lineFrom, measured.lines.length - 1), lineTo) : measured.lines;
|
|
403
|
+
const openedMidRun = lineFrom > 0;
|
|
404
|
+
let height;
|
|
405
|
+
if (vdtType === 'resource' && resourceBlock) {
|
|
406
|
+
height = measured.totalHeight;
|
|
407
|
+
blk.resourceBlock = resourceBlock;
|
|
408
|
+
blk.lines = [{
|
|
409
|
+
text: '',
|
|
410
|
+
bbox: createBoundingBox(0, 0, resourceBlock.bodyRect.width, height),
|
|
411
|
+
baseline: 0,
|
|
412
|
+
hyphenated: false,
|
|
413
|
+
segments: [],
|
|
414
|
+
isLastLine: true,
|
|
415
|
+
}];
|
|
416
|
+
blk.sourceStart = raw.sourceStart + (input.bodyOffset ?? ctx.bodyOffset);
|
|
417
|
+
blk.sourceEnd = raw.sourceEnd + (input.bodyOffset ?? ctx.bodyOffset);
|
|
418
|
+
}
|
|
419
|
+
else if (vdtType === 'mathDisplay' && mathDisplayRender) {
|
|
420
|
+
blk.mathRender = mathDisplayRender;
|
|
421
|
+
blk.tex = raw.tex;
|
|
422
|
+
const mathLine = { ...measured.lines[0], bbox: { ...measured.lines[0].bbox, y: 0 } };
|
|
423
|
+
blk.lines = [mathLine];
|
|
424
|
+
height = mathLine.bbox.height;
|
|
425
|
+
blk.sourceStart = raw.sourceStart + ctx.bodyOffset;
|
|
426
|
+
blk.sourceEnd = raw.sourceEnd + ctx.bodyOffset;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
blk.lines = resetLinePositions(keptLines, bs.lineHeightPx);
|
|
430
|
+
height = blk.lines.length * bs.lineHeightPx;
|
|
431
|
+
blk.sourceStart = keptLines[0].sourceStart ?? raw.sourceStart + ctx.bodyOffset;
|
|
432
|
+
blk.sourceEnd = keptLines[keptLines.length - 1].sourceEnd ?? raw.sourceEnd + ctx.bodyOffset;
|
|
433
|
+
}
|
|
434
|
+
// Relocate to the inner rect (box-relative).
|
|
435
|
+
blk.bbox = createBoundingBox(x, st.cursorY, width, height);
|
|
436
|
+
for (const line of blk.lines) {
|
|
437
|
+
line.bbox.x += x;
|
|
438
|
+
line.bbox.y += st.cursorY;
|
|
439
|
+
line.baseline += st.cursorY;
|
|
440
|
+
}
|
|
441
|
+
if (blk.resourceBlock)
|
|
442
|
+
offsetResourceBlock(blk.resourceBlock, x, st.cursorY);
|
|
443
|
+
if (listBullet && !openedMidRun) {
|
|
444
|
+
blk.listDepth = listDepth;
|
|
445
|
+
blk.listKind = listKind;
|
|
446
|
+
blk.bulletText = listBullet.bulletText;
|
|
447
|
+
blk.bulletFontString = listBullet.bulletFontString;
|
|
448
|
+
blk.bulletColor = listBullet.bulletColor;
|
|
449
|
+
blk.bulletOffsetX = x + bulletXOffsetInColumn;
|
|
450
|
+
if (listBullet.separatorText !== undefined) {
|
|
451
|
+
blk.separatorText = listBullet.separatorText;
|
|
452
|
+
blk.separatorFontString = listBullet.separatorFontString;
|
|
453
|
+
blk.separatorColor = listBullet.separatorColor;
|
|
454
|
+
blk.separatorX = blk.bulletOffsetX + (listBullet.separatorOffsetPx ?? 0);
|
|
455
|
+
}
|
|
456
|
+
if (strikethroughText)
|
|
457
|
+
blk.strikethroughText = true;
|
|
458
|
+
const firstLine = blk.lines[0];
|
|
459
|
+
if (firstLine) {
|
|
460
|
+
blk.bulletY = firstLine.baseline - listBullet.textFontSizePx * 0.3 + listBullet.verticalOffsetPx;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else if (listBullet) {
|
|
464
|
+
blk.listDepth = listDepth;
|
|
465
|
+
blk.listKind = listKind;
|
|
466
|
+
}
|
|
467
|
+
into.push(blk);
|
|
468
|
+
st.cursorY += height;
|
|
469
|
+
st.prevMarginBottom = bs.marginBottomPx;
|
|
470
|
+
st.prevWasListItem = vdtType === 'listItem';
|
|
471
|
+
st.first = false;
|
|
472
|
+
return blk;
|
|
473
|
+
};
|
|
474
|
+
/** The tail of a text block cut at line `l`: a block of its own with the
|
|
475
|
+
* lines from `l` on (no bullet — the head keeps it), at y = 0. */
|
|
476
|
+
const tailOf = (blk, l) => {
|
|
477
|
+
const lh = blk.lines[0]?.bbox.height ?? 0;
|
|
478
|
+
const tail = { ...blk, id: input.nextChildId(), lines: resetLinePositions(blk.lines.slice(l), lh) };
|
|
479
|
+
delete tail.bulletText;
|
|
480
|
+
delete tail.bulletFontString;
|
|
481
|
+
delete tail.bulletColor;
|
|
482
|
+
delete tail.bulletOffsetX;
|
|
483
|
+
delete tail.bulletY;
|
|
484
|
+
delete tail.separatorText;
|
|
485
|
+
delete tail.separatorFontString;
|
|
486
|
+
delete tail.separatorColor;
|
|
487
|
+
delete tail.separatorX;
|
|
488
|
+
tail.bbox = createBoundingBox(blk.bbox.x, 0, blk.bbox.width, tail.lines.length * lh);
|
|
489
|
+
tail.sourceStart = tail.lines[0]?.sourceStart ?? blk.sourceStart;
|
|
490
|
+
return tail;
|
|
491
|
+
};
|
|
492
|
+
/** A `:::columns{count=N}` group: children `k0 … k1 - 1` in N columns. */
|
|
493
|
+
const placeColumnsGroup = (k0, k1, count, st, breaks) => {
|
|
494
|
+
const cols = Math.max(1, Math.min(6, count));
|
|
495
|
+
const colW = Math.max(1, (innerWidth - columnGapPx * (cols - 1)) / cols);
|
|
496
|
+
// Spacing above the group: as a block with no margin of its own.
|
|
497
|
+
const spacing = st.first ? st.prevMarginBottom : st.prevMarginBottom;
|
|
498
|
+
const groupTop = st.cursorY + spacing;
|
|
499
|
+
const gst = { cursorY: 0, prevMarginBottom: 0, prevWasListItem: false, first: true };
|
|
500
|
+
const stack = [];
|
|
501
|
+
for (let k = k0; k < k1; k++) {
|
|
331
502
|
const raw = children[k];
|
|
332
|
-
const blockIdx = childStartIdx + k;
|
|
333
503
|
if (raw.type === 'directive' || isMarkerBlock(raw))
|
|
334
504
|
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);
|
|
505
|
+
placeChild(raw, k, colW, innerX, gst, stack);
|
|
506
|
+
}
|
|
507
|
+
if (stack.length === 0)
|
|
508
|
+
return;
|
|
509
|
+
const total = gst.cursorY;
|
|
510
|
+
const candidates = [];
|
|
511
|
+
stack.forEach((blk, i) => {
|
|
512
|
+
if (i > 0)
|
|
513
|
+
candidates.push({ y: blk.bbox.y, block: i, line: 0 });
|
|
514
|
+
const cuttable = blk.type !== 'resource' && blk.type !== 'mathDisplay' && blk.lines.length > 1 && !blk.mathRender;
|
|
515
|
+
if (cuttable) {
|
|
516
|
+
for (let l = 1; l < blk.lines.length; l++)
|
|
517
|
+
candidates.push({ y: blk.lines[l].bbox.y, block: i, line: l });
|
|
352
518
|
}
|
|
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);
|
|
519
|
+
});
|
|
520
|
+
const cuts = [];
|
|
521
|
+
let prevY = 0;
|
|
522
|
+
if (breaks && breaks.length > 0) {
|
|
523
|
+
// Explicit column starts (`breaks="3"`: the third child opens the
|
|
524
|
+
// second column): block-level cuts, no balancing.
|
|
525
|
+
for (const b of breaks.slice(0, cols - 1)) {
|
|
526
|
+
const i = b - 1;
|
|
527
|
+
if (i <= 0 || i >= stack.length)
|
|
528
|
+
continue;
|
|
529
|
+
const y = stack[i].bbox.y;
|
|
530
|
+
if (y <= prevY + 0.01)
|
|
531
|
+
continue;
|
|
532
|
+
cuts.push({ y, block: i, line: 0 });
|
|
533
|
+
prevY = y;
|
|
391
534
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
for (let c = 1; c < cols; c++) {
|
|
538
|
+
const target = (total * c) / cols;
|
|
539
|
+
let best;
|
|
540
|
+
for (const cand of candidates) {
|
|
541
|
+
if (cand.y <= prevY + 0.01)
|
|
542
|
+
continue;
|
|
543
|
+
if (!best || Math.abs(cand.y - target) < Math.abs(best.y - target))
|
|
544
|
+
best = cand;
|
|
545
|
+
}
|
|
546
|
+
if (!best)
|
|
547
|
+
break;
|
|
548
|
+
cuts.push(best);
|
|
549
|
+
prevY = best.y;
|
|
400
550
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
551
|
+
}
|
|
552
|
+
// Split the stack at the cuts into columns of blocks; a cut inside a
|
|
553
|
+
// text block leaves its head behind and opens the next column with the
|
|
554
|
+
// tail (a block of its own, bullet-less).
|
|
555
|
+
const columns = [[]];
|
|
556
|
+
const columnStarts = [0];
|
|
557
|
+
let cutIdx = 0;
|
|
558
|
+
for (let i = 0; i < stack.length; i++) {
|
|
559
|
+
let blk = stack[i];
|
|
560
|
+
while (cutIdx < cuts.length && cuts[cutIdx].block === i && cuts[cutIdx].line === 0) {
|
|
561
|
+
columns.push([]);
|
|
562
|
+
columnStarts.push(cuts[cutIdx].y);
|
|
563
|
+
cutIdx++;
|
|
406
564
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
line
|
|
411
|
-
|
|
412
|
-
|
|
565
|
+
let consumed = 0;
|
|
566
|
+
while (cutIdx < cuts.length && cuts[cutIdx].block === i && cuts[cutIdx].line > 0) {
|
|
567
|
+
const cut = cuts[cutIdx];
|
|
568
|
+
const l = cut.line - consumed;
|
|
569
|
+
cutIdx++;
|
|
570
|
+
if (l <= 0 || l >= blk.lines.length)
|
|
571
|
+
continue;
|
|
572
|
+
const lh = blk.lines[0]?.bbox.height ?? 0;
|
|
573
|
+
const tail = tailOf(blk, l);
|
|
574
|
+
blk.lines = blk.lines.slice(0, l);
|
|
575
|
+
blk.bbox = createBoundingBox(blk.bbox.x, blk.bbox.y, blk.bbox.width, l * lh);
|
|
576
|
+
columns[columns.length - 1].push(blk);
|
|
577
|
+
offsetBlock(tail, 0, cut.y);
|
|
578
|
+
columns.push([]);
|
|
579
|
+
columnStarts.push(cut.y);
|
|
580
|
+
consumed += l;
|
|
581
|
+
blk = tail;
|
|
413
582
|
}
|
|
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
|
-
}
|
|
583
|
+
columns[columns.length - 1].push(blk);
|
|
584
|
+
}
|
|
585
|
+
let groupHeight = 0;
|
|
586
|
+
columns.forEach((blocks, c) => {
|
|
587
|
+
const shiftX = c * (colW + columnGapPx);
|
|
588
|
+
const shiftY = groupTop - (columnStarts[c] ?? 0);
|
|
589
|
+
for (const blk of blocks) {
|
|
590
|
+
offsetBlock(blk, shiftX, shiftY);
|
|
591
|
+
childBlocks.push(blk);
|
|
592
|
+
groupHeight = Math.max(groupHeight, blk.bbox.y + blk.bbox.height - groupTop);
|
|
435
593
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
594
|
+
});
|
|
595
|
+
st.cursorY = groupTop + groupHeight;
|
|
596
|
+
st.prevMarginBottom = 0;
|
|
597
|
+
st.prevWasListItem = false;
|
|
598
|
+
st.first = false;
|
|
599
|
+
};
|
|
600
|
+
const st = { cursorY, prevMarginBottom: hasTitle ? gapPx : 0, prevWasListItem: false, first: true };
|
|
601
|
+
if (!isAuto) {
|
|
602
|
+
for (let k = 0; k < children.length; k++) {
|
|
603
|
+
const raw = children[k];
|
|
604
|
+
if (raw.type === 'containerStart' && raw.containerName === 'columns') {
|
|
605
|
+
let e = k + 1;
|
|
606
|
+
while (e < children.length && !(children[e].type === 'containerEnd' && children[e].containerName === 'columns' && children[e].containerId === raw.containerId))
|
|
607
|
+
e++;
|
|
608
|
+
const count = Number.parseInt(raw.containerAttrs?.count ?? '2', 10);
|
|
609
|
+
const breaks = (raw.containerAttrs?.breaks ?? '').split(',').map((v) => Number.parseInt(v.trim(), 10)).filter((v) => Number.isFinite(v) && v > 1);
|
|
610
|
+
placeColumnsGroup(k + 1, e, Number.isFinite(count) ? count : 2, st, breaks.length > 0 ? breaks : undefined);
|
|
611
|
+
k = e;
|
|
612
|
+
continue;
|
|
439
613
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
prevWasListItem = vdtType === 'listItem';
|
|
444
|
-
first = false;
|
|
614
|
+
if (raw.type === 'directive' || isMarkerBlock(raw))
|
|
615
|
+
continue;
|
|
616
|
+
placeChild(raw, k, innerWidth, innerX, st, childBlocks);
|
|
445
617
|
}
|
|
446
618
|
}
|
|
619
|
+
cursorY = st.cursorY;
|
|
447
620
|
// An icon taller than the content grows the inner area to fit it; with
|
|
448
621
|
// `align: 'center'` the title and children are centred on the icon.
|
|
449
622
|
let contentBottom = cursorY;
|
|
450
623
|
const contentH = contentBottom - innerTop;
|
|
451
|
-
if (hasIcon && iconSize > contentH) {
|
|
624
|
+
if (hasIcon && !cornerIcon && iconSize > contentH) {
|
|
452
625
|
const extra = iconSize - contentH;
|
|
453
626
|
if (style.icon.align === 'center') {
|
|
454
627
|
if (titleBlock)
|
|
@@ -491,13 +664,19 @@ export function layoutCallout(input) {
|
|
|
491
664
|
if (hasIcon) {
|
|
492
665
|
// Over a side stripe the icon is centred on the stripe; otherwise it
|
|
493
666
|
// sits in its own column left of the content.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
667
|
+
// A corner badge hangs on its corner half past the border, flush with
|
|
668
|
+
// the top edge.
|
|
669
|
+
const iconX = cornerIcon
|
|
670
|
+
? (cornerRight ? boxWidth - iconSize / 2 : -iconSize / 2)
|
|
671
|
+
: sideStripe
|
|
672
|
+
? (stripeLeft ? (stripeW - iconSize) / 2 : boxWidth - stripeW + (stripeW - iconSize) / 2)
|
|
673
|
+
: innerX - iconColumn;
|
|
674
|
+
const iconY = cornerIcon
|
|
675
|
+
? 0
|
|
676
|
+
: style.icon.align === 'center'
|
|
677
|
+
? innerTop + (contentBottom - innerTop - iconSize) / 2
|
|
678
|
+
: innerTop;
|
|
679
|
+
const built = buildIconBlock(style.icon, iconX, iconY, iconSize, ctx, iconBoxW);
|
|
501
680
|
if (built) {
|
|
502
681
|
overlayBlocks.push(built.block);
|
|
503
682
|
iconFileId = built.fileId;
|
|
@@ -506,6 +685,72 @@ export function layoutCallout(input) {
|
|
|
506
685
|
}
|
|
507
686
|
if (titleBlock)
|
|
508
687
|
overlayBlocks.push(titleBlock);
|
|
688
|
+
// --- Label tab ---------------------------------------------------------------
|
|
689
|
+
// The fence's `label` on a tab hugging a top corner, rising `offset`
|
|
690
|
+
// above the box; an icon beside it and a rule along the top edge.
|
|
691
|
+
let labelRisePx = 0;
|
|
692
|
+
if (style.label && !input.continuation && labelText.trim().length > 0) {
|
|
693
|
+
const lb = style.label;
|
|
694
|
+
const lbFontPx = dimensionToPx(lb.fontSize, dpi, em);
|
|
695
|
+
const lbFont = buildFontString(lb.fontFamily, lbFontPx, lb.fontWeight.toString());
|
|
696
|
+
const padX = dimensionToPx(lb.paddingX, dpi, lbFontPx);
|
|
697
|
+
const tabH = dimensionToPx(lb.height, dpi, lbFontPx);
|
|
698
|
+
const rise = px(lb.offset);
|
|
699
|
+
const inset = px(lb.inset);
|
|
700
|
+
const textW = measureTextWidth(labelText, lbFont);
|
|
701
|
+
const tabW = textW + 2 * padX;
|
|
702
|
+
const onRight = lb.position === 'top-right';
|
|
703
|
+
const tabX = onRight ? boxWidth - inset - tabW : inset;
|
|
704
|
+
const tabY = -rise;
|
|
705
|
+
labelRisePx = Math.max(0, rise);
|
|
706
|
+
overlayBlocks.push({
|
|
707
|
+
kind: 'box',
|
|
708
|
+
bbox: createBoundingBox(tabX, tabY, tabW, tabH),
|
|
709
|
+
box: { backgroundColor: lb.background.hex, borderWidthPx: 0, borderRadiusPx: 0 },
|
|
710
|
+
});
|
|
711
|
+
overlayBlocks.push({
|
|
712
|
+
kind: 'text',
|
|
713
|
+
bbox: createBoundingBox(tabX, tabY, tabW, tabH),
|
|
714
|
+
fontString: lbFont,
|
|
715
|
+
color: lb.color.hex,
|
|
716
|
+
lines: [{ text: labelText, xOffset: padX, baselineY: tabY + tabH / 2 + lbFontPx * 0.36, width: textW }],
|
|
717
|
+
clip: false,
|
|
718
|
+
});
|
|
719
|
+
let edgeX = onRight ? tabX : tabX + tabW;
|
|
720
|
+
if (lb.icon.resourceId) {
|
|
721
|
+
const iconW = px(lb.icon.width);
|
|
722
|
+
const gap = px(lb.icon.gap);
|
|
723
|
+
const iconX = onRight ? tabX - gap - iconW : tabX + tabW + gap;
|
|
724
|
+
const resource = ctx.resourceById.get(lb.icon.resourceId);
|
|
725
|
+
const fileId = resource?.bitmap?.fileId ?? resource?.svg?.fileId;
|
|
726
|
+
if (fileId) {
|
|
727
|
+
const dims = resource?.bitmap ?? resource?.svg;
|
|
728
|
+
let w = iconW;
|
|
729
|
+
let h = tabH;
|
|
730
|
+
if (dims?.width && dims?.height) {
|
|
731
|
+
const k = Math.min(iconW / dims.width, tabH / dims.height);
|
|
732
|
+
w = dims.width * k;
|
|
733
|
+
h = dims.height * k;
|
|
734
|
+
}
|
|
735
|
+
overlayBlocks.push({ kind: 'image', bbox: createBoundingBox(onRight ? iconX + iconW - w : iconX, tabY + (tabH - h) / 2, w, h), fileId });
|
|
736
|
+
edgeX = onRight ? iconX + iconW - w : iconX + w;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (lb.rule.enabled) {
|
|
740
|
+
const ruleW = px(lb.rule.width);
|
|
741
|
+
const x0 = onRight ? 0 : edgeX;
|
|
742
|
+
const len = onRight ? edgeX : boxWidth - edgeX;
|
|
743
|
+
if (len > 0) {
|
|
744
|
+
overlayBlocks.push({
|
|
745
|
+
kind: 'rule',
|
|
746
|
+
bbox: createBoundingBox(x0, -ruleW / 2, len, ruleW),
|
|
747
|
+
color: lb.rule.color.hex,
|
|
748
|
+
thicknessPx: ruleW,
|
|
749
|
+
direction: 'horizontal',
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
509
754
|
// --- Marker column + frame -----------------------------------------------------
|
|
510
755
|
// The frame is as tall as the tallest of box, marker and rule; the three
|
|
511
756
|
// are centred on each other (`marker.align: 'center'`) or top-aligned.
|
|
@@ -546,13 +791,26 @@ export function layoutCallout(input) {
|
|
|
546
791
|
});
|
|
547
792
|
}
|
|
548
793
|
}
|
|
794
|
+
// The label tab rises above the box inside the block: the box starts
|
|
795
|
+
// `labelRisePx` down, so the tab keeps its room at a column head too,
|
|
796
|
+
// where the top margin is dropped.
|
|
797
|
+
if (labelRisePx > 0) {
|
|
798
|
+
for (const b of overlayBlocks)
|
|
799
|
+
offsetDesignBlock(b, 0, labelRisePx);
|
|
800
|
+
for (const b of markerBlocks)
|
|
801
|
+
offsetDesignBlock(b, 0, labelRisePx);
|
|
802
|
+
for (const blk of childBlocks)
|
|
803
|
+
offsetBlock(blk, 0, labelRisePx);
|
|
804
|
+
innerRect.y += labelRisePx;
|
|
805
|
+
}
|
|
806
|
+
const frameHeight = totalHeight + labelRisePx;
|
|
549
807
|
const overlay = {
|
|
550
|
-
bbox: createBoundingBox(0, 0, width,
|
|
808
|
+
bbox: createBoundingBox(0, 0, width, frameHeight),
|
|
551
809
|
blocks: [...markerBlocks, ...overlayBlocks],
|
|
552
810
|
};
|
|
553
811
|
// --- Frame block -------------------------------------------------------------
|
|
554
812
|
const frame = createVDTBlock(frameId, 'callout', bodyStyle.fontString, bodyStyle.color, bodyStyle.textAlign);
|
|
555
|
-
frame.bbox = createBoundingBox(0, 0, width,
|
|
813
|
+
frame.bbox = createBoundingBox(0, 0, width, frameHeight);
|
|
556
814
|
frame.lines = [];
|
|
557
815
|
frame.dirty = false;
|
|
558
816
|
frame.snappedToGrid = false;
|
|
@@ -573,7 +831,7 @@ export function layoutCallout(input) {
|
|
|
573
831
|
frame,
|
|
574
832
|
children: childBlocks,
|
|
575
833
|
width,
|
|
576
|
-
totalHeight,
|
|
834
|
+
totalHeight: frameHeight,
|
|
577
835
|
marginTopPx: px(style.marginTop),
|
|
578
836
|
marginBottomPx: px(style.marginBottom),
|
|
579
837
|
};
|
|
@@ -599,15 +857,22 @@ function fitInSquare(x, y, size, w, h) {
|
|
|
599
857
|
/** The design block for an icon spec drawn in the square `(x, y, size)`:
|
|
600
858
|
* an image block for a resource (aspect-fitted; `undefined` when the
|
|
601
859
|
* resource is missing), a centred text block for a glyph. */
|
|
602
|
-
function buildIconBlock(spec, x, y, size, ctx) {
|
|
860
|
+
function buildIconBlock(spec, x, y, size, ctx, boxWidth = size) {
|
|
603
861
|
if (spec.kind === 'resource') {
|
|
604
862
|
const resource = ctx.resourceById.get(spec.resourceId);
|
|
605
863
|
const fileId = resource?.bitmap?.fileId ?? resource?.svg?.fileId;
|
|
606
864
|
if (!fileId)
|
|
607
865
|
return undefined;
|
|
608
866
|
const dims = resource?.bitmap ?? resource?.svg;
|
|
867
|
+
let bbox = fitInSquare(x, y, size, dims?.width, dims?.height);
|
|
868
|
+
if (boxWidth > size && dims?.width && dims?.height) {
|
|
869
|
+
const k = Math.min(boxWidth / dims.width, size / dims.height);
|
|
870
|
+
const fw = dims.width * k;
|
|
871
|
+
const fh = dims.height * k;
|
|
872
|
+
bbox = createBoundingBox(x, y + (size - fh) / 2, fw, fh);
|
|
873
|
+
}
|
|
609
874
|
return {
|
|
610
|
-
block: { kind: 'image', bbox
|
|
875
|
+
block: { kind: 'image', bbox, fileId },
|
|
611
876
|
fileId,
|
|
612
877
|
format: resource?.bitmap?.format,
|
|
613
878
|
};
|