pa_font 0.2.1 → 0.2.2

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/paFont.js CHANGED
@@ -3186,61 +3186,95 @@ var sharedGraphemeSegmenter = null;
3186
3186
  function layoutParagraph(fontInstance, text, options = {}, state = {}) {
3187
3187
  const normalized = normalizeParagraphOptions(fontInstance, options);
3188
3188
  const textValue = String(text ?? "");
3189
- const layoutState = normalized.engine === "pretext" && normalized.overflowWrap !== "normal" ? layoutWithPretext(fontInstance, textValue, normalized, state) : layoutWithNative(fontInstance, textValue, normalized);
3190
- const lines = positionLines(fontInstance, applyMaxLines(layoutState.lines, normalized, createTextMeasurer(fontInstance, normalized)), normalized);
3191
- const bbox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
3192
- const width = lines.reduce((max, line) => Math.max(max, line.width), 0);
3189
+ const layoutBox = resolveLayoutBox(normalized, state);
3190
+ const retainedPreparedState = resolveRetainedPreparedState(state, normalized);
3191
+ const pretextState = shouldAttemptPretextLayout(normalized) ? layoutWithPretext(textValue, normalized, retainedPreparedState, layoutBox) : null;
3192
+ const layoutState = pretextState != null && canUsePretextLayout(pretextState, normalized) ? pretextState : layoutWithNative(fontInstance, textValue, normalized, layoutBox);
3193
+ const measureWidth = createLazyTextMeasurer(fontInstance, normalized);
3194
+ const lines = positionLines(fontInstance, applyOverflowClamping(layoutState.lines, normalized, layoutBox, measureWidth), normalized, layoutBox, measureWidth);
3195
+ const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
3196
+ const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
3197
+ const textHeight = lines.length * normalized.lineHeight;
3198
+ const finalLayoutBox = finalizeLayoutBox(layoutBox, normalized, textHeight);
3199
+ const cachedPrepared = pretextState?.prepared ?? retainedPreparedState.prepared ?? null;
3200
+ const cachedPreparedWhiteSpace = pretextState?.preparedWhiteSpace ?? retainedPreparedState.preparedWhiteSpace ?? null;
3193
3201
  return {
3194
3202
  options: normalized,
3195
3203
  lines,
3196
3204
  metrics: {
3197
- x: normalized.x,
3198
- y: normalized.y,
3199
- width,
3200
- height: lines.length * normalized.lineHeight,
3205
+ x: finalLayoutBox.contentBox.x,
3206
+ y: finalLayoutBox.contentBox.y,
3207
+ width: textWidth,
3208
+ height: textHeight,
3201
3209
  lineCount: lines.length,
3202
- bbox
3210
+ bbox: textBBox,
3211
+ contentBox: { ...finalLayoutBox.contentBox },
3212
+ paddingBox: { ...finalLayoutBox.paddingBox },
3213
+ marginBox: { ...finalLayoutBox.marginBox },
3214
+ clipBox: { ...finalLayoutBox.clipBox }
3203
3215
  },
3204
- prepared: layoutState.prepared ?? null,
3205
- preparedWhiteSpace: layoutState.preparedWhiteSpace ?? null,
3206
- layoutEngine: layoutState.layoutEngine
3216
+ prepared: cachedPrepared,
3217
+ preparedWhiteSpace: cachedPreparedWhiteSpace,
3218
+ layoutEngine: layoutState.layoutEngine,
3219
+ layoutBox: finalLayoutBox,
3220
+ containerWidth: layoutBox.containerWidth,
3221
+ containerHeight: layoutBox.containerHeight
3207
3222
  };
3208
3223
  }
3209
3224
  function normalizeParagraphOptions(fontInstance, options = {}) {
3210
3225
  if (options == null || typeof options !== "object" || Array.isArray(options)) throw new TypeError("font.paragraph() options must be an object.");
3211
3226
  const textOptions = normalizeTextOptions(options);
3212
- const width = normalizePositive(options.width, 0);
3213
- if (width <= 0) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
3227
+ const wrapDefaults = resolveWrapDefaults(normalizeNullableEnum(options.wrap, [
3228
+ "word",
3229
+ "char",
3230
+ "keep"
3231
+ ], null));
3232
+ const width = normalizeDimension(options.width);
3233
+ const height = normalizeDimension(options.height);
3234
+ if (options.width != null && width == null) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
3235
+ if (options.height != null && height == null) throw new TypeError("font.paragraph() option \"height\" must be a positive number.");
3214
3236
  const font = resolveCanvasFont(fontInstance, textOptions.size, options);
3215
- const lineHeight = resolveLineHeight(options.lineHeight, textOptions.size);
3216
- const align = normalizeEnum(options.align, [
3217
- "left",
3218
- "center",
3219
- "right",
3220
- "justify"
3221
- ], "left");
3222
- const whiteSpace = normalizeEnum(options.whiteSpace, [
3223
- "normal",
3224
- "pre-wrap",
3225
- "nowrap"
3226
- ], "normal");
3227
- const overflowWrap = normalizeEnum(options.overflowWrap, [
3228
- "normal",
3229
- "break-word",
3230
- "anywhere"
3231
- ], "break-word");
3232
- const engine = normalizeEnum(options.engine, ["pretext", "native"], "pretext");
3233
- const maxLines = normalizeMaxLines(options.maxLines);
3234
- const ellipsis = normalizeEllipsis(options.ellipsis);
3235
3237
  return {
3236
3238
  ...textOptions,
3239
+ wrap: resolveWrapPreset(normalizeEnum(options.wordBreak, [
3240
+ "normal",
3241
+ "break-all",
3242
+ "keep-all"
3243
+ ], wrapDefaults.wordBreak), normalizeEnum(options.overflowWrap, [
3244
+ "normal",
3245
+ "break-word",
3246
+ "anywhere"
3247
+ ], wrapDefaults.overflowWrap)),
3237
3248
  width,
3238
- lineHeight,
3239
- align,
3240
- whiteSpace,
3241
- overflowWrap,
3242
- maxLines,
3243
- ellipsis,
3249
+ height,
3250
+ lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
3251
+ align: normalizeEnum(options.align, [
3252
+ "left",
3253
+ "center",
3254
+ "right",
3255
+ "justify"
3256
+ ], "left"),
3257
+ whiteSpace: normalizeEnum(options.whiteSpace, [
3258
+ "normal",
3259
+ "pre-wrap",
3260
+ "nowrap"
3261
+ ], "normal"),
3262
+ overflowWrap: normalizeEnum(options.overflowWrap, [
3263
+ "normal",
3264
+ "break-word",
3265
+ "anywhere"
3266
+ ], wrapDefaults.overflowWrap),
3267
+ wordBreak: normalizeEnum(options.wordBreak, [
3268
+ "normal",
3269
+ "break-all",
3270
+ "keep-all"
3271
+ ], wrapDefaults.wordBreak),
3272
+ overflow: normalizeEnum(options.overflow, ["visible", "hidden"], "visible"),
3273
+ textOverflow: normalizeNullableEnum(options.textOverflow, ["clip", "ellipsis"], null),
3274
+ maxLines: normalizeMaxLines(options.maxLines),
3275
+ ellipsis: normalizeEllipsis(options.ellipsis),
3276
+ margin: normalizeSpacing(options.margin),
3277
+ padding: normalizeSpacing(options.padding),
3244
3278
  fontStyle: normalizeEnum(options.fontStyle, [
3245
3279
  "normal",
3246
3280
  "italic",
@@ -3249,7 +3283,7 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
3249
3283
  fontWeight: typeof options.fontWeight === "string" || Number.isFinite(options.fontWeight) ? options.fontWeight : 400,
3250
3284
  fontFamily: resolveFontFamily(fontInstance, options.fontFamily),
3251
3285
  font,
3252
- engine
3286
+ engine: normalizeEnum(options.engine, ["pretext", "native"], "pretext")
3253
3287
  };
3254
3288
  }
3255
3289
  function resolveCanvasFont(fontInstance, size, options = {}) {
@@ -3263,11 +3297,12 @@ function resolveCanvasFont(fontInstance, size, options = {}) {
3263
3297
  function canReusePreparedParagraphState(previousState, previousOptions, nextOptions) {
3264
3298
  return previousState?.prepared != null && previousState.preparedWhiteSpace === resolvePretextWhiteSpace(nextOptions.whiteSpace) && previousOptions?.font === nextOptions.font;
3265
3299
  }
3266
- function layoutWithPretext(fontInstance, text, options, state) {
3300
+ function layoutWithPretext(text, options, state, layoutBox) {
3267
3301
  const preparedWhiteSpace = resolvePretextWhiteSpace(options.whiteSpace);
3268
3302
  const prepared = state.prepared != null && state.preparedWhiteSpace === preparedWhiteSpace && state.font === options.font ? state.prepared : prepareWithSegments(text, options.font, { whiteSpace: preparedWhiteSpace });
3303
+ const layout = layoutWithLines(prepared, options.whiteSpace === "nowrap" ? HUGE_LAYOUT_WIDTH : layoutBox.contentWidth, options.lineHeight);
3269
3304
  return {
3270
- lines: layoutWithLines(prepared, options.whiteSpace === "nowrap" ? HUGE_LAYOUT_WIDTH : options.width, options.lineHeight).lines.map((line) => ({
3305
+ lines: layout.lines.map((line) => ({
3271
3306
  text: line.text,
3272
3307
  width: line.width,
3273
3308
  start: null,
@@ -3277,10 +3312,10 @@ function layoutWithPretext(fontInstance, text, options, state) {
3277
3312
  prepared,
3278
3313
  preparedWhiteSpace,
3279
3314
  layoutEngine: "pretext",
3280
- fontInstance
3315
+ usedOverflowWrapFallbackBreaks: layout.lines.some((line) => line.end.graphemeIndex > 0)
3281
3316
  };
3282
3317
  }
3283
- function layoutWithNative(fontInstance, text, options) {
3318
+ function layoutWithNative(fontInstance, text, options, layoutBox) {
3284
3319
  const measuredWidth = createOpenTypeMeasurer(fontInstance, options);
3285
3320
  const source = normalizeNativeText(text, options.whiteSpace);
3286
3321
  if (source.length === 0) return {
@@ -3301,7 +3336,7 @@ function layoutWithNative(fontInstance, text, options) {
3301
3336
  preparedWhiteSpace: null,
3302
3337
  layoutEngine: "native"
3303
3338
  };
3304
- const tokens = tokenizeNativeText(source, options.whiteSpace, measuredWidth);
3339
+ const tokens = tokenizeNativeText(source, options.whiteSpace, measuredWidth, options.wordBreak);
3305
3340
  const lines = [];
3306
3341
  let currentText = "";
3307
3342
  let currentWidth = 0;
@@ -3329,12 +3364,12 @@ function layoutWithNative(fontInstance, text, options) {
3329
3364
  currentEnd = piece.end;
3330
3365
  };
3331
3366
  const appendWrappedToken = (token) => {
3332
- if (options.overflowWrap === "normal") {
3367
+ if (options.overflowWrap === "normal" && options.wordBreak !== "break-all") {
3333
3368
  appendPiece(token);
3334
3369
  return;
3335
3370
  }
3336
3371
  splitIntoGraphemePieces(token, measuredWidth).forEach((piece) => {
3337
- if (currentText.length > 0 && currentWidth + piece.width > options.width + JUSTIFY_EPSILON) pushCurrentLine(false, piece.start);
3372
+ if (currentText.length > 0 && currentWidth + piece.width > layoutBox.contentWidth + JUSTIFY_EPSILON) pushCurrentLine(false, piece.start);
3338
3373
  appendPiece(piece);
3339
3374
  });
3340
3375
  };
@@ -3344,7 +3379,7 @@ function layoutWithNative(fontInstance, text, options) {
3344
3379
  return;
3345
3380
  }
3346
3381
  if (token.type === "space" && currentText.length === 0 && options.whiteSpace !== "pre-wrap") return;
3347
- if (currentText.length === 0 || currentWidth + token.width <= options.width + JUSTIFY_EPSILON) {
3382
+ if (currentText.length === 0 || currentWidth + token.width <= layoutBox.contentWidth + JUSTIFY_EPSILON) {
3348
3383
  appendPiece(token);
3349
3384
  return;
3350
3385
  }
@@ -3381,45 +3416,69 @@ function splitIntoGraphemePieces(token, measureWidth) {
3381
3416
  }
3382
3417
  return pieces.length > 0 ? pieces : [token];
3383
3418
  }
3384
- function applyMaxLines(lines, options, measureWidth) {
3385
- if (options.maxLines == null || lines.length <= options.maxLines) return lines;
3386
- const clipped = lines.slice(0, options.maxLines).map((line) => ({ ...line }));
3387
- if (options.ellipsis !== false && clipped.length > 0) {
3388
- const lastLine = clipped[clipped.length - 1];
3389
- const suffix = options.ellipsis;
3390
- const suffixWidth = measureWidth(suffix);
3391
- const trimmed = trimTrailingWhitespace(lastLine.text);
3392
- if (suffixWidth > options.width + JUSTIFY_EPSILON) {
3393
- lastLine.text = "";
3394
- lastLine.width = 0;
3395
- } else {
3396
- let nextText = trimmed;
3397
- while (nextText.length > 0 && measureWidth(`${nextText}${suffix}`) > options.width + JUSTIFY_EPSILON) nextText = trimLastGrapheme(nextText);
3398
- lastLine.text = `${nextText}${suffix}`;
3399
- lastLine.width = measureWidth(lastLine.text);
3400
- }
3401
- lastLine.hardBreak = false;
3402
- }
3403
- return clipped;
3404
- }
3405
- function positionLines(fontInstance, lines, options) {
3419
+ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
3420
+ const contentWidth = layoutBox.contentWidth;
3421
+ const result = lines.map((line) => ({ ...line }));
3422
+ let lineLimit = options.maxLines;
3423
+ if (options.overflow === "hidden" && layoutBox.clipContentHeight != null) {
3424
+ const visibleLineCount = Math.max(0, Math.floor((layoutBox.clipContentHeight + JUSTIFY_EPSILON) / options.lineHeight));
3425
+ lineLimit = lineLimit == null ? visibleLineCount : Math.min(lineLimit, visibleLineCount);
3426
+ }
3427
+ let clippedByCount = false;
3428
+ if (lineLimit != null) {
3429
+ if (lineLimit <= 0) return [];
3430
+ if (result.length > lineLimit) {
3431
+ result.length = lineLimit;
3432
+ clippedByCount = true;
3433
+ }
3434
+ }
3435
+ if (result.length === 0) return result;
3436
+ if (options.overflow === "hidden" && options.whiteSpace === "nowrap" && result[0].width > contentWidth + JUSTIFY_EPSILON) result[0] = truncateLineToWidth(result[0], contentWidth, measureWidth, options.textOverflow === "ellipsis" ? options.ellipsis : false);
3437
+ else if (clippedByCount && shouldEllipsizeClampedLines(options)) {
3438
+ const lastIndex = result.length - 1;
3439
+ result[lastIndex] = truncateLineToWidth(result[lastIndex], contentWidth, measureWidth, options.ellipsis);
3440
+ result[lastIndex].hardBreak = false;
3441
+ }
3442
+ return result;
3443
+ }
3444
+ function truncateLineToWidth(line, maxWidth, measureWidth, suffix) {
3445
+ const suffixText = suffix === false ? "" : suffix;
3446
+ if ((suffixText.length > 0 ? measureWidth(suffixText) : 0) > maxWidth + JUSTIFY_EPSILON) return {
3447
+ ...line,
3448
+ text: "",
3449
+ width: 0,
3450
+ hardBreak: false
3451
+ };
3452
+ let nextText = trimTrailingWhitespace(line.text);
3453
+ while (nextText.length > 0 && measureWidth(`${nextText}${suffixText}`) > maxWidth + JUSTIFY_EPSILON) nextText = trimLastGrapheme(nextText);
3454
+ const text = `${nextText}${suffixText}`;
3455
+ return {
3456
+ ...line,
3457
+ text,
3458
+ width: measureWidth(text),
3459
+ hardBreak: false
3460
+ };
3461
+ }
3462
+ function shouldEllipsizeClampedLines(options) {
3463
+ return options.textOverflow === "ellipsis" || options.textOverflow == null && options.maxLines != null && options.ellipsis !== false;
3464
+ }
3465
+ function positionLines(fontInstance, lines, options, layoutBox, measureWidth) {
3406
3466
  const ascent = getFontAscender(fontInstance, options.size);
3407
3467
  const lineBoxHeight = options.lineHeight;
3408
- const measureWidth = createTextMeasurer(fontInstance, options);
3409
3468
  let cursor = 0;
3410
3469
  return lines.map((line, index) => {
3411
3470
  const justified = shouldJustifyLine(line, index, lines.length, options);
3412
- const offsetX = justified ? 0 : resolveAlignOffset(options.align, line.width, options.width);
3413
- const x = options.x + offsetX;
3414
- const y = options.y + index * lineBoxHeight;
3471
+ const offsetX = justified ? 0 : resolveAlignOffset(options.align, line.width, layoutBox.contentWidth);
3472
+ const x = layoutBox.contentX + offsetX;
3473
+ const y = layoutBox.contentY + index * lineBoxHeight;
3415
3474
  const baseline = y + ascent;
3416
- const fragments = justified ? buildJustifiedFragments(line, options, measureWidth) : [{
3475
+ const fragments = justified ? buildJustifiedFragments(line, layoutBox.contentX, layoutBox.contentWidth, measureWidth) : [{
3417
3476
  text: line.text,
3418
3477
  x,
3419
3478
  width: line.width,
3420
3479
  isWhitespace: false
3421
3480
  }];
3422
- const width = justified ? options.width : line.width;
3481
+ const width = justified ? layoutBox.contentWidth : line.width;
3423
3482
  const bbox = {
3424
3483
  x,
3425
3484
  y,
@@ -3444,7 +3503,7 @@ function positionLines(fontInstance, lines, options) {
3444
3503
  return positioned;
3445
3504
  });
3446
3505
  }
3447
- function buildJustifiedFragments(line, options, measureWidth) {
3506
+ function buildJustifiedFragments(line, contentX, contentWidth, measureWidth) {
3448
3507
  const tokens = splitPreservingWhitespace(line.text);
3449
3508
  const expandable = tokens.reduce((count, token, index) => {
3450
3509
  if (index > 0 && index < tokens.length - 1 && /\s/u.test(token)) return count + 1;
@@ -3452,13 +3511,13 @@ function buildJustifiedFragments(line, options, measureWidth) {
3452
3511
  }, 0);
3453
3512
  if (expandable === 0) return [{
3454
3513
  text: line.text,
3455
- x: options.x,
3514
+ x: contentX,
3456
3515
  width: line.width,
3457
3516
  isWhitespace: false
3458
3517
  }];
3459
- const extraPerGap = Math.max(0, options.width - line.width) / expandable;
3518
+ const extraPerGap = Math.max(0, contentWidth - line.width) / expandable;
3460
3519
  const fragments = [];
3461
- let cursorX = options.x;
3520
+ let cursorX = contentX;
3462
3521
  tokens.forEach((token, index) => {
3463
3522
  const isWhitespace = /\s/u.test(token);
3464
3523
  const isExpandable = isWhitespace && index > 0 && index < tokens.length - 1;
@@ -3486,20 +3545,169 @@ function resolveLineHeight(value, size) {
3486
3545
  if (value <= 10) return size * value;
3487
3546
  return value;
3488
3547
  }
3548
+ function resolveLayoutBox(options, state = {}) {
3549
+ const margin = options.margin;
3550
+ const padding = options.padding;
3551
+ const containerWidth = resolveContainerDimension(state.containerWidth, typeof window !== "undefined" ? window.innerWidth : null);
3552
+ const containerHeight = resolveContainerDimension(state.containerHeight, typeof window !== "undefined" ? window.innerHeight : null);
3553
+ const contentWidth = options.width ?? Math.max(1, (containerWidth ?? 0) - options.x - margin.left - margin.right - padding.left - padding.right);
3554
+ const contentX = options.x + margin.left + padding.left;
3555
+ const contentY = options.y + margin.top + padding.top;
3556
+ return {
3557
+ containerWidth,
3558
+ containerHeight,
3559
+ contentWidth,
3560
+ clipContentHeight: options.height,
3561
+ contentX,
3562
+ contentY,
3563
+ margin,
3564
+ padding,
3565
+ contentBox: {
3566
+ x: contentX,
3567
+ y: contentY,
3568
+ w: contentWidth,
3569
+ h: options.height ?? 0
3570
+ },
3571
+ paddingBox: {
3572
+ x: options.x + margin.left,
3573
+ y: options.y + margin.top,
3574
+ w: contentWidth + padding.left + padding.right,
3575
+ h: (options.height ?? 0) + padding.top + padding.bottom
3576
+ },
3577
+ marginBox: {
3578
+ x: options.x,
3579
+ y: options.y,
3580
+ w: contentWidth + padding.left + padding.right + margin.left + margin.right,
3581
+ h: (options.height ?? 0) + padding.top + padding.bottom + margin.top + margin.bottom
3582
+ },
3583
+ clipBox: {
3584
+ x: contentX,
3585
+ y: contentY,
3586
+ w: contentWidth,
3587
+ h: options.height ?? 0
3588
+ }
3589
+ };
3590
+ }
3591
+ function finalizeLayoutBox(layoutBox, options, textHeight) {
3592
+ const contentHeight = options.height ?? textHeight;
3593
+ return {
3594
+ ...layoutBox,
3595
+ contentBox: {
3596
+ x: layoutBox.contentX,
3597
+ y: layoutBox.contentY,
3598
+ w: layoutBox.contentWidth,
3599
+ h: contentHeight
3600
+ },
3601
+ paddingBox: {
3602
+ x: layoutBox.paddingBox.x,
3603
+ y: layoutBox.paddingBox.y,
3604
+ w: layoutBox.paddingBox.w,
3605
+ h: contentHeight + layoutBox.padding.top + layoutBox.padding.bottom
3606
+ },
3607
+ marginBox: {
3608
+ x: layoutBox.marginBox.x,
3609
+ y: layoutBox.marginBox.y,
3610
+ w: layoutBox.marginBox.w,
3611
+ h: contentHeight + layoutBox.padding.top + layoutBox.padding.bottom + layoutBox.margin.top + layoutBox.margin.bottom
3612
+ },
3613
+ clipBox: {
3614
+ x: layoutBox.contentX,
3615
+ y: layoutBox.contentY,
3616
+ w: layoutBox.contentWidth,
3617
+ h: options.overflow === "hidden" ? contentHeight : textHeight
3618
+ }
3619
+ };
3620
+ }
3621
+ function resolveContainerDimension(explicit, fallback) {
3622
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
3623
+ if (Number.isFinite(fallback) && fallback > 0) return fallback;
3624
+ return null;
3625
+ }
3489
3626
  function normalizeEnum(value, supported, fallback) {
3490
3627
  return typeof value === "string" && supported.includes(value) ? value : fallback;
3491
3628
  }
3629
+ function normalizeNullableEnum(value, supported, fallback) {
3630
+ if (value == null) return fallback;
3631
+ return typeof value === "string" && supported.includes(value) ? value : fallback;
3632
+ }
3492
3633
  function normalizeEllipsis(value) {
3493
- if (value === false || value == null) return false;
3494
- return typeof value === "string" ? value : "…";
3634
+ if (value === false) return false;
3635
+ if (typeof value === "string") return value;
3636
+ return "…";
3495
3637
  }
3496
3638
  function normalizeMaxLines(value) {
3497
3639
  if (!Number.isFinite(value)) return null;
3498
3640
  const rounded = Math.floor(value);
3499
3641
  return rounded > 0 ? rounded : null;
3500
3642
  }
3643
+ function normalizeDimension(value) {
3644
+ return Number.isFinite(value) && value > 0 ? value : null;
3645
+ }
3646
+ function normalizeSpacing(value) {
3647
+ if (value == null) return zeroSpacing();
3648
+ if (Number.isFinite(value)) {
3649
+ const next = Number(value);
3650
+ return {
3651
+ top: next,
3652
+ right: next,
3653
+ bottom: next,
3654
+ left: next
3655
+ };
3656
+ }
3657
+ if (Array.isArray(value)) {
3658
+ const numbers = value.map((entry) => Number.isFinite(entry) ? Number(entry) : 0);
3659
+ if (numbers.length === 2) return {
3660
+ top: numbers[0],
3661
+ right: numbers[1],
3662
+ bottom: numbers[0],
3663
+ left: numbers[1]
3664
+ };
3665
+ if (numbers.length === 3) return {
3666
+ top: numbers[0],
3667
+ right: numbers[1],
3668
+ bottom: numbers[2],
3669
+ left: numbers[1]
3670
+ };
3671
+ if (numbers.length === 4) return {
3672
+ top: numbers[0],
3673
+ right: numbers[1],
3674
+ bottom: numbers[2],
3675
+ left: numbers[3]
3676
+ };
3677
+ if (numbers.length === 1) return {
3678
+ top: numbers[0],
3679
+ right: numbers[0],
3680
+ bottom: numbers[0],
3681
+ left: numbers[0]
3682
+ };
3683
+ }
3684
+ if (typeof value === "string") {
3685
+ const tokens = parseSpacingString(value);
3686
+ if (tokens != null) return normalizeSpacing(tokens);
3687
+ }
3688
+ if (typeof value === "object") {
3689
+ const horizontal = normalizeNumber(value.x, 0);
3690
+ const vertical = normalizeNumber(value.y, 0);
3691
+ return {
3692
+ top: normalizeNumber(value.top, vertical),
3693
+ right: normalizeNumber(value.right, horizontal),
3694
+ bottom: normalizeNumber(value.bottom, vertical),
3695
+ left: normalizeNumber(value.left, horizontal)
3696
+ };
3697
+ }
3698
+ return zeroSpacing();
3699
+ }
3700
+ function zeroSpacing() {
3701
+ return {
3702
+ top: 0,
3703
+ right: 0,
3704
+ bottom: 0,
3705
+ left: 0
3706
+ };
3707
+ }
3501
3708
  function resolveFontFamily(fontInstance, fontFamily) {
3502
3709
  if (typeof fontFamily === "string" && fontFamily.trim().length > 0) return fontFamily.trim();
3710
+ if (typeof fontInstance?.canvasFamily === "string" && fontInstance.canvasFamily.trim().length > 0) return fontInstance.canvasFamily.trim();
3503
3711
  const preferred = fontInstance?.font?.names?.fullName?.en ?? fontInstance?.font?.names?.fontFamily?.en ?? fontInstance?.font?.familyName;
3504
3712
  return typeof preferred === "string" && preferred.trim().length > 0 ? preferred.trim() : "sans-serif";
3505
3713
  }
@@ -3510,6 +3718,42 @@ function formatFontFamily(value) {
3510
3718
  function resolvePretextWhiteSpace(whiteSpace) {
3511
3719
  return whiteSpace === "pre-wrap" ? "pre-wrap" : "normal";
3512
3720
  }
3721
+ function resolveWrapDefaults(wrap) {
3722
+ if (wrap === "char") return {
3723
+ overflowWrap: "break-word",
3724
+ wordBreak: "break-all"
3725
+ };
3726
+ if (wrap === "keep") return {
3727
+ overflowWrap: "normal",
3728
+ wordBreak: "keep-all"
3729
+ };
3730
+ return {
3731
+ overflowWrap: "break-word",
3732
+ wordBreak: "normal"
3733
+ };
3734
+ }
3735
+ function resolveWrapPreset(wordBreak, overflowWrap) {
3736
+ if (wordBreak === "break-all") return "char";
3737
+ if (wordBreak === "keep-all" && overflowWrap === "normal") return "keep";
3738
+ if (wordBreak === "normal" && overflowWrap === "break-word") return "word";
3739
+ return null;
3740
+ }
3741
+ function shouldAttemptPretextLayout(options) {
3742
+ return options.engine === "pretext" && options.wordBreak === "normal";
3743
+ }
3744
+ function canUsePretextLayout(layoutState, options) {
3745
+ return !(options.overflowWrap === "normal" && layoutState.usedOverflowWrapFallbackBreaks);
3746
+ }
3747
+ function resolveRetainedPreparedState(state, options) {
3748
+ if (state.prepared != null && state.preparedWhiteSpace === resolvePretextWhiteSpace(options.whiteSpace) && state.font === options.font) return {
3749
+ prepared: state.prepared,
3750
+ preparedWhiteSpace: state.preparedWhiteSpace
3751
+ };
3752
+ return {
3753
+ prepared: null,
3754
+ preparedWhiteSpace: null
3755
+ };
3756
+ }
3513
3757
  function isHardBreak(prepared, line) {
3514
3758
  return prepared.kinds?.[line.end.segmentIndex] === "hard-break";
3515
3759
  }
@@ -3517,7 +3761,7 @@ function normalizeNativeText(text, whiteSpace) {
3517
3761
  if (whiteSpace === "pre-wrap") return String(text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
3518
3762
  return String(text ?? "").replace(/\s+/gu, " ").trim();
3519
3763
  }
3520
- function tokenizeNativeText(text, whiteSpace, measureWidth) {
3764
+ function tokenizeNativeText(text, whiteSpace, measureWidth, wordBreak) {
3521
3765
  const tokens = [];
3522
3766
  let index = 0;
3523
3767
  while (index < text.length) {
@@ -3536,12 +3780,13 @@ function tokenizeNativeText(text, whiteSpace, measureWidth) {
3536
3780
  if ((char === " " || char === " ") && whiteSpace === "pre-wrap") {
3537
3781
  const start = index;
3538
3782
  while (index < text.length && (text[index] === " " || text[index] === " ")) index += 1;
3783
+ const segment = text.slice(start, index);
3539
3784
  tokens.push({
3540
- text: text.slice(start, index),
3785
+ text: segment,
3541
3786
  type: "space",
3542
3787
  start,
3543
3788
  end: index,
3544
- width: measureWidth(text.slice(start, index))
3789
+ width: measureWidth(segment)
3545
3790
  });
3546
3791
  continue;
3547
3792
  }
@@ -3558,7 +3803,7 @@ function tokenizeNativeText(text, whiteSpace, measureWidth) {
3558
3803
  }
3559
3804
  const nextStop = findNextStop(text, index, whiteSpace);
3560
3805
  const chunk = text.slice(index, nextStop);
3561
- for (const piece of segmentWords(chunk, index, measureWidth)) tokens.push(piece);
3806
+ tokens.push(...segmentChunk(chunk, index, measureWidth, wordBreak));
3562
3807
  index = nextStop;
3563
3808
  }
3564
3809
  return tokens;
@@ -3573,6 +3818,24 @@ function findNextStop(text, start, whiteSpace) {
3573
3818
  }
3574
3819
  return index;
3575
3820
  }
3821
+ function segmentChunk(text, baseOffset, measureWidth, wordBreak) {
3822
+ if (text.length === 0) return [];
3823
+ if (wordBreak === "keep-all") return [{
3824
+ text,
3825
+ type: "text",
3826
+ start: baseOffset,
3827
+ end: baseOffset + text.length,
3828
+ width: measureWidth(text)
3829
+ }];
3830
+ if (wordBreak === "break-all") return splitIntoGraphemePieces({
3831
+ text,
3832
+ type: "text",
3833
+ start: baseOffset,
3834
+ end: baseOffset + text.length,
3835
+ width: measureWidth(text)
3836
+ }, measureWidth);
3837
+ return segmentWords(text, baseOffset, measureWidth);
3838
+ }
3576
3839
  function segmentWords(text, baseOffset, measureWidth) {
3577
3840
  const pieces = [];
3578
3841
  const segmenter = getWordSegmenter();
@@ -3611,6 +3874,13 @@ function createTextMeasurer(fontInstance, options) {
3611
3874
  return width;
3612
3875
  };
3613
3876
  }
3877
+ function createLazyTextMeasurer(fontInstance, options) {
3878
+ let measureWidth = null;
3879
+ return (value) => {
3880
+ if (measureWidth == null) measureWidth = createTextMeasurer(fontInstance, options);
3881
+ return measureWidth(value);
3882
+ };
3883
+ }
3614
3884
  function createOpenTypeMeasurer(fontInstance, options) {
3615
3885
  const cache = /* @__PURE__ */ new Map();
3616
3886
  const widthOptions = {
@@ -3666,6 +3936,15 @@ function trimLastGrapheme(value) {
3666
3936
  function splitPreservingWhitespace(value) {
3667
3937
  return value.split(/(\s+)/u).filter((token) => token.length > 0);
3668
3938
  }
3939
+ function parseSpacingString(value) {
3940
+ const tokens = value.trim().split(/[\s,]+/u).filter(Boolean).map(parseSpacingToken);
3941
+ if (tokens.length >= 1 && tokens.length <= 4 && tokens.every(Number.isFinite)) return tokens;
3942
+ return null;
3943
+ }
3944
+ function parseSpacingToken(value) {
3945
+ const match = value.match(/^(-?\d+(?:\.\d+)?)(px)?$/u);
3946
+ return match ? Number(match[1]) : NaN;
3947
+ }
3669
3948
  function getFontAscender(fontInstance, size) {
3670
3949
  return normalizeNumber(fontInstance?.font?.ascender, fontInstance?.unitsPerEm ?? 1e3) / normalizePositive(fontInstance?.unitsPerEm, 1e3) * size;
3671
3950
  }
@@ -3674,29 +3953,12 @@ function getFontAscender(fontInstance, size) {
3674
3953
  var paParagraph = class paParagraph {
3675
3954
  constructor(fontInstance, text, options, state) {
3676
3955
  this._font = fontInstance;
3677
- this._state = state;
3678
3956
  this.text = text;
3679
- this.options = options;
3680
- this.lines = state.lines.map((line) => ({
3681
- index: line.index,
3682
- text: line.text,
3683
- start: line.start,
3684
- end: line.end,
3685
- x: line.x,
3686
- y: line.y,
3687
- width: line.width,
3688
- baseline: line.baseline,
3689
- height: line.height,
3690
- bbox: { ...line.bbox }
3691
- }));
3692
- this.metrics = {
3693
- ...state.metrics,
3694
- bbox: { ...state.metrics.bbox }
3695
- };
3696
3957
  this._cache = {
3697
3958
  baseShapes: /* @__PURE__ */ new Map(),
3698
3959
  layoutParagraphs: /* @__PURE__ */ new Map()
3699
3960
  };
3961
+ this._applySnapshot(options, state);
3700
3962
  }
3701
3963
  relayout(next = {}) {
3702
3964
  const normalized = normalizeParagraphOptions(this._font, {
@@ -3706,7 +3968,9 @@ var paParagraph = class paParagraph {
3706
3968
  const state = layoutParagraph(this._font, this.text, normalized, {
3707
3969
  prepared: canReusePreparedParagraphState(this._state, this.options, normalized) ? this._state.prepared : null,
3708
3970
  preparedWhiteSpace: this._state.preparedWhiteSpace,
3709
- font: this.options.font
3971
+ font: this.options.font,
3972
+ containerWidth: this._state.containerWidth,
3973
+ containerHeight: this._state.containerHeight
3710
3974
  });
3711
3975
  return new paParagraph(this._font, this.text, normalized, state);
3712
3976
  }
@@ -3718,12 +3982,19 @@ var paParagraph = class paParagraph {
3718
3982
  const fill = options.fill !== false;
3719
3983
  const stroke = options.stroke === true;
3720
3984
  if (!fill && !stroke) return;
3985
+ this._syncLayoutWithContext(ctx);
3721
3986
  ctx.save();
3722
3987
  ctx.font = resolveCanvasFont(this._font, this.options.size, this.options);
3723
3988
  ctx.textAlign = "left";
3724
3989
  ctx.textBaseline = "alphabetic";
3725
3990
  if (options.fillStyle != null) ctx.fillStyle = options.fillStyle;
3726
3991
  if (options.strokeStyle != null) ctx.strokeStyle = options.strokeStyle;
3992
+ if (this.options.overflow === "hidden" && this._state.layoutBox?.clipBox) {
3993
+ const clipBox = this._state.layoutBox.clipBox;
3994
+ ctx.beginPath();
3995
+ ctx.rect(clipBox.x, clipBox.y, clipBox.w, clipBox.h);
3996
+ ctx.clip();
3997
+ }
3727
3998
  this._state.lines.forEach((line) => {
3728
3999
  line.fragments.forEach((fragment) => {
3729
4000
  if (fill) ctx.fillText(fragment.text, fragment.x, line.baseline);
@@ -3751,6 +4022,48 @@ var paParagraph = class paParagraph {
3751
4022
  paragraph._cache.baseShapes.set("base", baseShape);
3752
4023
  return baseShape;
3753
4024
  }
4025
+ _applySnapshot(options, state) {
4026
+ this.options = options;
4027
+ this.layoutEngine = state.layoutEngine;
4028
+ this._state = state;
4029
+ this.lines = state.lines.map((line) => ({
4030
+ index: line.index,
4031
+ text: line.text,
4032
+ start: line.start,
4033
+ end: line.end,
4034
+ x: line.x,
4035
+ y: line.y,
4036
+ width: line.width,
4037
+ baseline: line.baseline,
4038
+ height: line.height,
4039
+ bbox: { ...line.bbox }
4040
+ }));
4041
+ this.metrics = {
4042
+ ...state.metrics,
4043
+ bbox: { ...state.metrics.bbox },
4044
+ contentBox: state.metrics.contentBox ? { ...state.metrics.contentBox } : void 0,
4045
+ paddingBox: state.metrics.paddingBox ? { ...state.metrics.paddingBox } : void 0,
4046
+ marginBox: state.metrics.marginBox ? { ...state.metrics.marginBox } : void 0,
4047
+ clipBox: state.metrics.clipBox ? { ...state.metrics.clipBox } : void 0
4048
+ };
4049
+ this._cache.baseShapes.clear();
4050
+ this._cache.layoutParagraphs.clear();
4051
+ }
4052
+ _syncLayoutWithContext(ctx) {
4053
+ const canvas = ctx?.canvas;
4054
+ if (!canvas || this.options.width != null) return;
4055
+ const containerWidth = Number.isFinite(canvas.width) ? canvas.width : null;
4056
+ const containerHeight = Number.isFinite(canvas.height) ? canvas.height : null;
4057
+ if (containerWidth === this._state.containerWidth && containerHeight === this._state.containerHeight) return;
4058
+ const state = layoutParagraph(this._font, this.text, this.options, {
4059
+ prepared: canReusePreparedParagraphState(this._state, this.options, this.options) ? this._state.prepared : null,
4060
+ preparedWhiteSpace: this._state.preparedWhiteSpace,
4061
+ font: this.options.font,
4062
+ containerWidth,
4063
+ containerHeight
4064
+ });
4065
+ this._applySnapshot(this.options, state);
4066
+ }
3754
4067
  _resolveParagraph(layout) {
3755
4068
  if (layout === "current") return this;
3756
4069
  if (layout === "native") return this._getLayoutParagraph("native");
@@ -3824,17 +4137,30 @@ function normalizeParagraphPointOptions(options = {}) {
3824
4137
  }
3825
4138
  //#endregion
3826
4139
  //#region src/paFont/paFont.js
4140
+ var browserFontRegistrationId = 0;
3827
4141
  var paFont = class paFont {
3828
4142
  constructor(font) {
3829
4143
  this.font = font;
3830
4144
  this.unitsPerEm = font.unitsPerEm ?? 1e3;
4145
+ this.family = resolveLoadedFontFamily(font);
4146
+ this.canvasFamily = this.family;
3831
4147
  this._glyphTopologyCache = /* @__PURE__ */ new Map();
3832
4148
  this._glyphFlatCache = /* @__PURE__ */ new Map();
3833
4149
  }
3834
4150
  static async load(source, options = {}) {
3835
4151
  const opts = normalizeLoadOptions(options);
3836
- if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) return new paFont(parse(toArrayBuffer(source)));
3837
- if (typeof window !== "undefined") return new paFont(parse(await fetchFontBytes(resolveBrowserFontSource(source, opts.base))));
4152
+ if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
4153
+ const bytes = toArrayBuffer(source);
4154
+ const instance = new paFont(parse(bytes));
4155
+ await instance._registerBrowserFont(bytes);
4156
+ return instance;
4157
+ }
4158
+ if (typeof window !== "undefined") {
4159
+ const bytes = await fetchFontBytes(resolveBrowserFontSource(source, opts.base));
4160
+ const instance = new paFont(parse(bytes));
4161
+ await instance._registerBrowserFont(bytes);
4162
+ return instance;
4163
+ }
3838
4164
  const { target, loadOptions } = await resolveNodeFontSource(source, opts.base);
3839
4165
  return new paFont(await load(target, void 0, loadOptions));
3840
4166
  }
@@ -3869,6 +4195,18 @@ var paFont = class paFont {
3869
4195
  paragraph(value, options = {}) {
3870
4196
  return createParagraph(this, String(value ?? ""), options);
3871
4197
  }
4198
+ async _registerBrowserFont(source) {
4199
+ if (typeof window === "undefined" || typeof FontFace === "undefined" || typeof document === "undefined" || document.fonts == null) return;
4200
+ try {
4201
+ const family = createBrowserFontFamily(this.family);
4202
+ const face = new FontFace(family, source);
4203
+ await face.load();
4204
+ document.fonts.add(face);
4205
+ this.canvasFamily = family;
4206
+ } catch {
4207
+ this.canvasFamily = this.family;
4208
+ }
4209
+ }
3872
4210
  _getGlyphTopology(glyph) {
3873
4211
  const key = String(glyph.index);
3874
4212
  if (!this._glyphTopologyCache.has(key)) this._glyphTopologyCache.set(key, buildGlyphTopology(glyph, this.unitsPerEm));
@@ -3953,6 +4291,13 @@ function readSignature(bytes) {
3953
4291
  function isHtmlSignature(signature) {
3954
4292
  return signature === "<!do" || signature === "<htm";
3955
4293
  }
4294
+ function resolveLoadedFontFamily(font) {
4295
+ return font?.names?.fontFamily?.en ?? font?.names?.preferredFamily?.en ?? font?.names?.fullName?.en ?? font?.familyName ?? "paFont";
4296
+ }
4297
+ function createBrowserFontFamily(family) {
4298
+ browserFontRegistrationId += 1;
4299
+ return `${String(family ?? "paFont").trim().replace(/\s+/g, " ").replace(/[^a-zA-Z0-9 _-]/g, "") || "paFont"}__pa_${browserFontRegistrationId}`;
4300
+ }
3956
4301
  //#endregion
3957
4302
  export { PAShape, paFont as default, paFont, paParagraph };
3958
4303