CJK 段落书写器架构

@typeset/typeset 的长期目标是实现接近 Adobe InDesign “CJK Paragraph Composer” 的排版效果。这个目标不是只在字符之间插入固定空隙,而是让引擎能够在整段范围内综合判断 character classification、Kinsoku、Mojikumi、Yakumono、line-edge spacing、孤字不成行、justification 和最终 positioning。

这份文档描述目标架构和工作流程。它不表示所有能力都已经实现,而是规定这些能力应该发生在哪个环节,避免后续功能堆叠时互相穿透边界。

术语约定

正文中专业术语优先保留英语或日语,下面给出中文含义,方便阅读时对照:

设计原则

Core engine 与 adapter 边界

@typeset/typeset 是整个排版器的 core engine。共享分析层负责如何分段、如何分词、哪里允许断行及局部 spacing 规则。Measured Composition 进一步决定实际断行、每行 tokens、spacing adjustment 和 diagnostics;Static Flow 则在不知道宽度时输出静态可确定的排版关系,不假装知道最终软换行。

Core engine 不负责决定排版结果如何呈现在用户界面上。具体呈现由外部 adapter / driver 完成。不同 adapter 可以有不同能力:

这个边界决定了功能归属:

当前 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。当前已有 noInlineMojikumiProfilenoKinsokuRuleTable、显式 alignment 和显式 lastLinePolicy 等局部机制;统一的 ToggleableProfile、可禁用的 LineKeep、line adjustment 与 hyphenation 尚未实现。

核心约束:

示意:

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 的最小闭环:

完整 profiles、fallback、diagnostics 和 glyph positioning 尚未实现。

Stage 1: Story and paragraph segmentation

输入文本首先应该被转换成 story-level blocks 和 paragraphs。

职责:

功能位置:

当前实现:

当前 Story / Paragraph 已接入 Playground 调用链;core 仍保持 stages 独立,不提供隐藏 paragraph orchestration 的 convenience entry。

未完成功能:

Stage 2: Grapheme segmentation

这个阶段把文本切成 Unicode grapheme clusters

职责:

目标数据结构:

type Grapheme = {
  text: string;
  index: number;
  start: number;
  end: number;
};

字段语义:

这个阶段的输出只描述 source text 的最小稳定单元,不携带 script、punctuation、Kinsoku、Mojikumi 或 justification 信息。

功能位置:

当前实现:

未完成功能:

Stage 3: Character facts and rule-local classes

这是 CJK composition 的共享基础,但这里需要避免把所有规则强行绑定到同一套全局 character classes 上。

更合理的分层是:

  1. CharacterFacts: 描述字符事实,例如 code point、script、general category、East Asian Width、是否 whitespace、是否 punctuation。
  2. Reusable predicates / character sets: 基于 facts 的可复用判断,例如 isOpeningPunctuationisClosingPunctuationisIdeographicSpaceisPercentLikeSuffix
  3. Rule-local classes: 每条规则自己定义需要的 classes,例如 MojikumiClassKinsokuSetLineKeepClass

规则之间没有直接架构依赖。它们共享底层 facts 和可复用 predicates,但 Mojikumi 不应该直接依赖 Kinsoku 的 class,LineKeep 也不应该直接复用 MojikumiClass 作为自己的语义。

职责:

功能位置:

当前实现:

未完成功能:

示意:

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。

职责:

功能位置:

当前实现:

未完成功能:

Stage 5: Measurement and font metrics

这个阶段把 measurement 附着到 tokens 和 glyph-level runs 上。

Stage 5 是 Measured Composition 的依赖,不是 Static Flow 的依赖。执行顺序允许调用方先完成 Stage 6 的宽度无关分析,再只在选择 Measured Composition 时请求外部测量;stage 编号表达职责依赖,不要求实现为了编号而提前执行昂贵工作。

职责:

功能位置:

当前实现:

未完成功能:

具体 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 上。

职责:

功能位置:

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)组成,避免为了语义分类维护两张需要额外关联的表。

本阶段输出的 beforeafter 都是 InlineGraphemeAddress,包含 tokenIndex 和 token-local graphemeIndexInToken

boundaryIndex 是 spacing gap 与 break boundary 的关联信息,不是 spacing opportunity 的类型。Mojikumi 始终匹配相邻 grapheme pair,不因为 tokenizer 如何分组而切换规则模型。

MojikumiProfile 还可以声明 paragraph-startline-startline-end rules。由于这些虚拟实体只有候选行成立时才存在,本阶段不查询、不展开这些 rules;Stage 7 根据候选行的实际首尾 grapheme 解析它们。

职责:

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。

不属于这个阶段:

当前实现:

未完成功能:

普通 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。

职责:

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,因此不应用这项约束。

当前实现:

[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 不因此增加阶段。

职责:

功能位置:

当前实现:

未完成功能:

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 可消费、工具可检查的数据。它不是重新求解,也不重新决定排版规则。

职责:

功能位置:

当前实现:

未完成功能:

Profile model

高级 CJK 行为大多应该由 profiles 配置。

当前状态:[未完成]。下列类型是目标模型,尚未作为完整 runtime contract 实现。Profile model 是横跨 Stage 3 至 Stage 7 的配置层,不是额外 pipeline stage;调用方当前仍分别向各 stage 传入局部参数。

未完成功能:

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 的预期职责:

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 视图还直接映射现有 lastLinePolicyLineEndHangingMode,不在 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

后续重构顺序

  1. 引入 ToggleableProfile 和 no-op profiles,保证禁用任意功能时 pipeline 仍然闭合。
  2. 在现有 last-line effective unit count / fill ratio 基础上扩展按语言区分的 line keep profiles,并实现 lonely word 和跨 frame 的 widow/orphan constraints。
  3. 在 solver 内部逐步完善 fallback policy、alignment-specific adjustment 和 cost model,但不把它们提升为 pipeline stage。
  4. 扩展 adapter-facing layout result 和 diagnostics,明确 core engine 与 Web / Figma / PDF adapters 的边界。
  5. 继续用 Playground 验证 @typeset/adapter-web 的 measurement/rendering 一致性,完善 conditional spacing、字体生命周期与 capability diagnostics。
  6. 后续引入完整 CharacterFacts、用户可导入字符集、vertical writing 和更丰富的 inline structures。

第一批重构应该优先改善架构,不必一次改变所有行为。关键是让每个未来功能都从正确的 pipeline stage 进入系统。