feat(core): segment-fit line-breaking seam + pretext engine#482
feat(core): segment-fit line-breaking seam + pretext engine#482arthrod wants to merge 2 commits into
Conversation
Add a swappable SegmentFitEngine seam to the layout-engine measurement path, gated behind a new `segmentFitLineBreaking` measurement feature flag. A registered engine fits plain-text runs from prepared segment widths instead of the word-walk's per-call word re-measurement and `findMaxFittingLength` slice-probe binary search. With the flag off or no engine installed (the default), the block is skipped and the legacy walk runs byte-identically. - segmentFit.ts: swappable engine registry + walk driver + style allowlist; core imports no concrete engine. - featureFlags.ts: add segmentFitLineBreaking flag (default off) + accessor, preserving the existing workerFontMetrics flag. - measureParagraph.ts: conservative call-site admission gate; declines automatic-hyphenation and protected-cross-run-glue runs. - cache.ts: clearAllCaches drops engine-prepared state on font-env change. - segmentFit.test.ts: engine-consulted parity + glue-run guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…enchmarks New private (unpublished) folio plugin providing a pretext-backed engine for the folio-core segment-fit seam, with a frozen parity suite and benchmark harnesses. - pretextEngine.ts: prepare-once segmentation + measurement, pure-arithmetic line fitting; declines offset-unsafe (CR/FF) text. - pretextParity.test.ts: exact line-break/width parity vs the legacy walk on spaced/CJK/overlong/trailing-space text; frozen canvas-call profile. - bench/: deterministic per-paragraph micro-benchmark + real-browser corpus harness, with RESULTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thank you for your pull request! Before we can merge it, we need you to sign the Contributor License Agreement. To sign, please post the following comment on this PR (exactly as written):
I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
📝 WalkthroughWalkthroughAdds a feature-gated, pluggable segment-fit path to paragraph measurement, implements a pretext-backed bridge engine with caching, and adds parity tests, benchmarks, package metadata, documentation, and release notes. ChangesSegment-fit measurement
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ParagraphMeasurement
participant SegmentFitWalk
participant PretextEngine
participant PreparedCache
ParagraphMeasurement->>SegmentFitWalk: provide text and available line width
SegmentFitWalk->>PretextEngine: prepare text and CSS font
PretextEngine->>PreparedCache: read or store prepared handle
SegmentFitWalk->>PretextEngine: fit next line
PretextEngine-->>SegmentFitWalk: return UTF-16 end offset and width
SegmentFitWalk-->>ParagraphMeasurement: commit fitted segment or fall back
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
packages/premirror-bridge/package.json (1)
15-18: 🔒 Security & Privacy | 🔵 TrivialConfirm
@chenglou/pretext@0.0.3is intentionally pinned rather than stale.The library has since added/changed CJK
wordBreak: 'keep-all'handling, punctuation attachment, and soft-hyphen/overlong hyphen behaviour through newer releases. If the frozen parity coverage is meant to use the latest stable layout behaviour, record or upgrade the pinned version accordingly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/premirror-bridge/package.json` around lines 15 - 18, Review the `@chenglou/pretext` dependency in packages/premirror-bridge/package.json and confirm whether version 0.0.3 is intentionally pinned. If frozen parity coverage should use the latest stable layout behavior, upgrade the dependency to the appropriate newer release; otherwise document the pin’s rationale near the dependency or in the project’s established dependency documentation.packages/premirror-bridge/bench/.gitignore (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winGitignore doesn't cover
measure-browser.mjs's default outputs.
measure-browser.mjsdefaults to writing./measure-browser.jsonland./measure-browser.summary.jsoninto this samebench/directory, but onlymeasure-engine-*.jsonis ignored here. If no broader.gitignoreelsewhere in the package already covers these, add patterns for them too.📝 Proposed fix
measure-engine-*.json +measure-browser.jsonl +measure-browser.summary.json🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/premirror-bridge/bench/.gitignore` at line 1, Update the bench .gitignore alongside measure-engine-*.json to ignore measure-browser.mjs’s default output files: measure-browser.jsonl and measure-browser.summary.json, without changing the existing pattern.packages/premirror-bridge/src/index.ts (1)
19-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
index.tsis a barrel-style re-export.This file only forwards three named exports from
./pretextEngine, so it functions as the package's public entrypoint rather than an internal barrel — but the filename still collides with the repo rule againstindex.tsbarrels. If package.json'smain/exportscan point at a differently-named file (e.g.mod.ts), consider renaming to keep the convention consistent repo-wide.As per coding guidelines, "Do not create barrel files such as
index.ts; import from explicit paths."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/premirror-bridge/src/index.ts` at line 19, Rename the public entrypoint currently defined in index.ts to a non-index filename such as mod.ts, preserve the pretextSegmentFitEngine, clearPreparedCache, and preparedCacheSize re-exports, and update package.json main/exports references and any imports that target index.ts to use the renamed entrypoint.Source: Coding guidelines
packages/premirror-bridge/src/pretextEngine.ts (1)
32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUnnecessary
ascasts and a duplicated type instead of consuming@chenglou/pretext's own exported types.
@chenglou/pretextships full type declarations and exports bothLayoutCursor({ segmentIndex, graphemeIndex }, identical to the local type at line 32) andPreparedTextWithSegments(the "richer manual-layout handle" that retains segment strings). This file instead:
- Redefines
LayoutCursorlocally (line 32) rather than importing it.- Casts through
unknownto reach.segments(line 64) rather than typingpreparedasPreparedTextWithSegmentsand accessing the field directly.- Casts
prepared/cursorback out ofunknowninfitLine(lines 155-156) with no// SAFETY:comment explaining why the cast is sound.Importing the library's own types would remove two of these casts entirely and trace the remaining one back to its actual source (the
SegmentFitEngineinterface's deliberateunknownboundary), where a// SAFETY:comment is genuinely warranted.As per coding guidelines, "Avoid unnecessary
ascasts; narrow with type guards orinchecks, and add a// SAFETY:comment when an unavoidable cast is sound," "Trace type mismatches to their source instead of casting at the consumer," and "Reuse utility types from installed libraries instead of defining equivalent custom types."Also applies to: 62-77, 154-166
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/premirror-bridge/src/pretextEngine.ts` at line 32, Replace the local LayoutCursor definition with the exported LayoutCursor and PreparedTextWithSegments types from `@chenglou/pretext`. Type the prepared-text value as PreparedTextWithSegments so segments can be accessed directly, and update fitLine to preserve only the cast required by the SegmentFitEngine unknown boundary, documenting that cast with a // SAFETY: comment; remove the other unknown-based casts.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/premirror-bridge/src/pretextEngine.ts`:
- Around line 26-30: Upgrade the fixed `@chenglou/pretext` dependency used by
pretextEngine.ts from 0.0.3 to at least 0.0.5, preferably the current safe
release, and update the lockfile accordingly. In the bridge path around
prepareWithSegments, add an application-level length cap or rejection for
pathological repeated-character runs while preserving existing CR/FF
normalization behavior.
---
Nitpick comments:
In `@packages/premirror-bridge/bench/.gitignore`:
- Line 1: Update the bench .gitignore alongside measure-engine-*.json to ignore
measure-browser.mjs’s default output files: measure-browser.jsonl and
measure-browser.summary.json, without changing the existing pattern.
In `@packages/premirror-bridge/package.json`:
- Around line 15-18: Review the `@chenglou/pretext` dependency in
packages/premirror-bridge/package.json and confirm whether version 0.0.3 is
intentionally pinned. If frozen parity coverage should use the latest stable
layout behavior, upgrade the dependency to the appropriate newer release;
otherwise document the pin’s rationale near the dependency or in the project’s
established dependency documentation.
In `@packages/premirror-bridge/src/index.ts`:
- Line 19: Rename the public entrypoint currently defined in index.ts to a
non-index filename such as mod.ts, preserve the pretextSegmentFitEngine,
clearPreparedCache, and preparedCacheSize re-exports, and update package.json
main/exports references and any imports that target index.ts to use the renamed
entrypoint.
In `@packages/premirror-bridge/src/pretextEngine.ts`:
- Line 32: Replace the local LayoutCursor definition with the exported
LayoutCursor and PreparedTextWithSegments types from `@chenglou/pretext`. Type the
prepared-text value as PreparedTextWithSegments so segments can be accessed
directly, and update fitLine to preserve only the cast required by the
SegmentFitEngine unknown boundary, documenting that cast with a // SAFETY:
comment; remove the other unknown-based casts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 190bd404-d42d-4caf-b530-4be8ffd43ebf
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
.changeset/segment-fit-seam.mdapi-reports/core/layout-bridge-engine-measuring.api.mdpackages/core/src/layout-engine/measure/cache.tspackages/core/src/layout-engine/measure/featureFlags.tspackages/core/src/layout-engine/measure/measureParagraph.tspackages/core/src/layout-engine/measure/segmentFit.test.tspackages/core/src/layout-engine/measure/segmentFit.tspackages/premirror-bridge/.gitignorepackages/premirror-bridge/NOTICEpackages/premirror-bridge/README.mdpackages/premirror-bridge/bench/.gitignorepackages/premirror-bridge/bench/RESULTS.mdpackages/premirror-bridge/bench/measure-browser.mjspackages/premirror-bridge/bench/measure-engine.mjspackages/premirror-bridge/package.jsonpackages/premirror-bridge/src/index.tspackages/premirror-bridge/src/pretextEngine.tspackages/premirror-bridge/src/pretextParity.test.tspackages/premirror-bridge/tsconfig.json
📜 Review details
⚠️ CI failures not shown inline (2)
GitHub Actions: CLA Assistant / 0_cla _ cla.txt: feat(core): segment-fit line-breaking seam + pretext engine
Conclusion: failure
##[group]Run contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08
with:
path-to-signatures: signatures/cla.json
path-to-document: https://github.com/stella/cla/blob/main/CLA.md
branch: cla-signatures
remote-organization-name: stella
remote-repository-name: cla
allowlist: dependabot[bot],renovate[bot],github-actions[bot],google-labs-jules[bot],cursoragent
custom-notsigned-prcomment: Thank you for your pull request! Before we can merge it, we need you to sign the [Contributor License Agreement](https://github.com/stella/cla/blob/main/CLA.md).
To sign, please post the following comment on this PR (exactly as written):
> I have read the CLA Document and I hereby sign the CLA
custom-allsigned-prcomment: All contributors have signed the CLA.
custom-pr-sign-comment: I have read the CLA Document and I hereby sign the CLA
use-dco-flag: false
lock-pullrequest-aftermerge: true
suggest-recheck: true
env:
GITHUB_***REDACTED***
PERSONAL_ACCESS_***REDACTED***
##[endgroup]
CLA Assistant GitHub Action bot has started the process
(node:1929) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:1929) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
##[error]Committers of Pull Request number 482 have to sign the CLA 📝
GitHub Actions: CLA Assistant / cla _ cla: feat(core): segment-fit line-breaking seam + pretext engine
Conclusion: failure
##[group]Run contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08
with:
path-to-signatures: signatures/cla.json
path-to-document: https://github.com/stella/cla/blob/main/CLA.md
branch: cla-signatures
remote-organization-name: stella
remote-repository-name: cla
allowlist: dependabot[bot],renovate[bot],github-actions[bot],google-labs-jules[bot],cursoragent
custom-notsigned-prcomment: Thank you for your pull request! Before we can merge it, we need you to sign the [Contributor License Agreement](https://github.com/stella/cla/blob/main/CLA.md).
To sign, please post the following comment on this PR (exactly as written):
> I have read the CLA Document and I hereby sign the CLA
custom-allsigned-prcomment: All contributors have signed the CLA.
custom-pr-sign-comment: I have read the CLA Document and I hereby sign the CLA
use-dco-flag: false
lock-pullrequest-aftermerge: true
suggest-recheck: true
env:
GITHUB_***REDACTED***
PERSONAL_ACCESS_***REDACTED***
##[endgroup]
CLA Assistant GitHub Action bot has started the process
(node:1929) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:1929) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
##[error]Committers of Pull Request number 482 have to sign the CLA 📝
🧰 Additional context used
📓 Path-based instructions (5)
**/*
📄 CodeRabbit inference engine (AGENTS.md)
Preserve
NOTICE.md,LICENSE, and Eigenpal/docx-editor attribution comments verbatim.
Files:
packages/premirror-bridge/tsconfig.jsonpackages/premirror-bridge/package.jsonapi-reports/core/layout-bridge-engine-measuring.api.mdpackages/premirror-bridge/README.mdpackages/premirror-bridge/src/index.tspackages/premirror-bridge/NOTICEpackages/core/src/layout-engine/measure/cache.tspackages/premirror-bridge/src/pretextParity.test.tspackages/premirror-bridge/bench/measure-browser.mjspackages/core/src/layout-engine/measure/featureFlags.tspackages/core/src/layout-engine/measure/segmentFit.test.tspackages/premirror-bridge/bench/RESULTS.mdpackages/core/src/layout-engine/measure/measureParagraph.tspackages/core/src/layout-engine/measure/segmentFit.tspackages/premirror-bridge/bench/measure-engine.mjspackages/premirror-bridge/src/pretextEngine.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Keeppackages/coreframework-neutral and React-free; never import React, React DOM, or React-package types into core.
Use discriminated unions for mutually exclusive states; avoid boolean flag sets and invalid optional-field combinations.
Prefer branded types, discriminated unions, exhaustive checks, and other TypeScript techniques that make bugs structurally impossible.
Do not use TypeScript enums; useas constobjects or union types.
Avoid unnecessaryascasts; narrow with type guards orinchecks, and add a// SAFETY:comment when an unavoidable cast is sound.
Trace type mismatches to their source instead of casting at the consumer.
Do not annotate values the compiler already infers or pass explicit type arguments to inference-driven hooks and API calls.
Useas const satisfies Tto validate large object literals without widening them.
Use.at(0)when an element may be absent; use[0]only when existence is established or documented with// SAFETY:.
Prefer arrow functions over function expressions.
Destructure function parameters when the intermediate object is not reused.
Prefer discriminated-union checks such asobj.type === "x"; useinonly when no discriminator exists.
Use an options/args/params object for three or more function arguments or interchangeable positional arguments; reservePropsfor React props.
Reuse utility types from installed libraries instead of defining equivalent custom types.
Keep helper-local type aliases immediately above the helper they describe.
Hoist noisy return types into nearby aliases, but keep simple return types inline.
Avoid expensive type instantiation in generic hot paths; prefer narrowing and keep unused large types out of inferred returns.
Shared modules must not combine reusable utilities with module-level side-effecting singletons; split them into separate files.
Never import test-only types into production code; use structural constraints instead.
Defer eager module initializatio...
Files:
packages/premirror-bridge/src/index.tspackages/core/src/layout-engine/measure/cache.tspackages/premirror-bridge/src/pretextParity.test.tspackages/core/src/layout-engine/measure/featureFlags.tspackages/core/src/layout-engine/measure/segmentFit.test.tspackages/core/src/layout-engine/measure/measureParagraph.tspackages/core/src/layout-engine/measure/segmentFit.tspackages/premirror-bridge/src/pretextEngine.ts
**/*.{tsx,ts}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{tsx,ts}: Do not create single-use mutation hooks; inline the API call and useResult.tryPromisefor retries.
PreferuseRouteContextfor data supplied by parent loaders over issuing a separate query.
Returnvoidfrom route loaders that only prime a TanStack Query cache.
UseuseSuspenseQueryonly for preloaded route/page content or content inside an explicit local Suspense boundary; useuseQueryin shared chrome.
UseselectwithuseParams,useSearch, anduseRouteContextto subscribe only to needed fields.
Passfromto route hooks and links, orstrict: falsefor shared chrome spanning routes.
UseuseDebouncedCallbackfromuse-debounceinstead of manually implementing debounce with refs and timeout cleanup.
Files:
packages/premirror-bridge/src/index.tspackages/core/src/layout-engine/measure/cache.tspackages/premirror-bridge/src/pretextParity.test.tspackages/core/src/layout-engine/measure/featureFlags.tspackages/core/src/layout-engine/measure/segmentFit.test.tspackages/core/src/layout-engine/measure/measureParagraph.tspackages/core/src/layout-engine/measure/segmentFit.tspackages/premirror-bridge/src/pretextEngine.ts
packages/core/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
packages/core/**/*.ts: Keep shared editor behavior and UI logic in framework-neutral core managers; React hooks must remain thin bindings.
Express fidelity fixes as reusable OOXML or layout invariants, keep normalization/layout inputs immutable and idempotent, and separate parser, model, measurement, pagination, and painting responsibilities.
Use explicit coordinate-space types and discriminated state machines for layout; avoid related booleans, optional combinations, and mutable local flags.
Files:
packages/core/src/layout-engine/measure/cache.tspackages/core/src/layout-engine/measure/featureFlags.tspackages/core/src/layout-engine/measure/segmentFit.test.tspackages/core/src/layout-engine/measure/measureParagraph.tspackages/core/src/layout-engine/measure/segmentFit.ts
packages/core/**/*.test.ts
📄 CodeRabbit inference engine (AGENTS.md)
Add minimal synthetic regression tests for fidelity invariants; do not encode fixture identity, source metadata, document text, or corpus details in tests.
Files:
packages/core/src/layout-engine/measure/segmentFit.test.ts
🪛 LanguageTool
packages/premirror-bridge/README.md
[uncategorized] ~3-~3: Although a hyphen is possible, it is not necessary in a compound modifier in which the first word is an adverb that ends in ‘ly’.
Context: # @stll/premirror-bridge A separately-versioned folio plugin: a [@chenglou/pretext]...
(HYPHENATED_LY_ADVERB_ADJECTIVE)
[uncategorized] ~31-~31: Possible missing comma found.
Context: ...in (workspace vs. release) Inside this monorepo the package resolves @stll/folio-core...
(AI_HYDRA_LEO_MISSING_COMMA)
🔇 Additional comments (18)
packages/core/src/layout-engine/measure/segmentFit.ts (3)
163-204: 🎯 Functional CorrectnessWalk driver logic is sound in isolation.
Traced the loop for infinite-loop risk (wrap not resetting
lineHasContent(), engine returning a non-advancingendChar, etc.) — all paths correctly fall through to the legacy walk or terminate. The one real correctness gap involving this function is in the caller (seemeasureParagraph.ts's admission gate comment).
101-124: LGTM!
137-144: LGTM!api-reports/core/layout-bridge-engine-measuring.api.md (1)
83-83: LGTM!packages/core/src/layout-engine/measure/featureFlags.ts (1)
27-33: LGTM!Also applies to: 50-57
packages/core/src/layout-engine/measure/cache.ts (1)
12-12: LGTM!Also applies to: 450-458
packages/core/src/layout-engine/measure/segmentFit.test.ts (1)
1-254: LGTM!packages/premirror-bridge/tsconfig.json (1)
1-21: LGTM!packages/premirror-bridge/.gitignore (1)
1-2: LGTM!packages/core/src/layout-engine/measure/measureParagraph.ts (1)
1826-1864: 🎯 Functional CorrectnessNo change needed for cross-run hyphenation resumes.
In the current flow, automatic hyphenation and suppression are resolved from the same paragraph attributes for every run in the paragraph, so an enabled cross-run hyphenation word cannot land inside a later run whose own effective hyphenation state is disabled.
> Likely an incorrect or invalid review comment.packages/premirror-bridge/src/pretextEngine.ts (1)
43-52: LGTM! Cache-key separator, LRU eviction, and lazy grapheme-segmenter singleton are correctly implemented and covered by the parity test's cache-collision check.Also applies to: 127-152
packages/premirror-bridge/src/pretextParity.test.ts (1)
1-182: LGTM! Thorough parity and cache-key coverage matching the documented divergences and benchmark claims.packages/premirror-bridge/bench/measure-engine.mjs (1)
1-330: LGTM!packages/premirror-bridge/bench/measure-browser.mjs (1)
1-242: LGTM! Solid concurrency handling, settle-polling, and paired-metric aggregation for a benchmark harness. (Noted separately: its default output filenames aren't covered bybench/.gitignore.)packages/premirror-bridge/bench/RESULTS.md (1)
1-98: LGTM! Numbers are internally consistent and match the PR objectives..changeset/segment-fit-seam.md (1)
1-14: LGTM!packages/premirror-bridge/README.md (1)
1-48: LGTM!packages/premirror-bridge/NOTICE (1)
1-22: LGTM!
| import { layoutNextLine, prepareWithSegments } from "@chenglou/pretext"; | ||
| import type { | ||
| SegmentFitEngine, | ||
| SegmentFitLine, | ||
| } from "@stll/folio-core/layout-engine/measure/segmentFit"; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate files =="
git ls-files | rg '(^|/)pretextEngine\.ts$|(^|/)package\.json$|(^|/)pnpm-lock\.yaml$|(^|/)yarn\.lock$|(^|/)package-lock\.json$' || true
echo
echo "== pretextEngine outline =="
ast-grep outline packages/premirror-bridge/src/pretextEngine.ts || true
echo
echo "== Relevant pretextEngine sections =="
sed -n '1,220p' packages/premirror-bridge/src/pretextEngine.ts
echo
echo "== Dependency pins mentioning pretext/pretextEngine core =="
rg -n '`@chenglou/pretext`|pretextEngine|layoutNextLine|prepareWithSegments|LayoutCursor|PreparedTextWithSegments|supportsText' packages -S
echo
echo "== Package manifests =="
for f in $(git ls-files '**/package.json'); do
if rg -q '"`@chenglou/pretext`"|"`@stll/folio-core`"|pretextEngine' "$f"; then
echo "--- $f"
rg -n '"name"|"version"|"`@chenglou/pretext`"|"`@stll/folio-core`"|"`@chenglou/pretext`"' "$f" -A2 -B2 || true
fi
done
echo
echo "== Lockfile entries =="
if [ -f pnpm-lock.yaml ]; then
rg -n '`@chenglou/pretext`|/pretext@|/pretext:' pnpm-lock.yaml -A5 -B2 || true
fi
if [ -f package-lock.json ]; then
node - <<'JS'
const fs = require('fs');
const lock = JSON.parse(fs.readFileSync('package-lock.json','utf8'));
const find = (obj, k, depth=0) => {
if(typeof obj !== 'object' || !obj || depth>4)return;
for(const key in obj){
if(key === k || key.endsWith('/'+k)||key.includes('/'+k)){
console.log(key, obj[key].version ?? '');
}
find(obj[key],k,depth+1);
}
}
find(lock, '`@chenglou/pretext`');
JS
fi
if [ -f yarn.lock ]; then
rg -n '`@chenglou/pretext`|pretext@' yarn.lock -A4 -B2 || true
fiRepository: stella/folio
Length of output: 13645
🌐 Web query:
GHSA-5478-66c3-rhxr @chenglou/pretext isRepeatedSingleCharRun 0.0.3 0.0.5
💡 Result:
The vulnerability GHSA-5478-66c3-rhxr refers to an algorithmic complexity (Denial of Service) flaw in the @chenglou/pretext library [1][2]. The issue stems from the isRepeatedSingleCharRun function in src/analysis.ts, which performed an O(n) full-string scan during text analysis [1][3]. When the library's buildMergedSegmentation function processed long sequences of identical punctuation characters, it called this function repeatedly as it merged segments, leading to O(n^2) total time complexity [1][4]. An attacker could exploit this by providing a large input of repeated punctuation (e.g., 80KB of identical characters), which could block the main thread for an extended period [1][5]. This vulnerability affects versions 0.0.4 and earlier of @chenglou/pretext [6]. It was addressed and patched in version 0.0.5 by replacing the O(n) scan with an O(1) constant-time check [7][3]. Users of the package are advised to upgrade to version 0.0.5 or higher to mitigate this risk [8][6].
Citations:
- 1: GHSA-5478-66c3-rhxr
- 2: https://mondoo.com/vulnerability-intelligence/vulnerability/GHSA-5478-66c3-rhxr
- 3: https://cvereports.com/reports/GHSA-5478-66C3-RHXR
- 4: GHSA-5478-66c3-rhxr
- 5: https://advisories.gitlab.com/npm/@chenglou/pretext/GHSA-5478-66c3-rhxr/
- 6: https://dependabot.ecosyste.ms/advisories/GHSA-5478-66c3-rhxr
- 7: https://www.endorlabs.com/vulnerability/ghsa-5478-66c3-rhxr
- 8: https://security.snyk.io/vuln/SNYK-JS-CHENGLOUPRETEXT-16321421
🌐 Web query:
NPM @chenglou/pretext versions 0.0.3 0.0.5 0.0.8
💡 Result:
The npm package @chenglou/pretext is a JavaScript/TypeScript library for multiline text measurement and layout [1][2]. It has released multiple versions since its initial publication in March 2026 [3][4]. The versions you requested were released on the following dates: - version 0.0.3 was released on March 29, 2026 [4][5]. - version 0.0.5 was released on April 9, 2026 [4][5][6]. - version 0.0.8 was released on June 12, 2026 [3][7]. As of July 24, 2026, there are 9 versions in total (0.0.0 through 0.0.8) [3][2].
Citations:
- 1: https://github.com/chenglou/pretext/blob/796b4691/README.md
- 2: https://www.npmjs.com/package/@chenglou/pretext?activeTab=code
- 3: https://registry.npmjs.org/@chenglou/pretext
- 4: https://github.com/chenglou/pretext/blob/main/CHANGELOG.md
- 5: https://npmx.dev/package-changelog/@chenglou/pretext/v/0.0.8
- 6: chenglou/pretext@v0.0.4...v0.0.5
- 7: chenglou/pretext@a79a6a5
🌐 Web query:
ghsa npm @chenglou/pretext GHSA-5478-66c3-rhxr fixed versions
💡 Result:
The vulnerability GHSA-5478-66c3-rhxr, which involves an algorithmic complexity issue (Denial of Service) in the text analysis phase of the @chenglou/pretext npm package, was addressed in version 0.0.5 [1][2][3]. Users of the package are advised to upgrade to version 0.0.5 or higher to mitigate the vulnerability, which affected all versions up to and including 0.0.4 [1][4][3]. The issue stemmed from an O(n²) performance degradation in the isRepeatedSingleCharRun function when processing large inputs of repeated identical punctuation characters [2][5][3].
Citations:
- 1: GHSA-5478-66c3-rhxr
- 2: GHSA-5478-66c3-rhxr
- 3: https://dependabot.ecosyste.ms/advisories/GHSA-5478-66c3-rhxr
- 4: https://security.snyk.io/vuln/SNYK-JS-CHENGLOUPRETEXT-16321421
- 5: https://advisories.gitlab.com/npm/@chenglou/pretext/GHSA-5478-66c3-rhxr/
Upgrade the fixed @chenglou/pretext dependency
@chenglou/pretext@0.0.3 is affected by GHSA-5478-66c3-rhxr: long runs of repeated punctuation can trigger O(n²) analysis during prepareWithSegments(), blocking the main thread. This bridge accepts arbitrary run text, so length-normalisation needs to cover this input shape as well as CR/FF. Update the pin to ≥0.0.5, ideally current, and add an application-level rejection/cap for pathological repeated-character input if appropriate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/premirror-bridge/src/pretextEngine.ts` around lines 26 - 30, Upgrade
the fixed `@chenglou/pretext` dependency used by pretextEngine.ts from 0.0.3 to at
least 0.0.5, preferably the current safe release, and update the lockfile
accordingly. In the bridge path around prepareWithSegments, add an
application-level length cap or rejection for pathological repeated-character
runs while preserving existing CR/FF normalization behavior.
|
Thanks again for putting this together. I think the general "prepare once logic" is promising. However I’m not yet able to reproduce the headline real-document result:
There seems to also be a few small correctness reproductions around partially occupied lines, kerning, and font-cache. I will write this separately once \i understand the (micro)benchmark results |
|
oh and could you please @arthrod sign the CLA #482 (comment) |
|
Thanks again for pointing out this direction. I implemented the core idea in #483 using a smaller Folio-native change. It reduced Canvas measurements by about 41% in focused prose and 32.5% across a 20-document DOCX sample, with identical pagination. Once it lands, please rebase and test whether the remaining segment-fit work can push this further. Thanks again! |
|
Thanks! Lemme look into this! |
|
I have read the CLA Document and I hereby sign the CLA |
Summary
Adds a swappable segment-fit line-breaking seam to the layout-engine
measurement path, plus a
@chenglou/pretext-backed engine that implements it.Plain-text line breaking today runs a word-walk that re-measures words per call
and, for overlong tokens, binary-searches with
findMaxFittingLengthsliceprobes — each probe a fresh canvas
measureText. The seam lets a registeredengine fit lines from segment widths prepared once per (font, text), turning
line fitting into pure arithmetic and collapsing the redundant canvas calls.
Two commits:
feat(core)— the seam in@stll/folio-core: a swappableSegmentFitEngineregistry + walk driver + a conservative style allowlist,gated behind a new
segmentFitLineBreakingmeasurement feature flag. Coreimports no concrete engine. With the flag off or no engine installed (the
default), the block is skipped and the legacy walk runs byte-identically.
feat(premirror-bridge)— a new private (unpublished) plugin packageproviding a
@chenglou/pretext-backed engine, a frozen parity suite, and thebenchmark harnesses. It depends only on published
@stll/folio-coreplus itsown third-party deps.
Correctness
Line-break and width parity vs the legacy walk is exact on spaced text,
trailing-space edges, overlong tokens, and space-less CJK, and is frozen in
pretextParity.test.ts. The engine declines offset-unsafe text (CR/FF) andmeasures legacy-identically there.
On a 500-document corpus of real
.docx(2 KB – 884 KB), 490 / 492 documentspaginate to an identical page count with the engine on vs off. 2 large
documents shifted by one page — the engine's documented break-rule divergence
(trailing whitespace hangs past the line edge; CJK/Thai breaks between
characters), both OOXML/CSS-defensible but not byte-identical to the legacy walk
on those two documents. This is the one behavior change to weigh.
Benchmarks
Full methodology and numbers in
packages/premirror-bridge/bench/RESULTS.md.Metric that matters: canvas
measureTextcalls (real-canvasmeasureText/font shaping is the expensive op the seam avoids).
Deterministic (fake canvas, frozen in the parity suite):
Real browser, real fonts, cold cache, 492 real
.docx:measureTextcalls< 5 KBdocsWhere the engine engages it is a large win (80–98% fewer calls on the paragraphs
the walk re-measures word-by-word or slice-probes); at steady state it is a
no-op (folio's width cache already covers repeats); when it does not help its
cost is +1 call. The aggregate real-document win is modest because most real
paragraphs are short or otherwise ineligible, but it is real, concentrated, and
never negative beyond noise.
Recommendation
Enable it by default. Steady-state parity, large first-pass/overlong wins where
it engages, +1-call worst case elsewhere, and exact break/width parity on the
common cases. The two one-page pagination shifts are the known trade-off; if
byte-identical pagination is a hard requirement it can ship flag-off and be
opted into per host.
Test plan
bun run typecheck— all packages cleanbun run build— cleanbun run lint— cleanbun teston the seam + parity suites — 21 passbun run changeset:check— OK (folio-core minor)bun run api:check— updated (segmentFitLineBreaking?added to the publicFolioMeasurementFeatureFlags)bun bench/measure-engine.mjsreproduces the deterministic tableAttribution
The segment-fit design (prepare-once segmentation + measurement, pure-math line
fitting) is
@chenglou/pretext's (MIT); the bridge belongs to the premirrorline (
samwillis/premirror, MIT © Sam Willis). Seepackages/premirror-bridge/NOTICE.CC on behalf of arthrod
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests