Skip to content

Implement text-transform: capitalize; make uppercase/lowercase language-aware - #947

Merged
nicoburns merged 13 commits into
mainfrom
devin/1790390713-text-transform-capitalize
Oct 7, 2026
Merged

nicoburns merged 13 commits into
mainfrom
devin/1790390713-text-transform-capitalize

Conversation

@nicoburns

@nicoburns nicoburns commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds text-transform: capitalize. uppercase and lowercase are now language-aware: non-ASCII text (and Turkic-language text) is mapped with ICU4X instead of str::to_uppercase/to_lowercase, and ASCII text takes a byte-mapping fast path that gives the same output. The logic is in a new layout/text_transform.rs, called from build_inline_layout_recursive.

Uses linebender/parley#856 (TreeBuilder::text and TreeBuilder::has_pending_whitespace). parley is pinned to linebender main at 62790943, the #856 merge. That rev changed Run::font() to return &FontData, so blitz-paint/src/text.rs is updated to match.

  • Words can span text nodes: hel<b>lo</b> → "Hello", and in a<span capitalize>b</span>c the "b" stays lowercase. A capitalize text node looks back at the last 32 bytes of builder.text() (the white-space-processed text pushed so far), starting from the last forced word break. If builder.has_pending_whitespace(), the node starts a new word, so no context is used. Nothing is copied or tracked for text nodes that aren't capitalized.
    • Only look-behind is needed: under UAX#29, whether there is a word break before a letter never depends on the text after it.
    • The earlier text in the context has already been case-transformed. That's fine for segmentation, since case mapping keeps letters as letters.
  • TextTransformer stores context_start, the builder text length at the last forced word break, plus reusable scratch/output buffers. word_break(builder) is called after a position-inside list marker, at in-flow inline boxes (replaced elements, inline-block, etc.) and at <br>. Out-of-flow and float boxes add no text and don't break words (p<span abs></span>ass → "Pass").
  • transform returns &str. Unchanged text is returned borrowed, with no allocation or copy (OutputSink only starts writing to the reused buffer at the first difference). ASCII text takes a byte-mapping fast path for uppercase/lowercase, except for Turkic languages (ascii_fast_path_matches_icu checks it against ICU4X).
  • capitalize runs icu_segmenter word segmentation over context + text. In each word that starts inside the current node, the first typographic letter unit (general category Letter or Number) is titlecased with icu_casemap's TitlecaseMapper (TrailingCase::Unchanged). This skips leading punctuation and leaves circled letters such as ⓐ alone.
  • The language comes from the computed -x-lang (the lang attribute). This enables Dutch ij→IJ and Turkish/Azeri dotted and dotless i. casing_language drops the language when an explicit script subtag contradicts it (tr-Cyrl → root casing), as css-text-3 § script tagging requires. ICU4X itself only looks at the language subtag.
  • New blitz-dom feature icu-text-transform (on by default) gates icu_casemap and writeable. It's forwarded and on by default in blitz, dioxus-native and dioxus-native-dom, and enabled explicitly in apps/browser and the WPT runner. Without it, uppercase/lowercase/titlecase_segment fall back to std char::to_uppercase/to_lowercase (final sigma via str::to_lowercase). Word segmentation and the letter check are the same either way, but casing is language-insensitive and titlecase is approximated by uppercase (dž→DŽ, ß→SS). CI's Clippy job also lints blitz-dom with the feature off.
  • New dependencies: icu_casemap, icu_casemap_data, icu_locale_core, icu_properties and icu_segmenter, all at 2.3, plus writeable 0.6. Only icu_casemap and icu_casemap_data are new to the tree. Release blitz browser binary (production profile): +71 KiB (+0.26%) with the feature on, about 42 KiB of case-mapping data and 27 KiB of code.

WPT css/css-text/text-transform: 57/113 → 92/113, no regressions. Remaining capitalize failures:

  • capitalize-033: a pixel difference that still happens with text-transform removed and "Pass" typed literally. The likely cause is the inline box splitting the shaping run; not investigated further.
  • capitalize-036: needs innerText.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/4641a2bf5eca4d4fb140d16886180423
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/4641a2bf5eca4d4fb140d16886180423?variant=devin-insiders
Requested by: @nicoburns

WPT results

Subtests: 53 newly passing, 0 newly failing (net +53).

Full diff (53 changed tests)
+ FAIL => PASS  [1/1]  +1  css/CSS2/css1/c545-txttrans-000.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-001.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-001.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-002.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-003.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-005.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-006.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-007.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-008.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-009.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-010.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-011.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-applies-to-014.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-bicameral-005.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-bicameral-006.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-cap-001.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-cap-002.xht
+ FAIL => PASS  [1/1]  +1  css/CSS2/text/text-transform-cap-003.xht
+ FAIL => PASS  [1/1]  +1  css/css-display/run-in/text-transform-applies-to-004.xht
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-001.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-003.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-005.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-007.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-009.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-010.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-014.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-016.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-018.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-020.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-022.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-024.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-028.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-030.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-031.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-032.xht
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-034.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-capitalize-035.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-letter-spacing-002.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-001.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-001a.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-002.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-002a.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-003.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-004.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-005.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-tailoring-document-lang-dynamic.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-upperlower-039.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-upperlower-040.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-upperlower-041.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-upperlower-042.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-upperlower-043.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-upperlower-044.html
+ FAIL => PASS  [1/1]  +1  css/css-text/text-transform/text-transform-upperlower-106.html

Generated by the WPT workflow.

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1790390713-text-transform-capitalize branch from 4359631 to 2146684 Compare October 7, 2026 18:00
@staging-devin-ai-integration staging-devin-ai-integration Bot changed the title Implement text-transform: capitalize; use ICU4X case mapping for all case transforms Implement text-transform: capitalize; make uppercase/lowercase language-aware Oct 7, 2026
@nicoburns
nicoburns merged commit 1e444d2 into main Oct 7, 2026
14 of 16 checks passed
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.

1 participant