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