CJK 段落书写器架构
@typeset/typeset 的长期目标是实现接近 Adobe InDesign “CJK Paragraph Composer” 的排版效果。这个目标不是只在字符之间插入固定空隙,而是让引擎能够在整段范围内综合判断 character classification、Kinsoku、Mojikumi、Yakumono、line-edge spacing、孤字不成行、justification 和最终 positioning。
这份文档描述目标架构和工作流程。它不表示所有能力都已经实现,而是规定这些能力应该发生在哪个环节,避免后续功能堆叠时互相穿透边界。
术语约定
正文中专业术语优先保留英语或日语,下面给出中文含义,方便阅读时对照:
CJK: 中文、日文、韩文相关排版系统。Composer: 书写器,负责决定段落如何断行、调整间距、形成最终行。Paragraph Composer: 段落书写器,在整段范围内寻找整体质量更好的断行方案。Single-line Composer: 单行书写器,逐行决定断行,不回看整段。pipeline: 排版处理流程。profile: 可配置规则集。grapheme: 用户感知的字符单元,例如组合音标、emoji sequence。实现中是带 UTF-16 source range 的结构化对象。token: 排版和测量的基本单元,可能由一个或多个grapheme组成。run: 具有共同属性或不可拆行为的一段连续内容。boundary: paragraph start、相邻token之间或 paragraph end 处的断行边界。spacing opportunity: 可以施加 spacing adjustment 的位置;既可以位于相邻token之间,也可以位于同一token内部的相邻grapheme之间。line adjustment: 行调整,为了满足行宽而按规则压缩或扩张可调整空间的过程。LineAdjustmentPlan: 候选行的调整计划,记录 base width、目标宽度、实际调整、使用的优先级和未满足量。LineAdjustmentScore: 对调整计划的结构化评价,分别记录未满足量和各 priority 的 distortion。line fitness: 根据实际压缩或扩张程度得到的tight、natural、loose行质量类别,用于评价相邻行的一致性。Mojikumi: 文字組み,控制不同字符类别之间的 spacing。Yakumono: 約物,日文排版中的标点与特殊符号类别。Kinsoku: 禁則,控制哪些字符不能出现在行首或行尾,以及如何处理违规断点。line-edge spacing: 行边缘间距,描述 grapheme 与 paragraph / line 虚拟边缘之间的 spacing。justification: 齐行,通过压缩或扩展 spacing 让文本填满行宽。H&J: hyphenation and justification,断词与齐行相关诊断。badness/demerit: 段落 composer 用来评价某个断行方案质量的代价。renderer: 把排版结果画到某个目标环境的渲染器。adapter: 连接 core engine 与具体目标环境的适配层,例如 Web、Canvas、Figma。driver: 面向某个运行环境的驱动层,负责测量、调用 core engine、再交给 renderer。ICF: Ideographic Character Face,东亚字体中的平均字面参考。Tate-chu-yoko: 縦中横,竖排中横排少量字符。Ruby: 旁注或注音。Kenten: 着重号。Warichu: 割注。
设计原则
- 一个文本事实来源:底层
CharacterFacts和 reusable predicates 应该共享;同一个 CJK composition profile 可以提供协调的 character class scheme,供Kinsoku、Mojikumi和 line adjustment rules 引用,但各规则不依赖彼此的实现。 - 段落优先:核心
Composer应该能够在段落级别选择断行,而不是只贪心决定当前行。 - 规则可配置:日文、简体中文、繁体中文、韩文、项目 house style 都应该通过
profile表达,而不是写死在算法里。 - 字符集合可覆盖:所有规则中使用的具体字符集合都应该可由用户导入、扩展、覆盖或禁用,类似 InDesign 允许用户管理自定义字符集。
- 功能可独立禁用:任何高级排版功能都应该有明确的 disabled / none profile。禁用某个功能时,pipeline 仍然继续运行,只是该功能输出 no-op 结果。
- 执行模型与功能开关分离:是否提供 available width 决定进入 Measured Composition 或 Static Flow;Kinsoku、Mojikumi 等功能是否启用仍由各自 profile 决定。
- 测量外置但结构内化:具体 glyph 宽度由外部测量器提供,但
Composer必须理解 advance、spacing、压缩、扩展和行宽约束。 - 渲染无关:核心包输出可渲染的数据结构,不绑定 DOM、Canvas、SVG、Figma 或 PDF。
- 规则属于 core engine:字符分类、Kinsoku、Mojikumi、line-edge spacing、justification、孤字不成行等排版决策应发生在
@typeset/typeset,而不是散落在具体 renderer 中。 - 呈现属于 adapter:Web、Figma、Canvas、SVG、PDF 等 adapter 可以选择支持部分视觉效果,但它们不应该重新定义排版规则。
Core engine 与 adapter 边界
@typeset/typeset 是整个排版器的 core engine。共享分析层负责如何分段、如何分词、哪里允许断行及局部 spacing 规则。Measured Composition 进一步决定实际断行、每行 tokens、spacing adjustment 和 diagnostics;Static Flow 则在不知道宽度时输出静态可确定的排版关系,不假装知道最终软换行。
Core engine 不负责决定排版结果如何呈现在用户界面上。具体呈现由外部 adapter / driver 完成。不同 adapter 可以有不同能力:
- Web adapter 可以把 positioned lines 渲染成 DOM、Canvas 或 SVG。
- Figma adapter 可以把结果转换成 Figma text nodes、letter spacing、独立 glyph nodes 或其他 Figma 可表达的结构。
- PDF / print adapter 可以把结果转换成精确 glyph placement。
- Debug adapter 可以把 tokens、boundaries、solver internals、badness 和 diagnostics 可视化。
这个边界决定了功能归属:
- 应在 core engine 实现:character classification、tokenization、Kinsoku、Mojikumi、line-edge spacing、paragraph composition、justification、line keep、diagnostics、logical positioning。
- 应由 adapter 实现:字体测量入口、目标环境的实际绘制、DOM/CSS/Figma API 调用、可选语义 annotations 与 renderer capability diagnostics。
- 应由 framework integration 或 application 实现:排版结果的生命周期、交互状态、选择同步,以及基于 adapter annotations 的调试 UI 与视觉样式。
- 可以由 adapter 降级处理:如果某个目标环境无法表达全部效果,adapter 可以选择近似渲染、拆分节点、忽略部分视觉细节或报告 unsupported diagnostics,但不能改变 core engine 的排版决策。
当前 Solid Playground 是 core engine 与 @typeset/adapter-web 的技术验证调用方。Measured 模式使用 adapter 的 Pretext provider 测量 token natural advance,再由 adapter 序列化 solved lines;Static 模式跳过 measurement 和 solver,由同一 adapter 序列化 Static Flow。Playground 把每个 paragraph 的内部 HTML 当作不透明结果整体替换,并自行负责配置、交互及基于 data-typeset-* annotations 的调试样式。
Feature toggles and no-op profiles
用户应该能够灵活启用或禁用排版引擎中的任意功能。这个能力不能通过在 pipeline 中跳过阶段来实现,否则后续 stages 会面对缺失的结构。正确做法是:pipeline 始终完整运行;被禁用的功能使用 no-op profile,输出中性数据。
本节描述目标模型,不是当前完整 runtime contract。当前已有 noInlineMojikumiProfile、noKinsokuRuleTable、显式 alignment 和显式 lastLinePolicy 等局部机制;统一的 ToggleableProfile、可禁用的 LineKeep、line adjustment 与 hyphenation 尚未实现。
核心约束:
- 每个 stage 都应该有稳定输入和稳定输出。
- 禁用 feature 不应该删除某类输出,只应该让该输出变成中性值。
profile中每个可选功能都应该有enabled或等价的 mode 字段。- disabled profile 不能产生 rule violations;它只能减少约束、减少 adjustment opportunities 或输出 no-op diagnostics。
- solver 不应该知道某个高级功能是否“存在于产品 UI 中”;它只消费 measurements、boundaries、spacing constraints、enabled features 和 fallback policy。
示意:
type FeatureMode = "enabled" | "disabled";
type ToggleableProfile<T> = {
mode: FeatureMode;
options: T;
};
type CompositionProfile = {
locale: "ja" | "zh-Hans" | "zh-Hant" | "ko" | "custom";
characterFacts: CharacterFactsProfile;
characterSets: CharacterSetProfile;
tokenization: TokenizationProfile;
kinsoku: ToggleableProfile<KinsokuProfile>;
mojikumi: ToggleableProfile<MojikumiProfile>;
lineKeep: ToggleableProfile<LineKeepProfile>;
alignment: "start" | "justify";
lineAdjustment: ToggleableProfile<LineAdjustmentProfile>;
hyphenation: ToggleableProfile<HyphenationProfile>;
};
各功能禁用后的中性行为:
| Feature | Disabled behavior |
|---|---|
CharacterSetProfile custom sets |
使用内置 basic sets;用户覆盖不生效,但仍产出 CharacterFacts |
Kinsoku |
不产生 prohibited boundary;所有普通 boundary 按基础 line break policy 处理 |
Mojikumi |
产出空的 InlineGraphemeSpacingOpportunity[];没有位置参与 Mojikumi adjustment |
LineKeep |
不产生 last-line / widow / orphan penalty |
lineAdjustment |
不消费 min / max capacity;只使用 desired spacing |
hyphenation |
不产生 token-internal hyphenation candidates |
glyph scaling |
不产生 glyph-scaling adjustment opportunities |
intra-token letter spacing |
不产生 token-internal spacing opportunities |
| diagnostics highlighting | 不影响 composition,只隐藏或减少 debug output |
这意味着 disabled 不是异常路径,而是合法 profile。比如关闭 Mojikumi 后,Stage 6 仍然执行 inline spacing resolution,只是产出空的 opportunities;关闭 Kinsoku 后,Stage 6 仍然产出 boundary,只是不添加 Kinsoku prohibition;关闭 lineAdjustment 后,Stage 7 仍然求解段落,只是不消费 spacing capacity。alignment: start 不是禁用 line adjustment:它仍允许为超宽候选执行必要 compression,只是不扩展欠宽行。
目标 pipeline
const story = createStory(input, storyOptions);
const paragraphs = story.paragraphs;
const composedParagraphs = paragraphs.map((paragraph) => {
const graphemes = segmentGraphemes(paragraph.text, {
sourceOffset: paragraph.start,
});
const resolvedFont = resolveCompositeFont({
graphemes,
characterClassProfile: compositionProfile.characterSets,
compositeFontProfile: compositionProfile.compositeFont,
});
const tokens = tokenizeGraphemes(graphemes);
const boundaries = resolveInlineBoundaries({
tokens,
paragraphRange: paragraph,
kinsokuRuleTable: compositionProfile.kinsoku.options.ruleTable,
});
const spacingOpportunities = resolveInlineSpacingOpportunities({
tokens,
boundaries,
characterClassProfile: compositionProfile.characterSets,
mojikumiProfile: compositionProfile.mojikumi.options.spacingProfile,
});
if (executionMode === "static") {
return compileStaticFlow({
paragraph,
tokens,
fontRuns: resolvedFont.runs,
boundaries,
spacingOpportunities,
characterClassProfile: compositionProfile.characterSets,
lineKeepProfile: compositionProfile.lineKeep,
mojikumiProfile: compositionProfile.mojikumi.options.spacingProfile,
lastLinePolicy: {
effectiveUnitCount: { mode: "enabled", minimum: 2 },
fillRatio: { mode: "disabled" },
},
});
}
const measurements = measureTokens(tokens, measure, {
fontRuns: resolvedFont.runs,
});
const solvedParagraph = solveParagraph({
measurements,
boundaries,
spacingOpportunities,
characterClassProfile: compositionProfile.characterSets,
lineKeepProfile: compositionProfile.lineKeep,
mojikumiProfile: compositionProfile.mojikumi.options.spacingProfile,
lineEndHangingProfile: compositionProfile.lineEndHanging,
availableWidth: frame.width,
alignment: "justify",
lastLinePolicy: {
effectiveUnitCount: { mode: "enabled", minimum: 2 },
fillRatio: {
mode: "enabled",
minimum: resolveMinimumLastLineFillRatio(frame.width),
},
},
});
return createLayoutResult({
paragraph,
measurements,
solvedParagraph,
});
});
当前实现已经形成从 Stage 1 到 Stage 8 的最小闭环:
story/对应createStory和segmentParagraphs。grapheme/对应segmentGraphemes。tokenizer/对应tokenizeGraphemes。inline-boundary/与inline-spacing/分别产出 break facts 和 spacing opportunities。static-flow/对应无 measurement、无 available width 的静态编译。measurement/、solver/和layout/对应带宽度的完整 composition。
完整 profiles、fallback、diagnostics 和 glyph positioning 尚未实现。
Stage 1: Story and paragraph segmentation
输入文本首先应该被转换成 story-level blocks 和 paragraphs。
职责:
- 保留 source offsets,供 selection、editing、diagnostics 和 renderer mapping 使用。
- 按 hard paragraph breaks 切分 paragraphs。
- 把 soft line breaks、tabs、special spaces、object placeholders、inline markers 和未来 inline annotations 保留为结构化内容。
功能位置:
Paragraph Composer与Single-line Composer的选择当前不在Story/Paragraph上表达,先由 composition profile 或调用参数统一决定。- writing mode、Kinsoku set、Mojikumi set、last-line policy 当前也不做 paragraph-level 覆盖。
- 跨 frame 或 page 的 widow/orphan policy 未来可以附着在更高层的 layout context,而不是现在提前放入 paragraph。
Ruby、Warichu、Kenten、inline objects、footnote markers 等 inline structures 应该在这里进入系统,而不是被临时塞进普通字符串。
当前实现:
story/story.ts已有初始createStory和segmentParagraphs。- hard paragraph breaks 包括
\n、\r\n、\r和 Unicode paragraph separatorU+2029。 - Unicode line separator
U+2028保留在 paragraph text 中,并在 Stage 4 至 Stage 8 作为 paragraph 内的 forced line break 处理。
当前 Story / Paragraph 已接入 Playground 调用链;core 仍保持 stages 独立,不提供隐藏 paragraph orchestration 的 convenience entry。
未完成功能:
- [未完成] Structured control characters:
U+2028已有forced-line-breaktoken、mandatory boundary 和 source range 语义;tab、special space、object placeholder、inline marker 以及更通用的 control model 仍未实现。 - [未完成] Structured inline runs:让
Ruby、Tate-chu-yoko、Warichu、Kenten、inline object、footnote marker 等结构在这里进入 pipeline。Stage 1 只负责保存结构及 source identity;measurement 属于 Stage 5,最终 layout 属于 Stage 8。
Stage 2: Grapheme segmentation
这个阶段把文本切成 Unicode grapheme clusters。
职责:
- 把 emoji sequences、combining marks、composed forms 当作不可拆的
grapheme。 - 保留 UTF-16 source offsets。
- 不做排版判断。这个阶段只回答“什么是一个用户可感知的文本单元”。
目标数据结构:
type Grapheme = {
text: string;
index: number;
start: number;
end: number;
};
字段语义:
text: 当前grapheme cluster的原文。index: 当前 paragraph 内的 grapheme 序号,不是 string offset。start/end: UTF-16 code unit range。默认相对于传入的 input;当使用sourceOffset时,可映射到外层 story source range。
这个阶段的输出只描述 source text 的最小稳定单元,不携带 script、punctuation、Kinsoku、Mojikumi 或 justification 信息。
功能位置:
- 字符集逻辑不属于这里。
Kinsoku、Mojikumi和 keep rules 不属于这里。
当前实现:
grapheme/segmenter.ts使用Intl.Segmenter,granularity: "grapheme"。segmentGraphemes(input)返回结构化Grapheme[]。segmentGraphemes(paragraph.text, { sourceOffset: paragraph.start })可在逐段编排时保留 story-level source ranges。- 没有
Intl.Segmenter时不做天真 fallback,应抛出明确错误。错误的 fallback 会把 combining marks、emoji ZWJ sequence 和 variation selector 拆坏,比提前失败更危险。
未完成功能:
- [未完成] Standards-compliant fallback:如果需要支持没有
Intl.Segmenter的环境,应引入符合 Unicode grapheme cluster 规则的 segmenter,而不是按 code point 拆分。 - [未完成] Segmentation diagnostics:当 runtime segmentation 与预期 Unicode 行为不一致时,输出 diagnostics。
Stage 3: Character facts and rule-local classes
这是 CJK composition 的共享基础,但这里需要避免把所有规则强行绑定到同一套全局 character classes 上。
更合理的分层是:
CharacterFacts: 描述字符事实,例如 code point、script、general category、East Asian Width、是否 whitespace、是否 punctuation。- Reusable predicates / character sets: 基于 facts 的可复用判断,例如
isOpeningPunctuation、isClosingPunctuation、isIdeographicSpace、isPercentLikeSuffix。 - Rule-local classes: 每条规则自己定义需要的 classes,例如
MojikumiClass、KinsokuSet、LineKeepClass。
规则之间没有直接架构依赖。它们共享底层 facts 和可复用 predicates,但 Mojikumi 不应该直接依赖 Kinsoku 的 class,LineKeep 也不应该直接复用 MojikumiClass 作为自己的语义。
职责:
- 产出稳定的
CharacterFacts:script、general category、East Asian Width、code point、normalized text、source range。 - 提供 reusable predicates 和 character sets:opening punctuation、closing punctuation、sentence-ending punctuation、number suffix、unit suffix、whitespace 等。
- 支持 profile-defined character sets,让用户导入、扩展、覆盖或禁用某些字符集合。
- 让
TokenizationProfile、KinsokuProfile、MojikumiProfile、LineKeepProfile各自把 facts / predicates 映射成自己的 rule-local classes。
功能位置:
- “字符集”首先属于这里,但它不是单一全局 enum,而是可命名、可配置、可导入的 character sets。
Yakumono的底层字符集合属于这里;Yakumono 在Mojikumi中如何产生 spacing 属于 inline spacing resolution。- locale-specific character sets 应该通过 profiles 表达,例如
ja、zh-Hans、zh-Hant、ko。 - 用户自定义字符集的导入、合并、覆盖策略属于这里,例如追加某个 line-start prohibited 字符,或替换某个 line-edge spacing 使用的字符集合。
当前实现:
character/已定义CharacterClassProfile、matching helpers 和 default character classes。composite-font/已定义 V1CompositeFontProfile:默认CompositeFontStack加有序 character-class rules。resolveCompositeFont对每个 grapheme 按 first-match 选择字体意图,并输出逐字符 assignments 和合并后的 source-ordered font runs。CompositeFontStack包含有序CompositeFontCandidate[],每个候选分别保存family与style,并可为整个字符类选择设置relativeSize与baselineShift。Core 不解析候选可用性或 glyph fallback;Web adapter 可编译为 exact-face synthetic families,宿主内部 glyph fallback identity 仍可能不可见。relativeSize相对于 composition 基准字号,baselineShift使用 composition em 且不改变 leading。Figma adapter 按候选顺序选择第一个可用 face,以 range font size 表达相对字号;由于 Plugin API 不提供 range baseline shift 或直接的 baseline getter,非零偏移行将相邻、非空格且 baseline shift 相同的 tokens 合并为 visual runs。Adapter 通过临时leadingTrim = "CAP_HEIGHT"clones 与 ordinary/trimmedabsoluteRenderBounds的差值测量整行 reference 及各 run 的 baseline offset,手动定位普通、未 trim 的最终 TextNodes。行距和 glyph scale 仍不进入本阶段配置。- Kinsoku 与 inline spacing rules 已引用
character/中已有的 class ids。 - 当前最小
LineKeepProfile也通过effectiveUnitClassIds选择这些已有 classes;这是现阶段的复用方式,不让 LineKeep 依赖 Mojikumi 或 Kinsoku profile 的规则语义。 - tokenizer 仍保留自己的 private grouping classification;完整
CharacterFacts尚未实现。
未完成功能:
- [未完成] CharacterFacts:引入稳定的 Unicode facts model,作为下游规则可共享的底层事实;是否建立公共 classifier 应以实际跨规则复用为前提,tokenizer-only predicates 继续留在 tokenizer 内部。
- [未完成] CharacterSetProfile:管理可命名、可导入、可追加、可替换、可禁用的 character sets,并定义 merge、override、validation 和 conflict reporting。
- [未完成] Rule-local classes:当前各规则通过自己的 profile 选择共享 Character classes,但尚未建立从 facts / predicates 映射出的完整 rule-local classes;未来扩展时仍不得让 LineKeep 直接依赖 Mojikumi 或 Kinsoku 的规则分类。
- [未完成] Locale profiles and default data review:补全并校正
ja、zh-Hans、zh-Hant、ko等默认 character sets。当前 default classes 仍是基础草案,不视为完整标准表。
示意:
type CharacterFacts = {
text: string;
codePoint: number;
script: Script;
generalCategory: UnicodeGeneralCategory;
eastAsianWidth: EastAsianWidth;
sourceStart: number;
sourceEnd: number;
};
type CharacterPredicate = (facts: CharacterFacts) => boolean;
type CharacterSetProfile = {
sets: Record<string, CharacterPredicate>;
};
type MojikumiClass = {
id: string;
match: CharacterPredicate;
};
type KinsokuProfile = {
lineStartProhibited: CharacterPredicate[];
lineEndProhibited: CharacterPredicate[];
};
type LineKeepProfile = {
effectiveUnitClassIds: string[];
};
Stage 4: Tokenization and run construction
这个阶段把 graphemes 组合成可测量、可保持、或可在受控规则下拆分的 units。
这里需要明确:token 是默认的语义、测量、断行单元,但它不等于“内部永远不能调整”。例如 Latin word 可以作为一个不可随意断开的 word token,同时在 justification 时暴露内部 letter-spacing opportunities。
职责:
- 把 Latin letters 合并成 words。
- 把 digits 与局部、确定的 numeric literal 语法合并成 number runs。
- 默认让 CJK graphemes 保持为单独 tokens。
- 把 punctuation 保留为带有 rich class metadata 的 tokens。
- 为 URLs、numbers with suffixes、dates、abbreviations、ruby bases、inline objects、显式 no-break spans 创建 unbreakable runs。
- 标记 token 的内部调整能力,例如是否允许 letter-spacing、tracking、glyph scaling。
- 为未来 hyphenation 或 emergency break 保留 token-internal break candidates 的位置。
- 保留 token source ranges 和 grapheme ranges。
- token 的
start/end应该来自它包含的第一个和最后一个Grapheme,不应该在 tokenizer 里重新推导 cursor。
功能位置:
- 基础 word grouping 属于这里。
- hard no-break spans 属于这里。
- soft break decisions 不属于这里;它们属于 boundary analysis。
- token-internal break candidates 可以在这里记录,但是否使用属于 Stage 7
Paragraph solving。
当前实现:
tokenizer/合并latin-word和spacetokens,并由高优先级 numeric-literal detector 构造完整numbertokens。tokenizeGraphemes(graphemes)是当前唯一入口;调用方必须显式完成segmentGraphemes。- token 的
start/end来自输入Grapheme,不再由 tokenizer 自己维护 cursor。 tokenizer/classify.ts只作为 tokenizer 内部分类步骤存在,不是公共CharacterFactsAPI。- 当前 tokenizer 分类规则已显式拆成
isAsciiSpaceGrapheme、isCjkPunctuationGrapheme、isCjkTextGrapheme、isLatinWordGrapheme、isNumberGrapheme和isEmojiGrapheme。 TokenKind不包含通用newline。hard paragraph breaks 属于story/的 paragraph segmentation;如果 tokenizer 单独收到\n、\r或\r\n,当前会落入symbol。U+2028被识别为独立的forced-line-breaktoken;它不与文本合并,也不是可渲染字符。U+2029是 Story 层的 paragraph break,若绕过 Story 直接传入 tokenizer,仍会落入symbol。- 当前 numeric-literal detector 覆盖 Unicode
Nddigit runs、点号小数、严格三位逗号分组、 简单斜线分数、相邻正负号、货币符号、数字后缀、成对圆括号和E/e科学计数法。 detector 不猜测 locale-specific separator、空格分组、单位、日期、时间、范围、版本号或 数学表达式。独立英文句点仍是symbol,CJK 标点 range 是粗分类,apostrophe / hyphen 仍会打断latin-word。前导小数的相邻字母检查当前不是 normalization-aware;视觉相同的 NFCÁ.5与 NFDA\u0301.5可能产生不同 tokenization,调用方目前需要在输入前自行 统一 Unicode normalization form。 - token 通过
layoutCapabilities显式记录内部是否允许断行和 spacing;后续 stages 消费 capability,不把TokenKind当作 Kinsoku 或 Mojikumi class。 number是 Stage 4 的默认测量与不可断单元,其默认 capability 禁止内部断行和 spacing。- 外部边界的 Kinsoku 始终匹配 token 的真实首尾 grapheme;数字 prefix、parenthesis 和 suffix 不会因为数字 grouping 而绕过调用方配置的禁则规则。
未完成功能:
- [未完成] TokenKind review and TokenizationProfile:完成
tokenizer/DRAFT.md中记录的类型审查,并支持可配置 grouping policy;在审查完成前不把当前TokenKind当作最终规则分类体系。 - [未完成] Rich run construction:支持 URL、locale-specific / unit-bearing number、date、time、range、version、abbreviation、ruby base、inline object、显式 no-break span 等可保持 runs。
- [部分完成] Token adjustment capabilities:token 已记录内部 break / spacing 的粗粒度 capability;具体 letter-spacing、tracking、glyph scaling 位置和数值限制仍未实现。
- [未完成] Token-internal break candidates:为 hyphenation 和 emergency break 记录候选位置及 synthetic hyphen metadata;是否采用候选仍由 Stage 7 决定。
Stage 5: Measurement and font metrics
这个阶段把 measurement 附着到 tokens 和 glyph-level runs 上。
Stage 5 是 Measured Composition 的依赖,不是 Static Flow 的依赖。执行顺序允许调用方先完成 Stage 6 的宽度无关分析,再只在选择 Measured Composition 时请求外部测量;stage 编号表达职责依赖,不要求实现为了编号而提前执行昂贵工作。
职责:
- 在当前 measurement context 下测量 token natural advance;Mojikumi、justification 等由排版器控制的 spacing 不属于 measurement。
- 同一次 composition 的测量、求解和渲染必须使用一致的字体上下文和统一的 layout unit;具体测量、单位转换与缓存生命周期由 adapter 负责。
- 当 token 需要 internal spacing、tracking 或 justification adjustment 时,保留 glyph-level advances。
- 分开记录 token advance、glyph advances、internal spacing positions,避免
token成为 justification 的最小调整单位。 - 在可用时记录 em size、baseline、ascent/descent、ideographic em box、ICF metrics、vertical metrics。
- 表达 fallback font changes,同时不丢失 source identity。
功能位置:
- Glyph scaling、tracking limits、font metric dependent alignment 需要这个阶段。
- Character alignment、baseline alignment、vertical text metrics 依赖这个阶段。
- Intra-token spacing adjustment 需要这个阶段提供 glyph-level advances。
- 具体怎么在 Web、Figma、Canvas 或 PDF 中取得 metrics 属于 adapter;metrics 的数据结构和 composer 如何消费 metrics 属于 core engine。
当前实现:
measurement/定义最小 measurement contract:MeasureToken = (token, context?) => TokenMeasurement。由measureTokens调用时,context.fontRuns是当前 token 与 resolved composite-font runs 的交集;未启用复合字体时为空数组。第二个参数保持可选,兼容 measurement provider 的直接单字体调用。TokenMeasurement包含{ token, advance, fontRuns? }。fontRuns保存 source range、文本和CompositeFontStack,但 V1 尚不包含每个 run 的独立 advance、adapter applied face 或 glyph metrics。resolveTokenFontRuns(tokens, resolvedRuns)按 source order 单次扫描,把 paragraph-level runs 线性分配为 token-local runs;Core measurement 与需要预先构造 platform request 的 adapter 共享这一 range-intersection 语义。measureTokens(tokens, measureToken, { fontRuns })把Token[]转换成TokenMeasurement[],保持输入顺序和 token identity;不传第三个参数时保持单字体行为。- 真实 measurement 应由 Web、Canvas、Figma、PDF 等 adapter 提供符合 contract 的函数。
- core 包暴露
measureTokenByTextLength作为 fake/debug implementation,用于让当前 pipeline 在没有 adapter 时仍可运行。它只按token.text.length生成 advance,不代表真实字体测量。 @typeset/adapter-web/pretext提供最小 Pretext measurement provider:使用prepareWithSegments(..., { whiteSpace: "pre-wrap", letterSpacing: 0 })和measureNaturalWidth()测量 token,把 CSS pixel 除以compositionEmPx后转换为 core 使用的 em advance。相对字号 run 的fontSizePx是实际字号,compositionEmPx仍是 base font size;该 provider 属于 Web adapter,不进入 core package。- 当前 Pretext provider 整体测量 token natural advance;token 内部的 Mojikumi spacing 仍由 Stage 6 单独生成,但 measurement 尚不包含每个 grapheme 的 natural advance。
未完成功能:
- [未完成] Rich measurement contract:继续使用 injected interface,但扩展
MeasuredToken,表达 glyph/run advances、token-internal break measurement、internal spacing positions、em size、ascent/descent、baseline、ink bounds、embox 和 ICF。 - [未完成] Applied font and glyph identity:V1 已记录字符类选择出的 candidate-stack identity;adapter 实际选择的可用 face、宿主缺字 fallback、glyph substitution 和 capability diagnostics 尚未进入稳定 contract。
- [未完成] Vertical metrics:为 vertical writing 提供 vertical advance、orientation、baseline 和 alignment metrics。
具体 metrics provider 属于 adapter,不是 core 的未完成功能;core 在本阶段尚未完成的是能够承载这些 metrics 的稳定 contract。
Stage 6: Inline boundary and spacing resolution
这个阶段包含两个职责独立的逻辑子步骤:inline boundary resolution 和 inline spacing resolution。两者都只解析求解前已经确定的局部事实,但输出不同的数据模型;它们不是同一个概念,也不互相拥有。
Inline boundary resolution
这个子步骤建立 paragraph 内的 annotated boundaries。它描述 paragraph-start、相邻 tokens 之间、paragraph-end 这些求解前已知的位置,并把只依赖相邻 token / character class 就能判断的局部断行规则附着到 boundary 上。
职责:
- 记录 paragraph-start、between-tokens、forced-line-break、paragraph-end boundaries。
- 保留 boundary 两侧 token identity、token index 和 source position。
- 根据
KinsokuRuleTable标注break.permission:allowed、forbidden、mandatory。 - 记录
break.reasons,例如 line-start prohibited、line-end prohibited、inseparable pair。 - 为 solver 提供局部、静态、可组合的 break facts。
功能位置:
- “禁则”中能静态判断的部分表现为 boundary permissions 和 reasons。
forced-line-breakboundary 是 mandatory;它分别保存前一行结束和下一行开始的 source position,使U+2028本身不进入任一行的可渲染 source range。- 普通 ASCII
spacetoken 前方禁止换行;其后方 boundary 分别保存空格前后的 source position。采用该断点时,空格仍属于上一行的语义 source,但不进入该行的 content width、spacing 或行尾字符判断。这个行为是 whitespace flow semantics,不属于Kinsoku或Mojikumi。 - grapheme 与实际 paragraph / line edge 的 spacing 不附着在 boundary 上,由 solver 确定候选行后查询 Mojikumi profile。
Inline spacing resolution
这个子步骤生成静态的 InlineGraphemeSpacingOpportunity[]。一个 opportunity 表示相邻 graphemes 的 base spacing 和允许调整的范围,包含 minimum、desired、maximum、compression priority、expansion priority 和匹配到的 character class pair,但不决定 solver 最终采用多少 spacing。配置仍是一张完整的 Mojikumi table;每行的扁平 constraint 在类型上由 InlineSpacingStyle(minimum / desired / maximum)与 InlineSpacingPolicy(方向性 priority)组成,避免为了语义分类维护两张需要额外关联的表。
本阶段输出的 before 和 after 都是 InlineGraphemeAddress,包含 tokenIndex 和 token-local graphemeIndexInToken:
- 如果两个地址具有相同的
tokenIndex,这个 gap 位于 token 内部,boundaryIndex为null。 - 如果两个地址具有不同的
tokenIndex,这个 gap 跨越 token boundary,并通过非空boundaryIndex关联已有的InlineBoundary。
boundaryIndex 是 spacing gap 与 break boundary 的关联信息,不是 spacing opportunity 的类型。Mojikumi 始终匹配相邻 grapheme pair,不因为 tokenizer 如何分组而切换规则模型。
MojikumiProfile 还可以声明 paragraph-start、line-start 和 line-end rules。由于这些虚拟实体只有候选行成立时才存在,本阶段不查询、不展开这些 rules;Stage 7 根据候选行的实际首尾 grapheme 解析它们。
职责:
- 根据
MojikumiProfile匹配 character class pairs。 - 生成 token 之间的 spacing opportunities。
- 生成 token 内部的 letter-spacing / tracking opportunities,例如一个 Latin word token 内部的 Roman-to-Roman 间距。
- 表达 Yakumono 周围的 punctuation compression 和 Aki before / after characters。
- 为每个 opportunity 记录可供 Stage 7 编译的 base amount、compression capacity、expansion capacity 和方向独立的 priority。
InlineBoundary 不包含 spacing 字段。一个 grapheme gap 可以同时关联合法或非法断点,并拥有 spacing opportunity;两种事实由 solver 通过 boundaryIndex 关联,而不是在架构上互相从属。
当前这里保留一项已知冲突:Kinsoku 判为不可断的 boundary 仍可能拥有可扩张 Mojikumi spacing,例如半角数字与数字后缀之间。复合字体只改变 font run,不改变 boundary permission,也不会新增 spacing opportunity,因此 V1 不扩大该冲突;后续应在 boundary constraints 与 line adjustment 汇合处禁止这类位置参与 justification 扩张,而不是在复合字体阶段打补丁。
遍历优化
当前阶段允许 inline boundary resolution 和 inline spacing resolution 分别遍历 tokens / graphemes,以优先保持职责、类型和测试边界清楚。两者会重复读取相邻 token edges、graphemes 和 character classes,因此未来可以由同一次内部遍历共同生成 InlineBoundary[] 与 InlineGraphemeSpacingOpportunity[],减少一次扫描和重复的 class matching。
这项优化只合并遍历,不合并语义:两个输出类型、两套规则配置和 solver 输入仍然保持独立。是否采用共享遍历属于实现细节,不应迫使 public architecture 暴露一个混合 boundary-spacing model。
不属于这个阶段:
- 不决定某个 boundary 最终是否成为 line start / line end。
- 不根据行宽排列 token。
- 不判断孤字不成行、最后一行质量或整段平衡。
- 不执行 fallback。
- 不实际应用 spacing adjustment。
当前实现:
inline-boundary/已实现resolveInlineBoundaries({ tokens, paragraphRange })。- 当前
InlineBoundaryKind为paragraph-start、between-tokens、forced-line-break、paragraph-end。 - 当前 boundary 包含结构信息:index、kind、before/after token、before/after token index、line-end source position 与下一行 line-start source position。
- 当前 boundary 只包含
rules.break.permission和rules.break.reasons。 spacetoken 前方带有结构性的before-collapsible-spaceforbidden reason;该规则不随KinsokuRuleTable关闭。空格后方仍可换行,其position指向空格开始,nextPosition指向空格结束。defaultKinsokuRuleTable定义了禁止出现在行首、禁止出现在行尾和禁止分开的字符规则。- 当前只提供一套严格中文 Kinsoku 默认表:后括号、逗号、句号、中间标点、冒号、句尾标点、数字后缀、破折号、省略号、连接号和分隔号等类别禁止出现在行首;前括号、数字前缀和分隔号等类别禁止出现在行尾;破折号与省略号的连续字符分别保持不可分。
forced-line-break与paragraph-end当前标记为 mandatory;paragraph-start不因为后方字符类别而变成 forbidden。InlineBoundary不再包含 spacing 字段,boundary resolution 也不再接收MojikumiProfile。inline-spacing/已实现resolveInlineSpacingOpportunities({ tokens, boundaries }),输出独立的InlineGraphemeSpacingOpportunity[]。- 当前 spacing opportunities 使用统一的 grapheme-gap model,同时支持 inter-token 和 intra-token 位置,并按 source order 输出。
defaultInlineMojikumiProfile已由inline-spacing/default-mojikumi.ts中的完整表格生成;源表覆盖 grapheme pairs、line start、paragraph start 和 line end,并只引用character/中已有的 character class ids。当前 paragraph-start 行全部为零,因此生成后的默认 profile 不包含 paragraph-start rule。- 表格中的
0%, 0%, 0%单元展开为显式零规则。它们用于确定重叠 character classes 下的匹配优先级:resolver 命中零规则后返回 no-op,不生成 spacing opportunity,也不继续回退到后续 class rule。 InlineMojikumiProfile.rules按 first-match 解析;规则顺序是显式 precedence。调用方组合默认表与覆盖规则时,必须把更具体的 override 放在默认规则之前。noInlineMojikumiProfile使用空 rules 作为 disabled / no-op profile,解析结果为空数组。- legacy line breaker、Mojikumi、line composer 和 positioner 已删除;core 不再导出平行的旧模型。
未完成功能:
noKinsokuRuleTable已作为 disabled / no-op profile:它移除可选 Kinsoku prohibition,但 paragraph end 与 forced line break 的结构性 mandatory 语义不受影响。- [未完成] Complete Kinsoku rule profiles:补全 locale-specific line-start prohibited、line-end prohibited 和 inseparable rules。
- [未完成] Complete Mojikumi profiles:机制已支持 paragraph-start / line-start / line-end rules;仍需补全默认 edge 数值、完整 class-pair tables、Yakumono compression、Aki 和 locale-specific profiles。
- [未完成] Shared traversal optimization:在 profiling 证明有必要后,让 boundary 与 spacing resolution 共享一次内部遍历,但继续保留两个输出模型。这是性能优化,不改变 public architecture。
普通 soft line-start 和 line-end 不是 Stage 6 的固定 boundary kind;它们仍由 solver 在某次排列中赋予 boundary 临时角色。paragraph start 只赋予 paragraph-start role,forced line break 后方赋予 hard line-start role;forced line break 前方与 paragraph end 在当前横排段落模型中必然是 hard line end,因此 Static Flow 可以解析这些已知边缘。对于普通 allowed boundary,Static Flow 还可以预先解析后方字符的 line-start rule,并把它输出为只有该 boundary 实际换行后才生效的条件关系;这不会把 boundary 提前认定为行首。
Execution branch: Static Flow
共享分析完成后,pipeline 可以不进入 Stage 5 / Stage 7,而由 compileStaticFlow 生成不依赖 measurement 和 available width 的静态结果。它是执行路径,不是新的排版规则 stage;输入仍然来自 Stage 1 至 Stage 4 和 Stage 6。
职责:
- 按 paragraph edge 和
forced-line-breakboundary 建立 hard segments。 - hard segment 的
sourceRange保留已知行尾的 ASCII space;已知line-endspacing 仍以该空格 token 作为文字组匹配对象。 - 给已知边缘分别附加
paragraph-start、line-start、line-end和paragraph-endroles,并解析其 desired Mojikumi spacing;paragraph start 不同时具有 line-start role。 - 把禁止内部断行的多 grapheme token 与 Kinsoku forbidden boundaries 合并成
unbreakableRuns;单 token run 的boundaryIndexes为空。 before-collapsible-spaceboundary 保留在 Static Flow 的 boundaries 中,但仅由该原因产生的禁止不编译为通用unbreakableRuns。支持普通可折叠空白的 adapter 应直接采用目标环境的 whitespace flow;用原子 no-break wrapper 包住字符与空格会错误保留空格宽度并导致提前换行。- 把 width-independent last-line effective-unit-count constraints 合并进同一组
unbreakableRuns,但保留独立的 line-keep 来源。 - 把 token-internal spacing 与 forbidden boundary spacing 标为
fixed。 - 把普通 allowed boundary 上的 pair spacing 标为
conditional,明确表示只有最终同行时才应存在。 - 对普通 allowed boundary 查询后方 token 的 line-start rule;只把非零 desired spacing 输出为
softLineStartSpacings,由 adapter 条件表达。 - 报告仍依赖未知 soft line-end 或末行 fill ratio 的效果,而不是伪造实际行尾。
Static Flow 可以完整表达局部字符对 spacing、forced breaks、Kinsoku keep relations、末行最低 effective unit 数、已知 hard-edge spacing,以及 adapter 平台能够条件表达的 soft line-start desired spacing。core Static Flow 本身不能执行 justification、按宽度压缩或扩张、全段断行优化、末行比例、soft line-end spacing、标点悬挂及其他依赖实际行的功能。Web adapter 可以额外接收 presentation-level 的 start / justify alignment,并用 CSS text-align 交给浏览器按实际 soft wrapping 排版;该选项不进入 StaticFlowResult,也不改变 core 已编译的静态关系。
输出保持 adapter-neutral。HTML adapter 可以把 unbreakableRuns 编译为 no-wrap markup,把 conditional pair spacing 编译为平台支持的静态关系,并把 softLineStartSpacings 编译为已有 allowed boundary 上的条件补偿;adapter 不应在其他位置创造换行机会。不支持这些条件表达的 adapter 应明确降级,而不是把它们当作 fixed spacing。
Web adapter 当前分别表达正、负 Web conditional spacing:正值使用可折叠空白;负值使用带负 inline margin 的零宽元素,并在其后保留 soft break。负值实现只是已知有缺陷的技术近似:软换行后不会偏移下一行行首,但负 margin 仍会影响上一行的 inline geometry,尚未完全实现“只在 boundary 同行时存在”的语义。该实现不进入 core 类型,adapter 通过 capability diagnostic 明确报告降级。
Web adapter 当前只翻译负值 softLineStartSpacings:它在已有 allowed boundary 上生成可折叠空白补偿和后一个 run 的 inline-start margin,同行时两者抵消;浏览器在该处换行时,行尾空白被折叠,后一个 run 保留行首 spacing。若后一个 run 同时属于不可分组,补偿载体位于该组外侧,行首 margin 由原子 no-wrap box 承载;forbidden boundary 不会获得这种条件关系。正值需要另一种 Web 条件补偿策略,adapter 输出 unsupported diagnostic 且不渲染该效果。
对于 Measured line-end spacing 和 Static Flow 已知 hard line-end spacing,Web adapter 把 amount 叠加到 opportunity 所引用 grapheme 的 spacingAfter,并以 letter-spacing 渲染,不在行末追加空元素。Inline parts 在 spacing 值变化处切分,使 line-end amount 只作用于被引用 grapheme 所在的 part;这种 CSS 技术验证可能在切分处改变 kerning、ligature 或 contextual shaping,尚不能替代未来基于 glyph positions 的正式 renderer。
Measured Flow 存在一项 adapter 级已知限制:测量器提供的 advance 与目标 DOM inline layout 不保证逐像素一致。若 adapter 把 solved line 重新交给浏览器执行自动换行,微小的 shaping、spacing 或取整差异也可能产生第二次断行。Measured renderer 必须以 core 已决定的行作为权威边界,并禁止每个 solved line 内再次换行;允许二次换行只能作为诊断手段。Static Flow 没有已决定的 soft line boundary,因此不应用这项约束。
当前实现:
static-flow/已实现严格入口compileStaticFlow({ paragraph, tokens, boundaries, spacingOpportunities, characterClassProfile, lineKeepProfile, mojikumiProfile, lastLinePolicy })。- 输出包含 hard segments、edge roles / spacing、fixed / conditional pair spacing、soft line-start spacing、line-keep constraints、unbreakable runs 和 unsupported effects。
- 入口不接收 measurement、available width、alignment 或 renderer;
lastLinePolicy.fillRatio若启用会被明确记录为 unsupported effect。 - Node 测试覆盖普通段落、forced line break、末尾空 hard segment、spacing 分类、soft line-start spacing、Kinsoku / line-keep run 合并与 unsupported effects。
[TODO] paragraph-end 与 line-end 的语义边界:当前 Measured solver 把 paragraph end 当作最后一行的 line-end,Static Flow 为保持一致也给 paragraph-end edge 同时附加 line-end role,并使用 line-end Mojikumi rule。物理位置上二者当前重合,但未来引入独立 paragraph-end rules、段末装饰、方向性边缘或规则冲突时,不能默认二者永远是同一语义实体;届时需要定义组合、优先级或独立输出,而不是静默覆盖。
Stage 7: Paragraph solving
这是核心 paragraph writer。它根据 measurement、annotated boundaries、spacing opportunities、Kinsoku、feature toggles、available width 和 fallback policy,直接求解一个 paragraph 的行结果。
这个阶段不要求先产出全局 line break candidates。候选、line fit、badness、dynamic programming、fallback search 都可以是 solver 内部实现细节。架构层面只要求输入清楚、输出清楚、职责清楚。
当前 solver 在一次求解中按需扫描候选、为候选建立 LineAdjustmentPlan,再更新 dynamic programming 路径:
从每个可达 start boundary 开始向后累计 token advance 和适用 spacing opportunities。
遇到合法 end boundary 时解析 line edges,并建立候选行的 LineAdjustmentPlan。
若 end boundary 遇到尾随 ASCII space 或 forced-line-break,则从候选 content width 与普通 inline spacing 中排除不参与可见排版的 token;尾随空格的 source identity 继续保留,并仍作为 `space → line-end` 的匹配对象。
LineAdjustmentPlan 从 desired 得到 base width,按方向和 priority 消费调整容量。
按 LineEndHangingProfile 解析候选行末;采用悬挂时以扩展后的 target 重新建立 plan。
根据 plan 的 final width 扣除 protrusion 得到 occupied width,以此判断 fit,再由独立 scorer 生成 LineAdjustmentScore。
根据 plan 得到 line fitness,并计算与前一行之间的 transition demerit。
使用该 transition 更新 end boundary 对应 fitness 状态的最佳累计 structured score 和前驱。
超过局部可容纳范围或遇到 mandatory boundary 时停止该次扫描。
到达 paragraph end 后沿前驱重建 solved lines。
后续仍可在 solver 内部替换 cost model,或采用 beam search、A*、branch and bound、Knuth-Plass style optimizer 等策略。外部 pipeline 不因此增加阶段。
职责:
- 在给定 frame width 和 paragraph style 下 solve 一个 paragraph。
- 消费
TokenMeasurement[]、InlineBoundary[]、InlineGraphemeSpacingOpportunity[]、character class / Mojikumi profiles 和其他 feature profile。 - 将
Mojikumidesired / min / max 编译成 base amount 和方向性调整容量,再通过LineAdjustmentPlan判断当前排列是否 fit。 - 根据
Kinsokuboundary permissions 决定普通换行点是否可用。 - 必须在
forced-line-breakboundary 结束当前行,并为连续或末尾U+2028保留 empty line。 - 根据 justification policy 分配或拒绝剩余空间。
- 根据 line keep policy 判断孤字不成行、最后一行质量、widow/orphan 等段落约束。
- 根据 fallback policy 决定在失败时允许放宽哪些约束。
- 对实际候选行尾查询
LineEndHangingProfile;标点悬挂改变 type area 占用,不改变 Kinsoku boundary permission,也不伪装成负 spacing。 - 输出 solved lines,包括每行 token 范围、spacing decisions、fallback actions 和 diagnostics。
- 支持
Single-line Composer,用于可预测的逐行行为。 - 支持
Paragraph Composer,用于更平滑的整体 paragraph color。
功能位置:
- “孤字不成行”在这里表现为 last-line 或 paragraph keep constraint。
- Widow/orphan control 在跨 frame / page composition 时属于这里。
- Full paragraph optimization 属于这里。
- Badness、demerits、global line-break selection 属于 solver 内部策略。
- Fallback 策略属于这里统一调度,例如 compress Mojikumi、expand Mojikumi、adjust word spacing、adjust letter spacing、放宽 Kinsoku boundary、token-internal hyphenation、allow overfull。
- Token-internal hyphenation 作为超长 token fallback 在这里被选择;可用断点可以来自 Stage 4,但是否使用属于 solver。
- Full justification 的决策属于这里;具体 output 中应保留实际 spacing values,而不是让 adapter 重新推导。
- 行尾标点悬挂属于这里的候选求解:必须在候选行尾已知后、候选剪枝前参与 fit;Stage 6 不生成 conditional hanging opportunity。
当前实现:
line-keep/已提供纯 resolver,把末行最低 effective unit 数解析为独立 boundary constraints;它不修改 Kinsoku facts,两条 execution path 共用相同语义。solver/已实现严格的solveParagraph输入;调用方必须显式提供 character class / Mojikumi profiles、inline alignment 和 last-line policy,没有默认模式。- 调用方还必须显式提供
LineEndHangingProfile。none禁用;allow只在普通候选无法容纳时尝试悬挂;force对符合规则的候选行尾始终悬挂。 line-end-hanging/当前提供独立 profile 与 resolver。默认中文表只包含、、,、。,三条规则的maximumProtrusion均为1,与 measurement advance 使用同一宽度单位;Playground 和 Figma 当前都使用 em。仅当候选行最后一个可见 token 恰好只含一个 grapheme 时才能解析;实际 protrusion 不超过maximumProtrusion和该 token 的 measured advance。line-adjustment/已实现独立的纯函数 evaluator 和 scorer。evaluator 消费 content width、目标宽度、请求调整量和已编译 opportunities,输出LineAdjustmentPlan;scorer 再根据 plan 生成LineAdjustmentScore。line-adjustment/是 Stage 7 的求值机制,不是独立的用户功能开关;其容量来自MojikumiProfile,当前求解策略由alignment控制,未来再由完整LineAdjustmentProfile扩展。InlineSpacingConstraint使用独立的compressionPriority与expansionPriority;minimum / desired / maximum 分别编译为 base amount 和两个方向的 capacity。- solver 为第一行创建
paragraph-startvirtual entity,为后续行创建line-start,并为每个候选结束位置创建line-end;edge rules 只在此时查询,未被选中的候选不会产生 solved spacing records。 - 当前到达 paragraph end 的候选同样创建
line-endentity;这与 Static Flow 的现有 paragraph-end / line-end 组合语义一致,相关长期疑问记录在 Static Flow TODO 中。 paragraph-start与line-start当前只匹配各自同名规则,不继承、不回退也不叠加。二者与首行缩进等规则的关系尚待讨论。- 当前 solver 从每个可达 boundary 按需向前扫描局部合法断点,不创建持久的全量 edge graph;每个 boundary 最多按
tight、natural、loose保留三条路径状态。 start使用 token advance 与适用 spacing opportunities 的desiredamount 建立 base width;超宽候选按 compression priority 尝试压缩,欠宽候选不扩展。justify先生成候选的LineAdjustmentPlan;未悬挂时occupiedWidth等于finalWidth,悬挂时扣除 protrusion,最终统一以occupiedWidth判断 line fit。只要按优先级消费允许的压缩容量后能够容纳,该 token range 就仍然可以成为当前行。- 启用悬挂的候选以
availableWidth + protrusion作为 adjustment target;求解完成后分别保留advanceWidth = finalWidth与occupiedWidth = max(0, advanceWidth - protrusion)。fit、overfull、末行比例和startraggedness 使用occupiedWidth。 LineAdjustmentPlan按 priority 从低到高消费容量;同一 priority 内按各 opportunity 的方向性容量比例分配,而不是平均分配。压缩和扩张使用各自独立的 priority。- plan 记录每个已使用 priority 的 capacity、applied amount 和 ratio,不包含好坏判断。scorer 将每层的
ratio²记录为独立 distortion,并将未满足调整量单独记录。 - line fitness 根据实际 adjustment direction 和 priority usage ratio 分类:超过一半容量的压缩为
tight,扩张为loose,其余为natural。start不会仅因行尾留白被分类为loose。 - dynamic programming 按整段累计的 structured score 比较路径:先选择未满足量较少的路径,再从较后的 priority 向较早的 priority 比较 distortion,然后比较相邻行的 fitness transition demerit,最后比较
startraggedness;因此较早 priority 的较大调整仍优先于对较后 priority 的少量使用。 tight与loose之间的直接相邻跳变产生一个 transition demerit;与natural相邻暂不惩罚。每个 boundary 分别保留三种末行 fitness 的最佳路径,避免局部最优路径过早覆盖后续更平滑的组合。start同时累计实际 compression distortion 与 final width raggedness;因此会在合法自然断点存在时避免仅为延长一行而压缩,但 Kinsoku 等约束要求时仍可消费 compression capacity。- 采用 ASCII space 后方的断点时,solver 消费该
spacetoken 以保持 token ranges 连续,但从 content width 与普通 inline spacing 中排除它;行尾文字组和悬挂资格仍查询该spacetoken。Stage 8 将它归入上一行的语义 source,下一行从空格后的 source position 开始。 - 严格 Kinsoku 下的基础 push-in / push-out 已由统一求解自然产生:候选行消费 Mojikumi compression 后能够容纳时形成 push-in;容量不足时,dynamic programming 选择更早的合法 boundary,形成 push-out。两者是求解结果,不是独立 feature 或额外 pipeline stage。
- solver 从 plan 物化 actual spacing;
SolvedLine同时保留完整adjustmentPlan,供 diagnostics 和后续 adapter-facing result 扩展使用。 start不扩展欠宽行;justify的非末行在容量允许时向 available width 扩展;两种模式的末行在不需要压缩时都保留desiredspacing。- paragraph-start / line-start / line-end spacing 与 grapheme-pair spacing 一样,被编译为 base amount 与 compression / expansion capacity,并参与候选行的
LineAdjustmentPlan和最终occupiedWidth。 - 调整容量不足时,spacing 停在
minimum或maximum;因此结果可能继续 overfull 或保持 underfull。 - 如果从 line start 到首个合法 boundary 即使压缩到
minimum仍超过 available width,solver 输出 overfull line,保证求解继续前进。 - 跨行 boundary 原有的 grapheme-pair spacing 不进入任意一行;上一行使用 grapheme-to-line-end spacing,下一行使用 line-start-to-grapheme spacing。token-internal spacing 参与 token 所在行,但不创建 break point。
lastLinePolicy.effectiveUnitCount与lastLinePolicy.fillRatio可独立禁用且可以共存。前者是 width-independent hard constraint,后者是 width-dependent soft path preference。- effective-unit resolver 使用
LineKeepProfile.effectiveUnitClassIds查询同一次求解的CharacterClassProfile。一个 token 只要有任一 grapheme 命中任一有效 class 就贡献一个单位,重叠 class 不重复计数;标点不计数但保留在末尾约束范围内。resolver 不跨越最后一个 mandatory boundary。 - effective-unit constraints 与 Kinsoku permission 保持不同来源,但 solver 在候选断点处共同消费。若尾部不可分范围本身超宽,当前严格约束允许产生 overfull,尚无 line-keep fallback。
- fill-ratio 由调用方提供
minimum;Core 不根据 available width 内置阈值公式。solver 以末行最终occupiedWidth / availableWidth判断,occupiedWidth已包含 actual spacing。 - 完整路径到达 paragraph end 时,满足末行比例的路径优先;如果不存在满足要求的路径,则保留累计 cost 最低的结果并输出
last-line-minimum-fill-unmetdiagnostic。 SolvedLine当前输出 half-open token range、start/end virtual entities、source range、advanceWidth、occupiedWidth、显式lineEndHanging、actual spacing decisions、line fitness 和 overfull 状态。SolvedParagraph.diagnostics当前输出三个最小结构化问题:line-overfull、spacing-adjustment-capacity-exhausted和last-line-minimum-fill-unmet。每项都引用最终lineIndex,并保留实际值与目标值;诊断在最佳 structured score 路径重建后生成,不会引用未选中的候选行。- 空 paragraph 输出一条 empty solved line。
- 当前已有最小
LineKeepProfile;按语言区分的完整 profiles、fallback policy 与完整 diagnostics 尚未接入。
未完成功能:
- [未完成] Complete line adjustment policy:当前已按方向和 priority 分层消费 spacing capacity,并使用结构化 priority score;word spacing、letter spacing、tracking、glyph scaling、final-line alignment 和可配置处理顺序仍未接入。
- [未完成] Coordinated line-end adjustment:当前
allow仅在普通候选 overfull 时采用 hanging,不会并行保留普通与悬挂候选参与评分,也未统一定义 line-end Mojikumi compression 与 hanging 的选择顺序和代价。 - [未完成] Complete LineKeepProfile:当前已实现基于现有 Character classes 的末行最低 effective unit 数与最低宽度占比;尚缺按语言区分的 profiles、lonely word 和跨 frame / page 的 widow/orphan 约束。默认 Character classes 尚无 Hangul、Bopomofo 与 Emoji class,这些字符当前不会贡献 effective unit,待字符类补齐后再由 profile 引用。
- [未完成] Hyphenation and emergency token breaking:消费 Stage 4 提供的 token-internal candidates,决定是否断词、插入 synthetic hyphen 或执行 emergency break。
- [未完成] FallbackPolicy:统一调度 Mojikumi compression / expansion、word / letter spacing、Kinsoku boundary relaxation、hyphenation、emergency break 和 allow-overfull,并记录每次约束放宽。严格规则下自然产生的 push-in / push-out 不依赖这个 policy。
- [未完成] Configurable Kinsoku relaxation:基础 push-in / push-out 已实现;尚未设计严格禁则无解时是否允许在 prohibited boundary 断行、其代价和 diagnostic。Stage 6 继续提供原始 forbidden facts,Stage 7 只在显式 fallback policy 允许时放宽它们。
- [未完成] Score model refinement:当前已有 unresolved amount、各 priority 的 capacity ratio distortion、三类 line fitness、相邻极端 fitness transition demerit 和
startraggedness;仍需让 fitness 阈值与 demerit 可配置,并引入连续标点、hyphenation 等其他段落级 demerits。 - [未完成] Composer strategies:正式定义
Single-line Composer与Paragraph Composer的内部 strategy / profile;它们共享solveParagraph所在的 Stage 7,不增加 pipeline stage。 - [未完成] Complete Solver diagnostics:当前已有 overfull、spacing capacity exhausted 和末行比例未满足三类基础诊断;fallback actions、Kinsoku relaxation、H&J badness、cost、其他 line keep violations 和更具体的 overfull 原因仍未实现。
- [未完成] Vertical solving:让 line fit、spacing direction 和 available extent 能在 vertical writing 下工作。
solver 内部可以增加 line fitting、cost calculation、fallback handling 等私有 helpers,但不作为 pipeline stage 暴露。
示意:
type SolveParagraphInput = {
measurements: readonly TokenMeasurement[];
boundaries: readonly InlineBoundary[];
spacingOpportunities: readonly InlineGraphemeSpacingOpportunity[];
characterClassProfile: CharacterClassProfile;
lineKeepProfile: LineKeepProfile;
mojikumiProfile: InlineMojikumiProfile;
lineEndHangingProfile: LineEndHangingProfile;
availableWidth: number;
alignment: "start" | "justify";
lastLinePolicy: {
effectiveUnitCount: { mode: "disabled" } | { mode: "enabled"; minimum: number };
fillRatio: { mode: "disabled" } | { mode: "enabled"; minimum: number };
};
};
type SolvedParagraph = {
sourceRange: SourceRange;
availableWidth: number;
lines: readonly SolvedLine[];
diagnostics: readonly SolverDiagnostic[];
};
type SolvedLine = {
startTokenIndex: number;
endTokenIndexExclusive: number;
startEdge: SolvedLineStartEdge;
endEdge: SolvedLineEndEdge;
sourceRange: SourceRange;
advanceWidth: number;
occupiedWidth: number;
lineEndHanging: SolvedLineEndHanging | null;
spacing: readonly SolvedSpacing[];
adjustmentPlan: LineAdjustmentPlan<SolvedSpacingOpportunity>;
fitness: "tight" | "natural" | "loose";
overfull: boolean;
};
FallbackPolicy 应该是有顺序的 recovery ladder。每一步都会增加 cost,并产生 diagnostics:
type FallbackStep =
| "use-desired-spacing"
| "compress-mojikumi"
| "expand-mojikumi"
| "adjust-word-spacing"
| "adjust-letter-spacing"
| "relax-kinsoku-boundary"
| "hyphenate-token"
| "emergency-break-token"
| "allow-overfull";
Stage 8: Layout result and diagnostics
这个阶段把 solver result 转换成 adapter 可消费、工具可检查的数据。它不是重新求解,也不重新决定排版规则。
职责:
- 返回 paragraphs、lines、positioned tokens / glyph runs、spacing decisions、source ranges。
- 返回 solver decisions、fallback actions、Kinsoku processing、H&J violations、keep violations、substituted glyphs、overfull lines 的 diagnostics。
- 应用 solver 已经决定的 actual spacing values,而不只是 desired spacing。
- 应用 token-internal letter spacing 或 glyph-level adjustment。
- 应用 line-edge spacing,同时保留 logical line bounds。
- 显式保留 terminal grapheme protrusion,使 adapter 可以区分 glyph advance、type area 占用与 spacing。
- 支持 horizontal 和 vertical writing modes 的 output shape。
- 支持 baseline、embox、ICF、character alignment offsets。
- 保留足够 metadata,供 playground visualization、hit testing 和 regression testing 使用。
- 避免 renderer-specific instructions。
- 明确标注哪些效果已由 core engine 决策但当前 adapter 可能无法完整表现。
功能位置:
- Positioning 属于这里,但 positioning 不再决定换行或 spacing policy。
- Highlighting Kinsoku processing 和 spacing problems 属于这里的 diagnostics。
- Playground / debug UI 应该消费这些 metadata,而不是重新推导状态。
- Web、Figma、PDF 等 adapter 的兼容性提示应从这里的 diagnostics 生成。
当前实现:
layout/已实现严格入口createLayoutResult({ paragraph, measurements, solvedParagraph }),不提供隐藏前置 stages 的 convenience orchestration。- 当前
LayoutResult包含 paragraph source range、available width、有序LayoutLine[]和 solver diagnostics。 LayoutLine包含 token range、start / end virtual entities、语义sourceRange、排版contentRange、text、advance / occupied / available width、显式 line-end hanging、actual spacing 和 overfull。- token range 可以包含尾随
space或forced-line-breaktoken。Stage 8 不物化 control token,但会把尾随空格保留在LayoutLine.tokens和text中,并标为collapsed-at-line-end;它属于sourceRange,不属于contentRange,adapter 不得通过删除字符模拟折叠。 - actual spacing 的 anchors 可以引用 grapheme、paragraph-start、line-start 或 line-end;virtual entities 即使没有匹配 spacing rule 也会存在。
LayoutToken与LayoutGrapheme已物化 token advance、grapheme source metadata 和 grapheme-pairspacingAfter;启用复合字体 measurement 时,token 保留 font runs,grapheme 记录字符类解析后的CompositeFontStack。edge spacing 保留在统一的 line spacing decisions 中。- Stage 8 原样保留
SolvedParagraph.diagnostics,不重新推导问题、不生成 renderer-specific message;adapter 可以展示、记录或忽略这些 metadata。 - Stage 8 会验证 paragraph source range、line token range 连续性、spacing address 和 width reconstruction,不接受不一致的 solver output。
@typeset/adapter-web直接消费LayoutResult/StaticFlowResult,将单个 paragraph 序列化为可信 HTML,并用内部 CSS Module 表达 Web 排版语义;Solid Playground 把结果作为不透明 HTML 整体更新。- Playground 使用 Pretext 测量 token natural advance,可以验证比例字体下的断行与 spacing;当前 contract 仍只有 token advance,不能验证 glyph positions、baseline、ink bounds 或完整 font fallback。
- Web adapter 会跨 token 合并最终渲染配置相同的连续 grapheme,只为配置变化、conditional spacing、soft line-start compensation 或 Static Flow 结构边界保留独立 inline run。默认输出不会只为 line-end hanging 创建 span;调用方显式请求 hanging annotation 时,adapter 才为对应 grapheme 保留可标注的边界。跨 token 合并可能引入 measurement token 之间的 shaping,独立 run 也可能切断整 token 测量时存在的 shaping;在 measurement 尚无 glyph positions 时,两者都属于当前 adapter 的已知近似。
- Playground 当前提供
default、SimpleChinesePrettySpacing、priority和custom四种 Mojikumi profile 视图。前两者是 core 内置的只读 profile;priority是 Playground 本地验证 profile;custom只在内存中克隆并修改默认规则的 minimum / desired / maximum 与 compression / expansion priority,不代表通用 Profile model、导入或持久化已经实现。 - Playground 提供独立的 line-end hanging 开关;默认关闭。启用后直接按
LayoutLine.lineEndHanging展示突出字符。
未完成功能:
- [未完成] Glyph/run positioning:在引入更丰富 measurement 后,输出 glyph/run coordinates、actual advances、baseline、embox、ICF、ink bounds 和 edge overflow,而不只输出 logical tokens 与
spacingAfter。 - [未完成] Complete CompositionDiagnostic:当前已物化 Stage 7 的三类基础 diagnostics;solver decisions、fallback actions、Kinsoku processing、完整 H&J / keep violations、substituted glyphs 和 adapter capability diagnostics 仍未实现。
- [未完成] Vertical layout output:为 vertical writing 输出明确的 logical / visual axes、glyph orientation 和 line progression。
- [未完成] Complex inline layout:物化 Stage 1 structured runs 和 Stage 5 metrics,支持
Ruby、Tate-chu-yoko、Warichu、Kenten与 inline objects。 - [未完成] Complete visual metrics naming:围绕 visual 和 logical metrics 明确并稳定以下字段:
- logical text range
- ink bounds
- advance width
- edge overflow
- baseline position
Profile model
高级 CJK 行为大多应该由 profiles 配置。
当前状态:[未完成]。下列类型是目标模型,尚未作为完整 runtime contract 实现。Profile model 是横跨 Stage 3 至 Stage 7 的配置层,不是额外 pipeline stage;调用方当前仍分别向各 stage 传入局部参数。
未完成功能:
- [未完成] ToggleableProfile:为 Kinsoku、Mojikumi、line keep、line adjustment 和 hyphenation 提供统一 enabled / disabled 语义;禁用功能仍必须产出结构稳定的 no-op output。Alignment 本身是
start/justify的必选语义,不作为 toggle。 - [未完成] Profile composition and validation:定义 locale defaults、用户覆盖、profile merge、引用有效性、冲突诊断和版本兼容规则。
- [未完成] CompositionProfile wiring:把统一 profile 分发到各 stage,同时保持当前要求的显式调用顺序,不增加 convenience orchestration。
type FeatureMode = "enabled" | "disabled";
type ToggleableProfile<T> = {
mode: FeatureMode;
options: T;
};
type CompositionProfile = {
locale: "ja" | "zh-Hans" | "zh-Hant" | "ko" | "custom";
characterFacts: CharacterFactsProfile;
characterSets: CharacterSetProfile;
tokenization: TokenizationProfile;
kinsoku: ToggleableProfile<KinsokuProfile>;
mojikumi: ToggleableProfile<MojikumiProfile>;
lineKeep: ToggleableProfile<LineKeepProfile>;
alignment: "start" | "justify";
lineAdjustment: ToggleableProfile<LineAdjustmentProfile>;
lineEndHanging: LineEndHangingProfile;
hyphenation: ToggleableProfile<HyphenationProfile>;
};
各 profile 的预期职责:
CharacterFactsProfile: 产出底层 Unicode / CJK facts,不直接表达某条排版规则的最终语义。CharacterSetProfile: 管理 named character sets 和 reusable predicates,支持用户导入、扩展、覆盖、禁用。TokenizationProfile: grouping 和 no-break run construction。KinsokuProfile: 自己定义 line-start / line-end prohibited sets、break recovery behavior、自定义 sets。MojikumiProfile: 定义 spacing classes、class-pair spacing、line-start / line-end spacing,以及同一表格行中的 style columns(min / desired / max)和 policy columns(方向性 priority)。LineKeepProfile: 引用CharacterClassProfile的 class ids 定义 last-line effective units;未来扩展按语言 profiles、widow/orphan constraints 与 lonely word rules,不复制字符 matcher。LineAdjustmentProfile: 定义 adjustment kinds 的 compression / expansion order、glyph scaling limits、tracking limits 和 fallback policy;是否扩展欠宽非末行首先由alignment决定。LineEndHangingProfile: 定义none/allow/force模式、可悬挂 character classes 与 protrusion 上限;它不修改 Mojikumi spacing 或 Kinsoku boundary。HyphenationProfile: token-internal break candidates、synthetic hyphen、emergency break policy。ToggleableProfile: 统一表达某个功能启用或禁用。禁用时必须返回 no-op output,而不是让 pipeline 缺少数据。
Playground 中 Facts / Rules / Style / Policy 属于整个排版配置的一级视图,不属于 MojikumiProfile。成组配置使用原生 details。Facts 视图只读投影当前 CharacterClassProfile,展示字符类的稳定 id、matcher、定义和备注;它尚不提供自定义、覆盖或重叠校验。Rules 视图展示 Kinsoku 的 before / after boundary 约束。Mojikumi table 是 Style / Policy 两个视图共享的一项可折叠配置;两个视图都提供功能开关与 profile 选择,Style 编辑 spacing range,Policy 编辑方向性 priority。Rules 与 Facts 使用同一份 CharacterClassProfile,不在规则视图中重复维护字符归属。Policy 视图还直接映射现有 lastLinePolicy 与 LineEndHangingMode,不在 adapter 中重新解释其求解语义。
Feature map
| Feature | Target stage | Current status |
|---|---|---|
| Character facts / reusable predicates | Stage 3 | 已有共享 character classes 和 matching helpers;完整 CharacterFacts 尚未实现 |
| 用户自定义字符集 | Stage 3 and Profile model | 尚未实现,目标是支持导入、扩展、覆盖、禁用 |
| Feature toggles / no-op profiles | Profile model and all stages | 已有 Mojikumi / Kinsoku no-op profile 及显式 alignment / last-line policy;通用 toggle/profile 尚未实现 |
| Static Flow | Stage 6 后的执行分支 | 已输出 hard segments、edge roles、fixed / conditional pair spacing、soft line-start spacing、unbreakable runs 与 unsupported effects |
| Rule-local classes | Stage 3, Stage 6, Stage 7 | 尚未系统化,目标是 Mojikumi / Kinsoku / LineKeep 各自定义 |
| Yakumono classification | Stage 3 | 当前已有基础 default character classes,并被 Kinsoku 与 inline spacing rules 引用 |
| 禁则 / Kinsoku | Stage 6 and Stage 7 | 当前已有一套严格中文 boundary permission、reasons 和不可分 pair 规则 |
| Kinsoku push in / push out | Stage 7 | 严格规则下已由统一求解自然产生:可压缩时 push in,容量不足时选择更早合法断点形成 push out |
| Mojikumi spacing | Stage 6 inline spacing | 已从 boundary 拆出,以语义字符类和统一 grapheme-gap model 支持 inter-token 与 intra-token |
| Mojikumi min/max/priority | Stage 6 and Stage 7 | Stage 7 已编译为 LineAdjustmentPlan,按方向和 priority 分层、同层按容量比例压缩或扩展 |
| 连续标点挤压 | Stage 6 and Stage 7 | 当前有基础 closing-to-opening bracket spacing |
| 行尾标点悬挂 | Stage 7 and Stage 8 | 已有独立 profile、候选 fit 与 protrusion output;默认表限 、,。;Playground 与 Figma UI 均支持三种 mode |
| 末行最低宽度占比 | Stage 7 | 已支持调用方传入比例、整段路径选择和未满足比例的 diagnostic |
| 末行最低 effective unit 数 | Stage 6 后共享 line-keep analysis 与 Stage 7 | 已支持 Measured / Static;由 LineKeepProfile 引用现有 Character classes,标点不计数但保留在末尾约束范围内 |
| 孤字不成行 | Stage 7 | 已支持基础 effective-unit hard constraint;完整 CJK class 覆盖、按语言 profile 与跨 frame widow/orphan 尚未实现 |
| Solver layer | Stage 7 | 已实现按需候选与 dynamic programming,消费 measurement、boundary、spacing 和 available width |
| Line adjustment evaluator | Stage 7 internal | 已独立生成 base/final width、方向性实际调整、priority usage 与未满足量 |
| Line adjustment scorer | Stage 7 internal | 已将 unresolved amount 与各 priority distortion 分开,并按严格 priority 顺序比较整段路径 |
| Line fitness | Stage 7 internal | 已支持 tight / natural / loose、相邻极端跳变 demerit,以及每个 boundary 按 fitness 保留路径 |
| Line fitting helpers | Stage 7 internal | 已通过 LineAdjustmentPlan 判断 fit;fitness 配置与其他 score 项仍需继续完善 |
| Fallback policy | Stage 7 and Stage 8 | 尚未实现,目标是统一降级顺序和 diagnostics |
| Paragraph Composer | Stage 7 | 已按整段累计 structured score 和基础 line fitness 连续性选择断行路径;高级 demerits 尚未实现 |
| Single-line Composer | Stage 7 | 尚未定义独立 strategy/profile |
| Start / full justification | Stage 7 / Web adapter | Measured Stage 7 已有显式 start / justify;Web adapter 可为 Static HTML 输出 CSS alignment,core Static Flow 不接收 alignment |
| Inter-token / intra-token adjustment | Stage 5, Stage 6 inline spacing, Stage 7, Stage 8 | Stage 6 已统一建模,Stage 7 已采用 desired;真实 glyph positioning 尚未实现 |
| Token-internal hyphenation | Stage 4 and Stage 7 | 尚未实现,作为超长 token fallback 扩展点 |
| H&J diagnostics | Stage 7 and Stage 8 | 已有 overfull、spacing capacity exhausted 和末行比例未满足;完整 badness、fallback 与 violation 原因尚未实现 |
| Adapter-facing layout result | Stage 8 | 已有 logical line/token/grapheme output 和基础 diagnostics;glyph positioning 与完整 diagnostics 尚未实现 |
| Font metrics | Stage 5 | 当前只有外部 measureToken |
| Vertical writing | Stage 5, Stage 7, Stage 8 | 尚未实现 |
| Ruby / Tate-chu-yoko / Kenten / Warichu | Stage 1, Stage 5, Stage 8 | 尚未实现,未来应作为 structured inline runs 进入系统 |
| Web driver / adapter | Stage 5 and Stage 8 | 已拆出 @typeset/adapter-web:提供 Pretext advance measurement、Measured / Static HTML serializer、内部 CSS Module 与降级 diagnostics |
后续重构顺序
- 引入
ToggleableProfile和 no-op profiles,保证禁用任意功能时 pipeline 仍然闭合。 - 在现有 last-line effective unit count / fill ratio 基础上扩展按语言区分的 line keep profiles,并实现 lonely word 和跨 frame 的 widow/orphan constraints。
- 在 solver 内部逐步完善 fallback policy、alignment-specific adjustment 和 cost model,但不把它们提升为 pipeline stage。
- 扩展 adapter-facing layout result 和 diagnostics,明确 core engine 与 Web / Figma / PDF adapters 的边界。
- 继续用 Playground 验证
@typeset/adapter-web的 measurement/rendering 一致性,完善 conditional spacing、字体生命周期与 capability diagnostics。 - 后续引入完整
CharacterFacts、用户可导入字符集、vertical writing 和更丰富的 inline structures。
第一批重构应该优先改善架构,不必一次改变所有行为。关键是让每个未来功能都从正确的 pipeline stage 进入系统。