Skip to content

Support GSUB type 6 format 2 with nested single substitutions - #861

Open
mh0pe wants to merge 1 commit into
opentypejs:masterfrom
mh0pe:agent/gsub-lookup-6-format-2
Open

Support GSUB type 6 format 2 with nested single substitutions#861
mh0pe wants to merge 1 commit into
opentypejs:masterfrom
mh0pe:agent/gsub-lookup-6-format-2

Conversation

@mh0pe

@mh0pe mh0pe commented Jul 24, 2026

Copy link
Copy Markdown

Description

Adds execution support for the GSUB chaining contextual substitution format that
the parser already exposes as lookup type 6, format 2.

The implementation:

  • gates rules through coverage and the input class set;
  • matches backtrack classes in reverse logical order, then input and lookahead
    classes through their separate class definitions;
  • applies nested lookup records in design order, with later records consuming
    earlier substitutions;
  • retains each sequenceIndex, so only the targeted token is changed;
  • stops at the first matching rule and subtable, including deliberate no-op
    rules;
  • supports nested single substitutions (formats 1 and 2), including type 7
    extension wrappers.

The nested-lookup scope is intentionally limited to single substitutions. Other
nested lookup types fail explicitly rather than being silently skipped. Lookup
flag filtering remains a broader shaping-engine follow-up; both real-world
reproducers below use lookup flag 0 and nested type 1.

Motivation and Context

On current master, the presence of a type 6 / format 2 subtable causes
getPath() and getAdvanceWidth() to throw before coverage is even evaluated:

substitutionType : 62 lookupType: 6 - substFormat: 2 is not yet supported

This reproduces with both SF Hebrew Rounded and the current Google Fonts Roboto
variable font.

This is a focused extraction of the GSUB 6/2 portion of #816, following the
review suggestion there to split the GSUB work from unrelated variation
changes. Thanks to @wiltse439 for the original implementation work; the commit
also credits them as a co-author. This PR leaves the existing type 6 / format 3
path untouched, preserving the sequenceIndex fix from #824.

How Has This Been Tested?

  • npm test: 348 passing, including build, minified distributions, and source
    lint.
  • Six synthetic regressions cover reverse backtrack order, first-match no-op
    rules, ordered records, nonzero sequenceIndex through token application,
    first matching nested and top-level subtables, extension wrappers, and
    explicit unsupported-type errors.
  • SF Hebrew Rounded:
    • an end-of-run mark changes from glyph 110 to final-form glyph 163;
    • the same mark followed by a protected lookahead remains glyph 110;
    • the previous unrelated Latin probe renders without throwing.
  • Google Fonts Roboto variable font:
    • i + combining acute shapes to glyphs [636, 169];
    • affinity renders without throwing.
  • Both generated ESM and CommonJS distributions produce the same real-font
    results.

Screenshots (if appropriate):

Not applicable.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I did npm run test and all tests passed green (including code styling checks).
  • I have added tests to cover my changes.
  • My change requires a change to the documentation.
  • I have updated the README accordingly.
  • I have read the Contribute README section.

Execute parsed class-based chaining rules with ordered, position-aware nested single substitutions. Preserve first-match semantics across rules and subtables, including deliberate no-op rules.

Add synthetic end-to-end coverage for backtrack and rule ordering, nonzero sequence indexes, extension lookups, and unsupported nested types.

Co-authored-by: Wiltse Carpenter <61297711+wiltse439@users.noreply.github.com>
@mh0pe

mh0pe commented Jul 29, 2026

Copy link
Copy Markdown
Author

Friendly maintainer ping @fdb @Connum — this is the focused GSUB type 6 format 2 extraction suggested during review of #816. It keeps the scope to nested single substitutions, fails explicitly for unsupported nested lookup types, and includes synthetic plus real-font coverage. I’d appreciate a review when time permits and am happy to adjust the scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant