diff --git a/Build/SilVersions.props b/Build/SilVersions.props
index b90cd986fc..6f9db561ad 100644
--- a/Build/SilVersions.props
+++ b/Build/SilVersions.props
@@ -12,7 +12,7 @@
=============================================================
-->
- 11.0.0-beta0173
+ 11.0.0-beta0178
18.0.0-beta0027
18.0.0-beta0012
6.0.0-beta0065
@@ -24,4 +24,4 @@
70.1.152
60.0.56
-
\ No newline at end of file
+
diff --git a/Src/FwCoreDlgs/FwCoreDlgControls/StyleInfo.cs b/Src/FwCoreDlgs/FwCoreDlgControls/StyleInfo.cs
index d6a4f85833..0b7fb7dbeb 100644
--- a/Src/FwCoreDlgs/FwCoreDlgControls/StyleInfo.cs
+++ b/Src/FwCoreDlgs/FwCoreDlgControls/StyleInfo.cs
@@ -40,24 +40,6 @@ public class StyleInfo : BaseStyleInfo
public StyleInfo(IStStyle style)
: base(style)
{
- LoadDefaultFontFeatures(style);
- }
-
- private void LoadDefaultFontFeatures(IStStyle style)
- {
- if (style == null || style.Rules == null)
- return;
-
- for (int i = 0; i < style.Rules.StrPropCount; i++)
- {
- int tpt;
- string value = style.Rules.GetStrProp(i, out tpt);
- if (tpt == (int)FwTextPropType.ktptFontVariations)
- {
- m_defaultFontInfo.m_features.ExplicitValue = value;
- return;
- }
- }
}
/// ------------------------------------------------------------------------------------
diff --git a/Src/xWorks/xWorksTests/CssGeneratorTests.cs b/Src/xWorks/xWorksTests/CssGeneratorTests.cs
index 58870627bf..e069e3f39b 100644
--- a/Src/xWorks/xWorksTests/CssGeneratorTests.cs
+++ b/Src/xWorks/xWorksTests/CssGeneratorTests.cs
@@ -2414,6 +2414,36 @@ public void GenerateCssForConfiguration_CustomPrintableAsciiFontFeatures_AreEsca
Does.Contain("font-feature-settings:\"!abc\" 2,\"a\\\"b\\\\\" 1"));
}
+ [Test]
+ public void GenerateCssForConfiguration_DefaultFontFeaturesFromPersistedStyleRules_ReachPreviewCss()
+ {
+ ConfiguredLcmGenerator.AssemblyFile = "xWorksTests";
+ const string styleName = "DefaultFontFeaturesStyle";
+ var persistedStyle = CreateStyleInfoFromPersistedRules(styleName, StyleType.kstCharacter, "smcp=1");
+ SafelyAddStyleToSheetAndTable(styleName, persistedStyle);
+ try
+ {
+ var headwordNode = new ConfigurableDictionaryNode
+ {
+ FieldDescription = "SIL.FieldWorks.XWorks.TestRootClass",
+ Label = "Headword",
+ DictionaryNodeOptions = ConfiguredXHTMLGeneratorTests.GetWsOptionsForLanguages(new[] { "fr" }),
+ Style = styleName,
+ IsEnabled = true
+ };
+
+ var model = new DictionaryConfigurationModel { Parts = new List { headwordNode } };
+ var cssResult = CssGenerator.GenerateCssFromConfiguration(model, m_propertyTable);
+
+ Assert.That(cssResult, Does.Contain("font-feature-settings:\"smcp\" 1"));
+ }
+ finally
+ {
+ // Fixture-owned collections outlive the model changes made by this test.
+ SafelyRemoveStyleFromSheetAndTable(styleName);
+ }
+ }
+
[Test]
public void GenerateCssForConfiguration_ReversalSenseNumberWorks()
{
@@ -3150,6 +3180,50 @@ public void GenerateCssForConfiguration_WsSpanWithNormalStyle_UsesWritingSystemD
Assert.That(cssResult, Contains.Substring("span[lang='" + vernWs.LanguageTag + "']{font-family:'Charis SIL',serif;font-feature-settings:\"ss11\" 1,\"ss12\" 1;"));
}
+ [Test]
+ public void GenerateCssForConfiguration_NormalStyleOwnFontFeatures_BeatWritingSystemDefaultFontFeatures()
+ {
+ const string styleName = "Normal";
+ var vernWs = Cache.ServiceLocator.WritingSystemManager.Get(Cache.DefaultVernWs);
+ vernWs.DefaultFont = new FontDefinition("Charis SIL") { Features = "ss11=1,ss12=1" };
+
+ var persistedStyle = CreateStyleInfoFromPersistedRules(styleName, StyleType.kstParagraph, "smcp=1");
+ SafelyAddStyleToSheetAndTable(styleName, persistedStyle);
+ try
+ {
+ var glossNode = new ConfigurableDictionaryNode
+ {
+ FieldDescription = "Gloss",
+ DictionaryNodeOptions = ConfiguredXHTMLGeneratorTests.GetWsOptionsForLanguages(new[] { vernWs.LanguageTag })
+ };
+ var testSensesNode = new ConfigurableDictionaryNode
+ {
+ FieldDescription = "Senses",
+ Children = new List { glossNode }
+ };
+ var testEntryNode = new ConfigurableDictionaryNode
+ {
+ FieldDescription = "LexEntry",
+ Children = new List { testSensesNode }
+ };
+ var model = new DictionaryConfigurationModel
+ {
+ Parts = new List { testEntryNode }
+ };
+ PopulateFieldsForTesting(testEntryNode);
+
+ var cssResult = Regex.Replace(CssGenerator.GenerateCssFromConfiguration(model, m_propertyTable), @"\t|\n|\r", "");
+
+ Assert.That(cssResult, Contains.Substring("span[lang='" + vernWs.LanguageTag + "']{font-family:'Charis SIL',serif;font-feature-settings:\"smcp\" 1;"));
+ Assert.That(cssResult, Does.Not.Contain("ss11"));
+ }
+ finally
+ {
+ // Fixture-owned collections outlive the model changes made by this test.
+ SafelyRemoveStyleFromSheetAndTable(styleName);
+ }
+ }
+
[Test]
public void GenerateCssForConfiguration_NormalStyleForWsDoesNotOverrideNodeStyle()
{
@@ -4126,7 +4200,22 @@ private static TestStyle GenerateStyleFromFontInfo(LcmCache cache, string name,
return new TestStyle(fontInfo, cache) { Name = name, IsParagraphStyle = isParagraphStyle };
}
- private void SafelyAddStyleToSheetAndTable(string name, TestStyle style)
+ private BaseStyleInfo CreateStyleInfoFromPersistedRules(string name, StyleType type, string fontFeatures)
+ {
+ var style = Cache.ServiceLocator.GetInstance().Create();
+ Cache.LanguageProject.StylesOC.Add(style);
+ style.Name = name;
+ style.Context = ContextValues.Internal;
+ style.Function = FunctionValues.Prose;
+ style.Structure = StructureValues.Undefined;
+ style.Type = type;
+ var propsBldr = TsStringUtils.MakePropsBldr();
+ propsBldr.SetStrPropValue((int)FwTextPropType.ktptFontVariations, fontFeatures);
+ style.Rules = propsBldr.GetTextProps();
+ return new BaseStyleInfo(style);
+ }
+
+ private void SafelyAddStyleToSheetAndTable(string name, BaseStyleInfo style)
{
if (m_styleSheet.Styles.Contains(name))
m_styleSheet.Styles.Remove(name);
@@ -4136,6 +4225,14 @@ private void SafelyAddStyleToSheetAndTable(string name, TestStyle style)
m_owningTable.Add(name, style);
}
+ private void SafelyRemoveStyleFromSheetAndTable(string name)
+ {
+ if (m_styleSheet.Styles.Contains(name))
+ m_styleSheet.Styles.Remove(name);
+ if (m_owningTable.ContainsKey(name))
+ m_owningTable.Remove(name);
+ }
+
private void GenerateBulletStyle(string name)
{
var fontInfo = new FontInfo();
diff --git a/Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs b/Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs
index b535cc954c..31c8c2ee8c 100644
--- a/Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs
+++ b/Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs
@@ -328,6 +328,68 @@ public void GenerateCharacterStyleFromLcmStyleSheet_NormalStyle_UsesWritingSyste
Is.EqualTo(new[] { W14.OnOffValues.True, W14.OnOffValues.True }));
}
+ [Test]
+ public void GenerateCharacterStyleFromLcmStyleSheet_DefaultFontFeaturesFromPersistedStyleRules_AddsWordTypographyProperties()
+ {
+ var styleName = "WordFeatureStylePersisted" + Guid.NewGuid().ToString("N");
+ var styles = FontHeightAdjuster.StyleSheetFromPropertyTable(m_propertyTable).Styles;
+ styles.Add(CreateStyleInfoFromPersistedRules(styleName, StyleType.kstCharacter,
+ "liga=0,lnum=1,pnum=1,calt=0,ss02=0,cv01=2"));
+ try
+ {
+ var style = WordStylesGenerator.GenerateCharacterStyleFromLcmStyleSheet(styleName, Cache.DefaultVernWs,
+ new ReadOnlyPropertyTable(m_propertyTable));
+
+ var runProps = style.GetFirstChild();
+ AssertWordTypographyProperties(runProps, W14.LigaturesValues.None, W14.NumberFormValues.Lining,
+ W14.NumberSpacingValues.Proportional, false, 2U, false);
+ }
+ finally
+ {
+ // Fixture-owned styles outlive the model changes made by this test.
+ styles.Remove(styleName);
+ }
+ }
+
+ [Test]
+ public void GenerateCharacterStyleFromLcmStyleSheet_NormalStyleOwnFontFeatures_BeatWritingSystemDefaultFontFeatures()
+ {
+ var vernWs = Cache.ServiceLocator.WritingSystemManager.Get(Cache.DefaultVernWs);
+ vernWs.DefaultFont = new FontDefinition("Charis SIL") { Features = "ss11=1,ss12=1" };
+
+ var styles = FontHeightAdjuster.StyleSheetFromPropertyTable(m_propertyTable).Styles;
+ if (styles.Contains(WordStylesGenerator.NormalParagraphStyleName))
+ styles.Remove(WordStylesGenerator.NormalParagraphStyleName);
+ styles.Add(CreateStyleInfoFromPersistedRules(WordStylesGenerator.NormalParagraphStyleName,
+ StyleType.kstParagraph, "ss02=1"));
+ try
+ {
+ var style = WordStylesGenerator.GenerateCharacterStyleFromLcmStyleSheet(
+ WordStylesGenerator.NormalParagraphStyleName,
+ vernWs.Handle,
+ new ReadOnlyPropertyTable(m_propertyTable));
+
+ var runProps = style.GetFirstChild();
+ Assert.That(runProps, Is.Not.Null);
+
+ var runFonts = runProps.GetFirstChild();
+ Assert.That(runFonts, Is.Not.Null);
+ Assert.That(runFonts.Ascii?.Value, Is.EqualTo("Charis SIL"));
+
+ var stylisticSets = runProps.GetFirstChild();
+ Assert.That(stylisticSets, Is.Not.Null);
+ var styleSet = stylisticSets.Elements().Single();
+ Assert.That(styleSet.Id?.Value, Is.EqualTo(2U));
+ Assert.That(styleSet.Val?.Value, Is.EqualTo(W14.OnOffValues.True));
+ }
+ finally
+ {
+ // Fixture-owned styles outlive the model changes made by this test.
+ styles.Remove(WordStylesGenerator.NormalParagraphStyleName);
+ styles.Add(new BaseStyleInfo { Name = WordStylesGenerator.NormalParagraphStyleName, IsParagraphStyle = true });
+ }
+ }
+
[Test]
[Category("ManualDocx")]
public void GenerateManualDocxArtifact_CharisBaseline_NoFontOptions()
@@ -350,6 +412,21 @@ public void GenerateManualDocxArtifact_CharisSs11Ss12()
Assert.That(styleSetIds, Does.Contain(12U));
}
+ private BaseStyleInfo CreateStyleInfoFromPersistedRules(string name, StyleType type, string fontFeatures)
+ {
+ var style = Cache.ServiceLocator.GetInstance().Create();
+ Cache.LanguageProject.StylesOC.Add(style);
+ style.Name = name;
+ style.Context = ContextValues.Internal;
+ style.Function = FunctionValues.Prose;
+ style.Structure = StructureValues.Undefined;
+ style.Type = type;
+ var propsBldr = TsStringUtils.MakePropsBldr();
+ propsBldr.SetStrPropValue((int)FwTextPropType.ktptFontVariations, fontFeatures);
+ style.Rules = propsBldr.GetTextProps();
+ return new BaseStyleInfo(style);
+ }
+
private static void AssertWordTypographyProperties(OpenXmlCompositeElement runProps,
W14.LigaturesValues ligaturesValue, W14.NumberFormValues numberFormValue,
W14.NumberSpacingValues numberSpacingValue, bool contextualAlternativesValue,
diff --git a/openspec/changes/archive/2026-07-14-add-opentype-font-features/design.md b/openspec/changes/archive/2026-07-14-add-opentype-font-features/design.md
index 14936d0767..5cd0fa93e4 100644
--- a/openspec/changes/archive/2026-07-14-add-opentype-font-features/design.md
+++ b/openspec/changes/archive/2026-07-14-add-opentype-font-features/design.md
@@ -17,7 +17,7 @@ The longer product phases are: add OpenType features now, remove Graphite later
- Keep persisted feature strings renderer-neutral and compatible with future Avalonia/HarfBuzz-style consumption.
- Accept any syntactically valid OpenType tag and reject malformed tags safely with trace logging.
- Add trace logging for discovery, validation, native shaping, and fallback decisions.
-- Keep style/default font-feature loading on the existing inheritance path, with only the minimal compatibility adapter still required by the current build graph.
+- Keep style/default font-feature loading on the existing inheritance path.
- Fix truncation and malformed-input robustness gaps in legacy feature-string handling.
- Add tests for UI control behavior and visual rendering differences caused by feature toggles.
- Add test-only HarfBuzzSharp + SkiaSharp comparison tooling for future visual-fidelity confidence.
@@ -119,11 +119,13 @@ The longer product phases are: add OpenType features now, remove Graphite later
### 11. Existing inheritance paths remain authoritative
-**Decision:** `FontInfo.m_features`, `FwTextPropType.ktptFontVariations`, and style rule round-tripping remain the authoritative inheritance/data-flow path for default and explicit font features. `StyleInfo` retains a minimal compatibility adapter that reads default `ktptFontVariations` from `IStStyle.Rules` because focused validation showed that removing it loses persisted default font features in the current build graph.
+**Decision:** `FontInfo.m_features`, `FwTextPropType.ktptFontVariations`, and style rule round-tripping remain the authoritative inheritance/data-flow path for default and explicit font features. `StyleInfo` originally retained a minimal compatibility adapter that read default `ktptFontVariations` from `IStStyle.Rules` because focused validation showed that removing it lost persisted default font features in the then-current build graph.
-**Rationale:** The local LCM source contains `BaseStyleInfo.ProcessStyleRules` support for `ktptFontVariations`, but the active FieldWorks build/test path still requires the `StyleInfo` adapter to reload persisted defaults. The adapter is therefore a compatibility boundary, not a second policy path.
+**Rationale:** At the time of this decision, the active FieldWorks build/test path still required the `StyleInfo` adapter to reload persisted defaults even though the local LCM source contained `BaseStyleInfo.ProcessStyleRules` support for `ktptFontVariations`. The adapter was a compatibility boundary, not a second policy path.
-**Alternatives considered:** Remove the `StyleInfo` adapter immediately. Rejected for this change because `SaveToDB_DefaultFontFeatures_RoundTripsThroughRules` failed after removal. Broader LCM dependency alignment can retire the adapter later with the same round-trip tests as the gate.
+**Alternatives considered:** Remove the `StyleInfo` adapter immediately. Rejected for this change because `SaveToDB_DefaultFontFeatures_RoundTripsThroughRules` failed after removal. Broader LCM dependency alignment could retire the adapter later with the same round-trip tests as the gate.
+
+**Status update (LT-22351):** The gating condition has been satisfied. liblcm's `BaseStyleInfo.ProcessStyleRules` now loads default `ktptFontVariations` (sillsdev/liblcm#388), `SaveToDB_DefaultFontFeatures_RoundTripsThroughRules` passes through the authoritative `BaseStyleInfo` path, and the `StyleInfo.LoadDefaultFontFeatures` adapter was removed under LT-22351.
### 12. Overlong and malformed feature strings fail safe