Skip to content

Commit 4bc8e16

Browse files
committed
docs(formula, plugin-email, lint): disclose the whole double-quoted-argument class in the changesets; drop the stale raw-fallback sentence
Claude-Session: https://claude.ai/code/session_01WGufzhX8CnnYtA7h6jieDA Co-authored-by: Claude <noreply@anthropic.com>
1 parent b0fe927 commit 4bc8e16

5 files changed

Lines changed: 9 additions & 7 deletions

File tree

‎.changeset/27225-mail-formatter-shape-formula.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44

55
feat(formula)!: the mail hole grammar refuses text after a hole's `|` that is not a formatter name, and reads every formatter part exactly as the in-app dialect does
66

7-
Clause-②: no (narrowing) — `parseMailHole` answered `null` for a path followed by `|` whose formatter part failed its own pattern (`{{ due | date-long }}`, `{{ x | two words }}`, `{{ x | }}`), so `judgeMailHoleGrammar` never judged the hole. It now parses the hole and carries a refusal under the new rule `formatter-shape`, which `judgeHoleFormatter` judges for both dialects. No authoring door accepts anything it refused before; the formatter parts the in-app dialect accepts and the mail pattern did not (`currency :EUR`, `default:it's`) were accepted unjudged before and are accepted now. `TemplateHoleGrammarRule` gains the member `'formatter-shape'`; the package index exports nothing new.
7+
Clause-②: no (narrowing) — `parseMailHole` answered `null` for a path followed by `|` whose formatter part failed its own pattern (`{{ due | date-long }}`, `{{ x | two words }}`, `{{ x | }}`), so `judgeMailHoleGrammar` never judged the hole. It now parses the hole and carries a refusal under the new rule `formatter-shape`, which `judgeHoleFormatter` judges for both dialects. One mail spelling is accepted that was refused before: a double-quoted `currency` argument (`currency:"EUR"`) kept its quotes, so `currency-code-literal` refused it, and it now reads `EUR`, publishes and renders, as in-app. The narrowing is the BREAKING arm and decides the level. The formatter parts the in-app dialect accepts and the mail pattern did not (`currency :EUR`, `default:it's`) were accepted unjudged before and are accepted now. `TemplateHoleGrammarRule` gains the member `'formatter-shape'`; the package index exports nothing new.
88
Metadata-protocol: narrowed
99

1010
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no export is removed: a hole whose formatter part never formatted anything (the renderer printed the hole as written) is now refused where the template is judged. A conversion cannot know which formatter the author meant, and the refusal names the hole and states the shape, so there is nothing for a migration to rewrite. -->
1111

1212
**BREAKING** accept-set narrowing in the shared hole grammar.
1313

1414
- **One tokenizer.** `splitHoleFormatter` (`template-engine.ts`) splits a formatter part at its first `:`, trims both sides and removes one enclosing quote mark at each end of the argument. The in-app `parseHole` and the mail `parseMailHole` both read it, so the two dialects accept exactly the same formatter parts. The mail grammar's own pattern is gone.
15-
- **Before.** `parseMailHole(' due | date-long ')` answered `null`: the text was not a hole to any reader, and the mail renderer printed `{{ due | date-long }}` to the recipient. `{{ amount | currency :EUR }}` and `{{ note | default:it's }}` were printed as written too, though the in-app dialect formats them. `default:"n/a"` supplied the text with its double quotes.
16-
- **Now.** A name that is not one word is refused by `formatter-shape`, whose sentence names the hole, states the shape (`{{ path | formatter[:arg] }}`, one word optionally followed by `:` and its argument) and lists `TEMPLATE_FORMATTERS`. A chained hole is still refused by `one-formatter` first, and a one-word unknown name by `known-formatter`. `currency :EUR` is `currency` with `EUR`, `default:it's` supplies `it's`, and `default:"n/a"` supplies `n/a`, as in-app. Brace text whose part before any `|` is not a path (`{{ not a path }}`, `{{ | upper }}`) is still not a hole.
15+
- **Before.** `parseMailHole(' due | date-long ')` answered `null`: the text was not a hole to any reader, and the mail renderer printed `{{ due | date-long }}` to the recipient. `{{ amount | currency :EUR }}` and `{{ note | default:it's }}` were printed as written too, though the in-app dialect formats them. Every double-quoted argument kept its quotes: `default:"n/a"` supplied `"n/a"`, `date:"long"` and `number:"2"` reached their formatter as `"long"` and `"2"` (so `date` fell back to its short style and `number` to no fixed digits), `truncate:"80"` was not a number, `default:""` supplied `""`, and `currency:"EUR"` was refused by `currency-code-literal`.
16+
- **Now.** A name that is not one word is refused by `formatter-shape`, whose sentence names the hole, states the shape (`{{ path | formatter[:arg] }}`, one word optionally followed by `:` and its argument) and lists `TEMPLATE_FORMATTERS`. A chained hole is still refused by `one-formatter` first, and a one-word unknown name by `known-formatter`. `currency :EUR` is `currency` with `EUR`, `default:it's` supplies `it's`, and one enclosing quote mark is removed at each end of every argument, as in-app: `default:"n/a"` supplies `n/a`, `date:"long"` is `long`, `number:"2"` is `2`, `truncate:"80"` is `80`, `default:""` supplies nothing, and `currency:"EUR"` formats in EUR. Brace text whose part before any `|` is not a path (`{{ not a path }}`, `{{ | upper }}`) is still not a hole.
1717
- **In-app.** No accept set moves: its compile already refused these holes, and keeps its own sentence.
1818
- **Who sees it.** A mail template whose hole writes a formatter that is not one word. No template shipped in this repository does.
1919
- **Supersedes, in the same release.** The `known-formatter` entry of this release says text after the `|` that is not a single word "still names no formatter and is left as written". That is no longer so: it is refused, as above.

‎.changeset/27225-mail-formatter-shape-lint.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
feat(lint)!: `email-template-hole-grammar` refuses an email template hole whose text after the `|` is not a formatter name, stating the formatter shape
66

7-
Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door (`422 INVALID_METADATA`) refuse an `email_template` whose `subject`, `bodyText` or `bodyHtml` writes a path followed by `|` and text that is not a formatter name (`{{ due | date-long }}`, `{{ x | two words }}`, `{{ x | }}`), which they accepted before. No accept set is widened, and no export or rule id is added or removed.
7+
Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door (`422 INVALID_METADATA`) refuse an `email_template` whose `subject`, `bodyText` or `bodyHtml` writes a path followed by `|` and text that is not a formatter name (`{{ due | date-long }}`, `{{ x | two words }}`, `{{ x | }}`), which they accepted before. One spelling is widened: a double-quoted `currency` argument (`currency:"EUR"`), refused before by `currency-code-literal` because the mail grammar kept its quotes, is now read as `EUR` and accepted, as in-app. The narrowing is the BREAKING arm and decides the level. No export or rule id is added or removed.
88
Metadata-protocol: narrowed
99

1010
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: a hole whose formatter part never formatted anything (the renderer printed it as written) is now refused where it is authored. A conversion cannot know which formatter was meant, and the refusal names the hole and states the shape, so there is no key for a migration to rewrite and no tombstone. -->

‎.changeset/27225-mail-formatter-shape-plugin-email.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Metadata-protocol: narrowed
1313

1414
- **Before.** `{{ due | date-long }}` rendered the text `{{ due | date-long }}` in every face. So did `{{ amount | currency :EUR }}` and `{{ note | default:it's }}`, though the in-app dialect formats both.
1515
- **Now.** A formatter part that is not a formatter name makes the render throw `TEMPLATE_INVALID: email template NAME (locale=LOCALE): the hole … writes 'date-long' where the formatter name goes …`, stating the shape and listing the known formatters; `sendTemplate` sends nothing. The authoring doors refuse the same hole first (`email-template-hole-grammar`), so this is reached only by a template stored before that rule, until its next publish. `renderTemplate` and `renderPlainTextTemplate` throw it too when handed a body directly.
16-
- **Read as in-app.** `{{ amount | currency :EUR }}` now renders `€1,234.50`, `{{ note | default:it's }}` renders `it's` for an empty value, and `default:"n/a"` supplies `n/a` without its quotes.
16+
- **Read as in-app.** `{{ amount | currency :EUR }}` now renders `€1,234.50`, `{{ note | default:it's }}` renders `it's` for an empty value, and every double-quoted argument loses one enclosing quote mark at each end: `default:"n/a"` supplies `n/a`, `date:"long"` formats in the long style, `number:"2"` with two digits, `truncate:"80"` at 80, `default:""` supplies nothing, and `currency:"EUR"`, refused before, formats in EUR.
1717
- **Still verbatim.** Brace text whose part before any `|` is not a path (`{{ not a path }}`, `{{}}`, `{{ | upper }}`) is the template's own text and renders as written.
1818

1919
**The remedy.** Republish the template with the formatter written as one of the names the refusal lists, optionally followed by `:` and its argument.

‎content/docs/automation/email-templates.mdx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,6 @@ Three properties of the renderer to author around:
163163
rendering by design; branching belongs in the caller, which passes in the
164164
already-decided values.
165165

166-
An unknown formatter falls back to the raw value rather than failing the render.
167-
168166
## Declared variables
169167

170168
`variables` documents the holes: each entry has a `name` (the path as written in

‎packages/formula/src/template-formatter-shape.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const PARTS: ReadonlyArray<readonly [part: string, accepted: boolean]> = [
2424
["default:it's", true],
2525
["default:'n/a'", true],
2626
['default:"n/a"', true],
27+
['currency:"EUR"', true],
28+
['date:"long"', true],
2729
['number: 2', true],
2830
['upper:', true],
2931
['date-long', false],
@@ -85,6 +87,8 @@ describe('the mail grammar', () => {
8587
expect(parseMailHole(' amount | currency :EUR ')).toEqual({ path: 'amount', formatter: 'currency', arg: 'EUR' });
8688
expect(parseMailHole(" note | default:it's ")).toEqual({ path: 'note', formatter: 'default', arg: "it's" });
8789
expect(parseMailHole(' note | default:"n/a" ')).toEqual({ path: 'note', formatter: 'default', arg: 'n/a' });
90+
// A double-quoted `currency` code was refused while the mail grammar kept its quotes; it reads as in-app now.
91+
expect(parseMailHole(' amount | currency:"EUR" ')).toEqual({ path: 'amount', formatter: 'currency', arg: 'EUR' });
8892
expect(parseMailHole(' amount | currency :@currency_code ')).toEqual({
8993
path: 'amount',
9094
formatter: 'currency',

0 commit comments

Comments
 (0)