Skip to content

Fix LT-22351: load default font features in BaseStyleInfo.ProcessStyleRules#388

Open
johnml1135 wants to merge 4 commits into
sillsdev:masterfrom
johnml1135:fix/LT-22351-load-default-font-features
Open

Fix LT-22351: load default font features in BaseStyleInfo.ProcessStyleRules#388
johnml1135 wants to merge 4 commits into
sillsdev:masterfrom
johnml1135:fix/LT-22351-load-default-font-features

Conversation

@johnml1135

@johnml1135 johnml1135 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Font features set as a style's default font (not tied to a writing system) saved correctly but silently disappeared on reload — breaking Dictionary Preview CSS generation for anyone using default-level font features.

1. The bug that started it all

flowchart LR
    A["Styles dialog<br/>sets default font features"] --> B["Save<br/>GetOverridesString serializes fine"]
    B --> C[("Database<br/>style rules blob")]
    C --> D["Load<br/>ProcessStyleRules switch"]
    D -->|"no case for ktptFontVariations"| E["Dropped"]
Loading

BaseStyleInfo.ProcessStyleRules had a case for the default font name but none for default font features (ktptFontVariations), so it fell to default: break and the value was thrown away on load. The save path and the per-writing-system override path both already understood this property; only the default-font load path didn't.

2. Fixing it exposed a second, older bug

Take a parent style Title Main with an English override of Fontastic and a default font of Times New Roman. A child style Inherit Title is based on it, and the only thing set on the child is a default font feature: smcp=1. What does the child's English text render in?

Before this PR Fixed the obvious way (one-line) This PR (final)
Default font features loaded? No — no case in the switch Yes Yes
Does the child's default become "has explicit"? No Yes Yes
What runs on the child's English override nothing — the propagate branch never triggers InheritAllProperties(default) — copies every default value, explicit or not InheritExplicitProperties(default) — copies only values the default itself set explicitly
Resulting English font name Fontastic ✅ (correct, by luck) Times New Roman ❌ (clobbered) Fontastic ✅ (correct, on purpose)
Resulting English font features missing ❌ smcp=1 ✅ smcp=1 ✅

The middle column isn't hypothetical — it's what a one-line fix to just ProcessStyleRules would have produced, since fixing the load path is what makes IsAnyExplicit true and turns on the propagate-to-overrides branch for the first time for these styles.

Fix: only propagate default properties the style explicitly set, via new FontInfo.InheritExplicitProperties. Properties the default merely inherited from further up the style chain are left alone, so they no longer clobber an override's own, more specific inheritance.

3. The rule now, in plain terms

  • Explicitly set default → propagates. If a style explicitly sets a default (font name, bold, features…), that value wins over whatever a based-on style's own writing-system override had. Intentional, longstanding behavior (LT-18109).
  • Merely inherited default → hands off. If a style's default value for some property was never explicitly set — just riding along from the based-on style's default — it must not overwrite an override correctly inherited from the based-on style's own writing-system override.

4. A third, smaller bug found along the way: bullet fonts

BulletInfo.DecodeFontInfo stopped reading an encoded blob after the first string property, so a bullet with both a custom font name and font features lost the features on decode:

Encoded property (in order) Before (break) Now (continue)
ktptFontFamily = "Algerian" read read
ktptFontVariations = "smcp=1,ss01=2" never reached — loop exits read

One-character fix: breakcontinue.

Tests

  • Default font features round-trip through save/load.
  • The clobber, pinned failing-before / passing-after.
  • Explicit defaults still correctly propagate to overrides (LT-18109 behavior preserved).
  • Explicit default font name still wins across writing systems.
  • Bullet font decode round-trips both font name and features.

Full suite green on net462 and net8.0 (SIL.LCModel.Tests 1706/1706, SIL.LCModel.Core.Tests 787/787, SIL.LCModel.Utils.Tests 302 passed/2 skipped, SIL.LCModel.FixData.Tests 21/21) — no regressions.

References JIRA LT-22351. A companion FieldWorks PR (#1005) will consume this liblcm fix once merged and released.

🤖 Generated with Claude Code


This change is Reviewable

…eRules

BaseStyleInfo.ProcessStyleRules iterates the string properties of a
style's TsTextProps and handles ktptFontFamily, ktptBulNumTxtBef/Aft,
ktptCustomBullet, ktptBulNumFontInfo, and ktptWsStyle, but had no case
for ktptFontVariations (font features). As a result, font features set
as a style-level default (not a per-writing-system override) fell
through to the default: break and were silently dropped when the style
was loaded/reloaded.

The save path already writes ktptFontVariations for style defaults
(see GetOverridesString), and SetFontStringProp already knows how to
apply it to a FontInfo -- it's just never called for the default font
info, only for per-WS overrides via MakeFontWsOverrides. This created
a save/load asymmetry: a user could set font features as a style
default via the Styles dialog, save, and have them vanish on reload.

Fix: add a case for ktptFontVariations in the string-prop switch in
ProcessStyleRules that sets m_defaultFontInfo.m_features.ExplicitValue,
mirroring how ktptFontFamily sets m_fontName.

Added BaseStyleInfoTests.ConstructBasedOnStyle_FontFeatures, which
builds style rules with ktptFontVariations set as a default (no WS
override) and asserts FontInfoForWs(-1).m_features.IsExplicit is true
with the value round-tripping exactly. Confirmed the test fails before
the fix (IsExplicit false) and passes after, with no regressions in
the broader DomainServices test suite (488 passed / 0 failed on both
net462 and net8.0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@johnml1135

Copy link
Copy Markdown
Contributor Author

Companion FieldWorks PR consuming this fix: sillsdev/FieldWorks#1005 (draft, pending a liblcm release containing this change).

johnml1135 and others added 3 commits July 10, 2026 15:36
Review follow-ups on the ktptFontVariations load fix:

Loading font features into the default font info made
FontInfo.IsAnyExplicit true for styles whose only explicit style-level
property is font features. That newly triggered the branch in
SetNonExplicitPropertiesToInherited that pushes the default font info
into each ws-specific override via InheritAllProperties -- AFTER the
override had already inherited the based-on style's corresponding ws
override. Since InheritValue overwrites the value of every
still-inherited property (last caller wins), the second pass clobbered
what the first pass set: e.g. a based-on style with ws override
FontName=Fontastic for English but style-level default Times New Roman
would flip a child style's English override font to Times New Roman
when the child's only explicit default was font features.

Fix: add FontInfo.InheritExplicitProperties, which propagates only the
explicitly-set properties of the source font info, and use it for the
default-into-override pass. The default's other values were merely
inherited from the based-on style's default and must not overwrite
values just inherited from the based-on style's ws override.

Note this intentionally changes long-standing behavior for styles with
any explicit default font property (e.g. an explicit default font
name): previously ALL still-inherited override properties were
overwritten with the default font info's values; now only the
explicitly-set default properties are propagated. The old behavior was
the same clobber bug reached an older way.

Also per review:
- ProcessStyleRules now delegates the ktptFontVariations case to
  SetFontStringProp, keeping the tag-to-field mapping in one place
  (mirrors how the int-prop loop delegates to SetExplicitFontIntProp).
- ConstructBasedOnStyle_FontFeatures now asserts the font family it
  sets up, matching the sibling ConstructBasedOnStyle test.

Added SetBasedOnStyleAndInheritValues_ExplicitDefaultFeaturesDoNot-
ClobberWsOverride, which fails before the fix (English override font
"Times New Roman" instead of "Fontastic") and passes after. Full
solution test suite is green on net462 and net8.0 (SIL.LCModel.Tests
1704 passed, Core.Tests 787, Utils.Tests 302, FixData.Tests 21).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tring prop

In BulletInfo.DecodeFontInfo, the ktptFontFamily and ktptFontVariations
branches ended with loop-level break statements, so decoding stopped
entirely at the first string property encountered. EncodeFontInfo
always writes the font family before the font variations, which meant a
bullet font with both a font name and font features lost its features
on every decode -- another save/load asymmetry for ktptFontVariations.

Fix: continue the loop after handling each string property instead of
breaking out of it. The manual index advancement past the property's
terminating NUL already leaves the index at the next property, so the
int-prop parsing below is unaffected.

Added BulletInfoTests.RoundTripEncodingAndDecodingOfFontInfo_FontName-
AndFontFeatures, an encode/decode round trip with both m_fontName and
m_features explicit. It fails before the fix (features not explicit
after decode) and passes after, along with the rest of BulletInfoTests
and the full solution test suite on net462 and net8.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pin the intended behavior of the IsAnyExplicit block that commit
113a768 (Fix for LT-18109 - Inherit writing system overrides, 2017)
added to SetNonExplicitPropertiesToInherited, proving that the
InheritExplicitProperties change preserves it:

- SetBasedOnStyleAndInheritValues_ExplicitDefaultPropsPropagateInto-
  WsOverride: a child style's explicit style-level default (bold)
  shows through in its ws override, while the font name the child does
  not set explicitly keeps the value inherited from the parent's ws
  override.
- SetBasedOnStyleAndInheritValues_ExplicitDefaultFontNameBeatsParent-
  WsOverride: a child style's explicit style-level default font name
  applies across writing systems and wins over the parent's ws
  override, per the LT-18109 commit message ("If the default font info
  in a style specifies overrides the defaults overrule the parent
  style ws overrides").

Together with SetBasedOnStyleAndInheritValues_ExplicitDefaultFeatures-
DoNotClobberWsOverride these pin all three semantics: no clobber of
inherited ws-override values by merely-inherited defaults, propagation
of explicit defaults into ws overrides, and explicit-default precedence
over the parent's ws override. Both new tests pass with no production
change; full solution suite green on net462 and net8.0
(SIL.LCModel.Tests 1706 passed / 0 failed / 18 skipped, Core.Tests
787/787, Utils.Tests 302 passed / 2 skipped, FixData.Tests 21/21).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@johnml1135

Copy link
Copy Markdown
Contributor Author

Regarding the question of whether the original one-line load fix was preferable to the follow-up inheritance change: the PR description now has a dedicated subsection, "Why the inheritance change is needed (v1 vs v2)", laying out the evidence -- including why the one-line fix alone regresses ws-override inheritance for styles whose only style-level setting is font features, how the change preserves the intent of the original LT-18109 commit (113a768), and the alternative that was considered and rejected.

Commit 66eda1b adds two tests that pin the LT-18109 semantics under the new code (explicit defaults propagate into ws overrides, and an explicit default font name wins over the parent's ws override); both pass with no further production change, and the full suite is green on net462 and net8.0.

johnml1135 added a commit to sillsdev/FieldWorks that referenced this pull request Jul 10, 2026
…feature precedence

Review follow-ups on the LT-22351 branch:

- Widen SafelyAddStyleToSheetAndTable to accept BaseStyleInfo (both
  collections it touches already store BaseStyleInfo) and add a matching
  SafelyRemoveStyleFromSheetAndTable helper.
- The persisted-Rules preview test now uses those helpers and removes its
  style in a finally block: m_styleSheet is per-fixture but the base class
  undoes all data changes per test, so leaving a BaseStyleInfo wrapping a
  real IStStyle behind would strand a stale entry with a dead RealStyle
  that can crash later tests order-dependently.
- Add GenerateCssForConfiguration_NormalStyleOwnFontFeatures_
  BeatWritingSystemDefaultFontFeatures: now that liblcm loads a style's
  own default ktptFontVariations from persisted Rules (sillsdev/liblcm#388),
  a Normal style WITH its own features must win over the writing system's
  DefaultFontFeatures fallback in AddFontInfoCss; the existing sibling test
  only covered Normal WITHOUT its own features.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 added a commit to sillsdev/FieldWorks that referenced this pull request Jul 10, 2026
… and precedence

The Word export path (WordStylesGenerator.AddFontInfoWordStyles) has the
same precedence logic as the CSS path but had no coverage through the
authoritative liblcm load path: the existing typography test used a
TestStyle double with an in-memory ExplicitValue (bypassing
BaseStyleInfo.ProcessStyleRules) and the existing Normal-style test only
covered the writing-system DefaultFontFeatures fallback branch.

Add two tests mirroring the CSS-side coverage:

- GenerateCharacterStyleFromLcmStyleSheet_DefaultFontFeaturesFromPersisted
  StyleRules_AddsWordTypographyProperties: a real IStStyle whose persisted
  Rules carry ktptFontVariations, wrapped in a plain BaseStyleInfo the way
  LcmStyleSheet does in production, produces the expected w14 typography
  properties (sillsdev/liblcm#388).
- GenerateCharacterStyleFromLcmStyleSheet_NormalStyleOwnFontFeatures_Beat
  WritingSystemDefaultFontFeatures: a Normal style with its own persisted
  features wins over the WS DefaultFontFeatures fallback (font family
  still falls back to the WS default font).

Both tests clean up in finally blocks so no stale entry wrapping an
undone IStStyle is left in the fixture-lifetime stylesheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 added a commit to sillsdev/FieldWorks that referenced this pull request Jul 10, 2026
design.md Decision 11, proposal.md, and research.md still described
StyleInfo.LoadDefaultFontFeatures as an existing, load-bearing
compatibility adapter. Update those passages to record that the adapter
was removed under LT-22351 once liblcm's BaseStyleInfo.ProcessStyleRules
gained the ktptFontVariations case (sillsdev/liblcm#388) and the Decision
11 gating condition (SaveToDB_DefaultFontFeatures_RoundTripsThroughRules
passing through the authoritative path) was satisfied. Surgical wording
updates only; no restructuring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 added a commit to sillsdev/FieldWorks that referenced this pull request Jul 13, 2026
liblcm's BaseStyleInfo.ProcessStyleRules previously dropped a style's
default ktptFontVariations (font features) when loading from persisted
Rules (sillsdev/liblcm#388). FieldWorks compensated for this in the Styles
dialog only, via StyleInfo.LoadDefaultFontFeatures, which manually rescanned
style.Rules for ktptFontVariations after calling the base constructor. The
Dictionary Preview CSS path (CssGenerator.GenerateCssStyleFromLcmStyleSheet
-> AddFontInfoCss) never went through StyleInfo -- it reads plain
BaseStyleInfo objects straight out of LcmStyleSheet -- so it had no such
adapter and silently lost default font features, meaning
font-feature-settings never reached the generated preview CSS.

Now that liblcm loads default font features itself, remove the
FieldWorks-side adapter and let StyleInfo(IStStyle) rely entirely on
BaseStyleInfo.ProcessStyleRules, as the LT-22324 design doc intended once
the round-trip test passed through the authoritative liblcm path.

Add a CssGenerator regression test that builds a style the way
LcmStyleSheet does in production (a real IStStyle with persisted Rules,
wrapped in BaseStyleInfo -- not a TestStyle double with an in-memory
ExplicitValue) and asserts that default font features reach the generated
CSS end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 added a commit to sillsdev/FieldWorks that referenced this pull request Jul 13, 2026
Review follow-ups on the LT-22351 branch:

- Widen SafelyAddStyleToSheetAndTable to accept BaseStyleInfo (both
  collections it touches already store BaseStyleInfo) and add a matching
  SafelyRemoveStyleFromSheetAndTable helper.
- The persisted-Rules preview test now uses those helpers and removes its
  style in a finally block: m_styleSheet is per-fixture but the base class
  undoes all data changes per test, so leaving a BaseStyleInfo wrapping a
  real IStStyle behind would strand a stale entry with a dead RealStyle
  that can crash later tests order-dependently.
- Add GenerateCssForConfiguration_NormalStyleOwnFontFeatures_
  BeatWritingSystemDefaultFontFeatures: now that liblcm loads a style's
  own default ktptFontVariations from persisted Rules (sillsdev/liblcm#388),
  a Normal style WITH its own features must win over the writing system's
  DefaultFontFeatures fallback in AddFontInfoCss; the existing sibling test
  only covered Normal WITHOUT its own features.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 added a commit to sillsdev/FieldWorks that referenced this pull request Jul 13, 2026
The Word export path (WordStylesGenerator.AddFontInfoWordStyles) has the
same precedence logic as the CSS path but had no coverage through the
authoritative liblcm load path: the existing typography test used a
TestStyle double with an in-memory ExplicitValue (bypassing
BaseStyleInfo.ProcessStyleRules) and the existing Normal-style test only
covered the writing-system DefaultFontFeatures fallback branch.

Add two tests mirroring the CSS-side coverage:

- GenerateCharacterStyleFromLcmStyleSheet_DefaultFontFeaturesFromPersisted
  StyleRules_AddsWordTypographyProperties: a real IStStyle whose persisted
  Rules carry ktptFontVariations, wrapped in a plain BaseStyleInfo the way
  LcmStyleSheet does in production, produces the expected w14 typography
  properties (sillsdev/liblcm#388).
- GenerateCharacterStyleFromLcmStyleSheet_NormalStyleOwnFontFeatures_Beat
  WritingSystemDefaultFontFeatures: a Normal style with its own persisted
  features wins over the WS DefaultFontFeatures fallback (font family
  still falls back to the WS default font).

Both tests clean up in finally blocks so no stale entry wrapping an
undone IStStyle is left in the fixture-lifetime stylesheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 added a commit to sillsdev/FieldWorks that referenced this pull request Jul 13, 2026
design.md Decision 11, proposal.md, and research.md still described
StyleInfo.LoadDefaultFontFeatures as an existing, load-bearing
compatibility adapter. Update those passages to record that the adapter
was removed under LT-22351 once liblcm's BaseStyleInfo.ProcessStyleRules
gained the ktptFontVariations case (sillsdev/liblcm#388) and the Decision
11 gating condition (SaveToDB_DefaultFontFeatures_RoundTripsThroughRules
passing through the authoritative path) was satisfied. Surgical wording
updates only; no restructuring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@jasonleenaylor jasonleenaylor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm:

Make sure that we squash with a clean and concise commit comment describing the 3 different changes in appropriate detail.

@jasonleenaylor reviewed 6 files and all commit messages, and made 1 comment.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on johnml1135).

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.

2 participants