feat(richtext): parameterized styles, alignment/image/base-style channels, HTML round-trip #70

Closed
opened 2026-07-01 21:22:42 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-01 21:22:42 +00:00 (Migrated from github.com)

Foundation for the compose-editor rich formatting epic (strikethrough, font family/size/color, highlight, paragraph alignment, inline images, remembered font defaults). Today RichStyle is a parameterless enum (BOLD/ITALIC/UNDERLINE), so none of those features fit the model. All other epic tickets stack on this branch: feat-richtext-foundation.

Scope (org.libremail.richtext, pure JVM):

  • RichStyle becomes a sealed interface: Bold, Italic, Underline, Strikethrough, FontFamily(css: String), FontSize(pt: Int), FontColor(argb: Int), Highlight(argb: Int).
  • RichTextContent gains alignments: List<RichAlignment> (RichAlign START/CENTER/END, paragraph ranges), images: List<RichImage> (start, end, contentId, name — the range covers a visible [image: name] token in the plain text), and baseStyle: RichBaseStyle? (fontCss, fontSizePt).
  • Serializer: parameterized styles as merged <span style="…">; alignment as text-align on <p>/<li> (consecutive-line <p> merging must split at alignment boundaries); images as <img src="cid:…" alt="name"> replacing the token; baseStyle as a single outer <div style="…"> (no stray newlines).
  • Parser: faithful inverse of everything above; also <s>/<del>/<strike>, strong/em, pt and px sizes, hex colors; unknown CSS properties ignored without dropping the text run.
  • hasFormatting() covers every new channel (any span, link, block marker, alignment, image, or baseStyle).
  • Editor mapping (ui/compose/RichTextEditor.kt): style identity carried through the AnnotatedString via string annotations (like the existing URL_TAG) so FontColor can never be confused with link paint; visual SpanStyle derived per style; toAnnotatedString gains a font-resolver hook (css → Compose FontFamily, default = system fonts); baseStyle applied to the field's text style; alignment ranges → ParagraphStyle(textAlign). RichTextBodyField's signature stays source-compatible (new params defaulted — it is also used by SignatureEditScreen).
  • RichTextEditing.toggleStyle/isStyled generalized: toggling the exact style off; applying a different value of the same kind replaces it. (Alignment/image ops are NOT in scope — later tickets own them.)
  • New reusable ui/compose/format/ColorSwatchRow.kt.

Acceptance

  • Zero visible UI change; all existing tests stay green.
  • Exhaustive round-trip tests: for every new style/channel, fromHtml(toHtml(x)) == x and hasFormatting() is true — this guards ComposeViewModel.normalizedHtml(), which silently drops any HTML the parser cannot read back as formatted.
Foundation for the compose-editor rich formatting epic (strikethrough, font family/size/color, highlight, paragraph alignment, inline images, remembered font defaults). Today `RichStyle` is a parameterless enum (`BOLD/ITALIC/UNDERLINE`), so none of those features fit the model. All other epic tickets stack on this branch: `feat-richtext-foundation`. **Scope** (`org.libremail.richtext`, pure JVM): - `RichStyle` becomes a sealed interface: `Bold`, `Italic`, `Underline`, `Strikethrough`, `FontFamily(css: String)`, `FontSize(pt: Int)`, `FontColor(argb: Int)`, `Highlight(argb: Int)`. - `RichTextContent` gains `alignments: List<RichAlignment>` (`RichAlign START/CENTER/END`, paragraph ranges), `images: List<RichImage>` (`start, end, contentId, name` — the range covers a visible `[image: name]` token in the plain text), and `baseStyle: RichBaseStyle?` (`fontCss`, `fontSizePt`). - Serializer: parameterized styles as merged `<span style="…">`; alignment as `text-align` on `<p>/<li>` (consecutive-line `<p>` merging must split at alignment boundaries); images as `<img src="cid:…" alt="name">` replacing the token; baseStyle as a single outer `<div style="…">` (no stray newlines). - Parser: faithful inverse of everything above; also `<s>/<del>/<strike>`, `strong/em`, `pt` and `px` sizes, hex colors; unknown CSS properties ignored without dropping the text run. - `hasFormatting()` covers every new channel (any span, link, block marker, alignment, image, or baseStyle). - Editor mapping (`ui/compose/RichTextEditor.kt`): style identity carried through the AnnotatedString via string annotations (like the existing `URL_TAG`) so `FontColor` can never be confused with link paint; visual `SpanStyle` derived per style; `toAnnotatedString` gains a font-resolver hook (css → Compose `FontFamily`, default = system fonts); baseStyle applied to the field's text style; alignment ranges → `ParagraphStyle(textAlign)`. `RichTextBodyField`'s signature stays source-compatible (new params defaulted — it is also used by `SignatureEditScreen`). - `RichTextEditing.toggleStyle/isStyled` generalized: toggling the exact style off; applying a different value of the same kind replaces it. (Alignment/image *ops* are NOT in scope — later tickets own them.) - New reusable `ui/compose/format/ColorSwatchRow.kt`. **Acceptance** - Zero visible UI change; all existing tests stay green. - Exhaustive round-trip tests: for every new style/channel, `fromHtml(toHtml(x)) == x` **and** `hasFormatting()` is true — this guards `ComposeViewModel.normalizedHtml()`, which silently drops any HTML the parser cannot read back as formatted.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#70