diff --git a/src/core/domain/grammar/implementations/SpacingRule.ts b/src/core/domain/grammar/implementations/SpacingRule.ts index 4efbb7ea..3c64716e 100644 --- a/src/core/domain/grammar/implementations/SpacingRule.ts +++ b/src/core/domain/grammar/implementations/SpacingRule.ts @@ -107,14 +107,6 @@ export class SpacingRule extends SpacingRuleShared implements GrammarRule { return this.createTechnicalCompactionEdit(":", lastChar, "time or ratio notation"); } - if ( - punctChar === "." && - this.isIdentifierStartChar(lastChar) && - this.shouldCompactAccessor(inputStr, length - 3) - ) { - return this.createTechnicalCompactionEdit(".", lastChar, "code accessor"); - } - return null; } diff --git a/src/core/domain/grammar/implementations/TechnicalTokenCompactionRule.ts b/src/core/domain/grammar/implementations/TechnicalTokenCompactionRule.ts index 7ce46b60..60b02be4 100644 --- a/src/core/domain/grammar/implementations/TechnicalTokenCompactionRule.ts +++ b/src/core/domain/grammar/implementations/TechnicalTokenCompactionRule.ts @@ -31,14 +31,6 @@ export class TechnicalTokenCompactionRule extends SpacingRuleShared implements G return this.createEdit(`:${lastChar}`, 3, "Compacted technical time or ratio notation"); } - if ( - punctChar === "." && - this.isIdentifierStartChar(lastChar) && - this.shouldCompactAccessor(inputStr, length - 3) - ) { - return this.createEdit(`.${lastChar}`, 3, "Compacted technical accessor spacing"); - } - return null; } } diff --git a/src/core/domain/grammar/implementations/helpers/SpacingRuleShared.ts b/src/core/domain/grammar/implementations/helpers/SpacingRuleShared.ts index 665b21c0..414b46ce 100644 --- a/src/core/domain/grammar/implementations/helpers/SpacingRuleShared.ts +++ b/src/core/domain/grammar/implementations/helpers/SpacingRuleShared.ts @@ -174,35 +174,6 @@ export abstract class SpacingRuleShared { return leftSingleIdentifier && rightSingleIdentifier; } - protected shouldCompactAccessor(inputStr: string, punctIndex: number): boolean { - const tokenEnd = punctIndex - 1; - let tokenStart = tokenEnd; - - while (tokenStart >= 0 && this.isIdentifierChar(inputStr[tokenStart])) { - tokenStart -= 1; - } - - tokenStart += 1; - if (tokenStart > tokenEnd) { - return false; - } - - const previousSignificantIndex = this.findPreviousSignificantIndex(inputStr, tokenStart - 1); - if (previousSignificantIndex === null) { - const token = inputStr.slice(tokenStart, tokenEnd + 1); - return /\d/.test(token) || token.startsWith("$"); - } - - // Treat cue chars as code context only when tightly attached to the token - // before the dot (e.g. "obj.user. x"), not across sentence whitespace. - if (previousSignificantIndex !== tokenStart - 1) { - return false; - } - - const previousSignificant = inputStr[previousSignificantIndex]; - return previousSignificant === "." || SpacingRuleShared.CODE_CUE_CHARS.has(previousSignificant); - } - protected isTightlyAttached(inputStr: string, index: number): boolean { if (index <= 0) { return false; diff --git a/src/ui/options/fluenttyperI18n.ts b/src/ui/options/fluenttyperI18n.ts index a8a115b7..3e153bda 100644 --- a/src/ui/options/fluenttyperI18n.ts +++ b/src/ui/options/fluenttyperI18n.ts @@ -1724,15 +1724,15 @@ i18n.extend({ pr: "Compactação de tokens técnicos", }, grammar_rule_technical_compaction_desc: { - en: "Compacts spaces inside technical tokens such as decimals, times, and code accessors.", - fr: "Supprime les espaces à l'intérieur des éléments techniques tels que décimaux, heures et accesseurs de code.", - hr: "Uklanja razmake unutar tehničkih tokena poput decimala, vremena i pristupnika u kodu.", - es: "Compacta espacios dentro de tokens técnicos como decimales, horas y accesores de código.", - el: "Συμπτύσσει κενά μέσα σε τεχνικά σύμβολα όπως δεκαδικοί αριθμοί, ώρες και προσπελάσεις κώδικα.", - sv: "Tar bort mellanslag i tekniska token som decimaler, tider och kodaccessorer.", - de: "Entfernt Leerzeichen innerhalb technischer Tokens wie Dezimalzahlen, Zeiten und Code-Zugriffen.", - pl: "Usuwa spacje wewnątrz tokenów technicznych, takich jak liczby dziesiętne, godziny i akcesory kodu.", - pr: "Remove espaços dentro de tokens técnicos como decimais, horários e acessores de código.", + en: "Compacts spaces inside numeric tokens such as decimals, times, and ratios.", + fr: "Supprime les espaces à l'intérieur des éléments numériques tels que décimaux, heures et rapports.", + hr: "Uklanja razmake unutar numeričkih tokena poput decimala, vremena i omjera.", + es: "Compacta espacios dentro de tokens numéricos como decimales, horas y proporciones.", + el: "Συμπτύσσει κενά μέσα σε αριθμητικά σύμβολα όπως δεκαδικοί αριθμοί, ώρες και αναλογίες.", + sv: "Tar bort mellanslag i numeriska token som decimaler, tider och förhållanden.", + de: "Entfernt Leerzeichen innerhalb numerischer Tokens wie Dezimalzahlen, Zeiten und Verhältnissen.", + pl: "Usuwa spacje wewnątrz tokenów liczbowych, takich jak liczby dziesiętne, godziny i proporcje.", + pr: "Remove espaços dentro de tokens numéricos como decimais, horários e proporções.", }, grammar_rule_technical_compaction_example: { en: 'Example: "3. 14" -> "3.14"', diff --git a/tests/e2e/coverage-matrix.json b/tests/e2e/coverage-matrix.json index cd7fab9c..4950c05e 100644 --- a/tests/e2e/coverage-matrix.json +++ b/tests/e2e/coverage-matrix.json @@ -804,17 +804,17 @@ }, { "id": "grammar_punctuation_spacing", - "description": "Grammar spacing compacts technical punctuation while preserving prose continuation.", + "description": "Grammar spacing compacts numeric punctuation while preserving dotted-word continuation.", "coverage": [ { "layer": "e2e-full", "file": "tests/e2e/full.e2e.test.ts", - "test": "Grammar Rule Engine compacts technical punctuation spacing and preserves prose continuation" + "test": "Grammar Rule Engine compacts numeric punctuation spacing and preserves prose continuation" }, { "layer": "unit", "file": "tests/grammar/V1GrammarRules.test.ts", - "test": "compacts decimal, time/ratio, and accessor spacing conservatively" + "test": "compacts decimal and time/ratio spacing" } ] }, diff --git a/tests/e2e/full.e2e.test.ts b/tests/e2e/full.e2e.test.ts index 714e1c01..1effd565 100644 --- a/tests/e2e/full.e2e.test.ts +++ b/tests/e2e/full.e2e.test.ts @@ -5028,7 +5028,7 @@ describeE2E(`Extension E2E Test [${BROWSER_TYPE}]`, () => { ); test( - "Grammar Rule Engine compacts technical punctuation spacing and preserves prose continuation", + "Grammar Rule Engine compacts numeric punctuation spacing and preserves prose continuation", async () => { const selector = "#test-input"; @@ -5073,16 +5073,25 @@ describeE2E(`Extension E2E Test [${BROWSER_TYPE}]`, () => { await typeInInput(page, selector, "12:30"); await waitForNormalizedValue("12:30"); - await clearInputContent(page, selector); - await typeInInput(page, selector, "cfg_1.x"); - await waitForNormalizedValue("cfg_1.x"); - await clearInputContent(page, selector); await typeInInput(page, selector, "Hello."); await waitForNormalizedValue("Hello. "); await typeInInput(page, selector, "w"); await waitForNormalizedValue("Hello. w"); + await clearInputContent(page, selector); + // Prime an already-authored abbreviation so this isolates typing after its trailing space. + await page.$eval(selector, (element) => { + if (!(element instanceof HTMLInputElement)) { + throw new Error("Expected test input"); + } + element.value = "9 a.m. "; + element.focus(); + element.setSelectionRange(element.value.length, element.value.length); + }); + await typeInInput(page, selector, "and"); + await waitForNormalizedValue("9 a.m. and"); + await setSettingAndWaitStable( worker!, KEY_ENABLED_GRAMMAR_RULES, diff --git a/tests/grammar/SpacingRule.test.ts b/tests/grammar/SpacingRule.test.ts index 37becdea..cbfd5996 100644 --- a/tests/grammar/SpacingRule.test.ts +++ b/tests/grammar/SpacingRule.test.ts @@ -289,35 +289,16 @@ describe("SpacingRule", () => { }); }); - test("compacts conservative accessor contexts with code cues", () => { - expect(ruleA.apply(getContext("obj.cfg_1.\xA0x"))).toEqual({ - replacement: ".x", - deleteBackwards: 3, - deleteForwards: 0, - confidence: "high", - description: "Compacted technical punctuation spacing for code accessor", - }); - - expect(ruleA.apply(getContext("obj.user.\xA0n"))).toEqual({ - replacement: ".n", - deleteBackwards: 3, - deleteForwards: 0, - confidence: "high", - description: "Compacted technical punctuation spacing for code accessor", - }); - - expect(ruleA.apply(getContext("cfg_1.\xA0x"))).toEqual({ - replacement: ".x", - deleteBackwards: 3, - deleteForwards: 0, - confidence: "high", - description: "Compacted technical punctuation spacing for code accessor", - }); - }); - - test("does not compact prose continuation without code cues", () => { + test("preserves spacing after dotted words without language-specific detection", () => { expect(ruleA.apply(getContext("Hello.\xA0w"))).toBeNull(); expect(ruleA.apply(getContext("old_word.\xA0X"))).toBeNull(); expect(ruleA.apply(getContext("Read on.\xA0Duplicate.\xA0W"))).toBeNull(); + expect(ruleA.apply(getContext("obj.cfg_1.\xA0x"))).toBeNull(); + expect(ruleA.apply(getContext("a.b.\xA0c"))).toBeNull(); + expect(ruleA.apply(getContext("return a.b.\xA0c"))).toBeNull(); + expect(ruleA.apply(getContext("9 a.m.\xA0a"))).toBeNull(); + expect(ruleA.apply(getContext("Use e.g.\xA0examples"))).toBeNull(); + expect(ruleA.apply(getContext("Siehe z.B.\xA0examples"))).toBeNull(); + expect(ruleA.apply(getContext("Użyj m.in.\xA0examples"))).toBeNull(); }); }); diff --git a/tests/grammar/V1GrammarRules.test.ts b/tests/grammar/V1GrammarRules.test.ts index 54673487..5f0921ce 100644 --- a/tests/grammar/V1GrammarRules.test.ts +++ b/tests/grammar/V1GrammarRules.test.ts @@ -280,7 +280,7 @@ describe("V1 grammar rules", () => { }); describe("TechnicalTokenCompactionRule", () => { - test("compacts decimal, time/ratio, and accessor spacing conservatively", () => { + test("compacts decimal and time/ratio spacing", () => { const rule = new TechnicalTokenCompactionRule(true); expect(rule.apply(context("3. 1"))).toEqual({ @@ -298,21 +298,20 @@ describe("V1 grammar rules", () => { confidence: "high", description: "Compacted technical time or ratio notation", }); - - expect(rule.apply(context("obj.cfg_1. x"))).toEqual({ - replacement: ".x", - deleteBackwards: 3, - deleteForwards: 0, - confidence: "high", - description: "Compacted technical accessor spacing", - }); }); - test("does not compact prose continuation", () => { + test("preserves spacing after dotted words without language-specific detection", () => { const rule = new TechnicalTokenCompactionRule(true); expect(rule.apply(context("Hello. w"))).toBeNull(); expect(rule.apply(context("old_word. X"))).toBeNull(); expect(rule.apply(context("Read on. Duplicate. W"))).toBeNull(); + expect(rule.apply(context("obj.cfg_1. x"))).toBeNull(); + expect(rule.apply(context("a.b. c"))).toBeNull(); + expect(rule.apply(context("return a.b. c"))).toBeNull(); + expect(rule.apply(context("9 a.m. a"))).toBeNull(); + expect(rule.apply(context("Use e.g. examples"))).toBeNull(); + expect(rule.apply(context("Siehe z.B. examples"))).toBeNull(); + expect(rule.apply(context("Użyj m.in. examples"))).toBeNull(); }); });