Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/validation/code-oss-word-breaking-787.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Code OSS retained word breaking (#787)

Date: 2026-09-20

## Source baseline

- WebScene parent: `dd03421e180546c39aa4c272396b36af628d38c5`
- unchanged Code OSS: `645f29cc3176500b4b5762ba887cf2a7f0ffdf2c`
- tracking: WebScene #787, CSS epic #235

## Reduced defect

The retained inline formatter and fallback text painter wrapped only at whitespace. A token wider than its used content box stayed on one line even when author CSS requested `word-break: break-all`, `word-break: break-word`, `overflow-wrap: anywhere`, `overflow-wrap: break-word`, or the legacy `word-wrap` alias.

The unchanged Code OSS inventory reaches this behavior in Markdown preview, dialogs, notifications, hover and parameter-hint content, quick input, suggestions, REPL/debug surfaces, editor placeholders, comments, and chat/session UI.

## Implementation

- Generated CSS property metadata now gives `word-break` and `overflow-wrap` stable native/managed identities. `word-wrap`, `wordWrap`, and compact CSSOM spellings resolve to `overflow-wrap`.
- Both properties retain authored cascade values in the existing cold textual-style allocation and inherit through empty descendant sentinels. No hot `dom_node` field, timer, animation-frame participant, platform widget, document scan, or application CSS rewrite was added.
- Inline assignment/removal, stylesheet application, custom-property resolution, global keywords, `all: unset`, CSSOM computed values, and managed mutation invalidation include the new properties.
- Both retained inline fragments and fallback text lines use one UTF-8-boundary segmenter. It measures each code point once for the initial fit estimate, verifies the final shaped segment, never splits a multibyte sequence, and preserves source byte ranges used by selection/highlight paint.
- `break-all` may use any code-point boundary. `break-word`/`anywhere` use emergency breaks for over-wide tokens. `normal`, `initial`, `revert`, and the currently bounded Latin `keep-all` path preserve whitespace wrapping.

## Authored gates

- `tests/WebPlatformSubset/contracts/css-word-breaking.html`: browser-referenced geometry, inheritance, CSSOM aliases, mutation/removal, and supplementary-plane text.
- `test_word_break_and_overflow_wrap_layout`: native retained-layout regression for the same behavior.
- `test_word_break_layout_performance_gate`: 4,096 nodes and ten forced style/layout transitions, with at most one layout pass per transition and a five-second wall-time ceiling.
- The WPT-subset profile records the contract as candidate until direct Chromium/native evidence exists.

## Evidence status

No build, test, WPT run, benchmark, visual comparison, memory run, packaged-product run, or CI result was executed for this implementation. Evidence is zero. The gates above are source-authored and must be run during consolidated qualification.

## Remaining qualification

1. Run the focused native correctness and performance filters.
2. Run the candidate contract in the pinned Chromium and native WPT lanes.
3. Compare Markdown preview, dialog, notification, hover, parameter-hint, quick-input, suggestion, REPL, and editor-placeholder pixels and geometry with unchanged Chromium Code OSS.
4. Record long-token, mixed-script, resize, mutation, lifecycle, CPU, allocation, scene-publication, and package evidence.
5. Keep complete Unicode line-breaking, grapheme-cluster breaking, hyphenation, and international `keep-all` behavior outside the claim until separately implemented and qualified.
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@
{"id": "text_align", "name": "text-align", "aliases": ["textalign"]},
{"id": "text_transform", "name": "text-transform", "aliases": ["texttransform"]},
{"id": "white_space", "name": "white-space", "aliases": ["whitespace"]},
{"id": "word_break", "name": "word-break", "aliases": ["wordbreak"]},
{"id": "overflow_wrap", "name": "overflow-wrap", "aliases": ["word-wrap", "overflowwrap", "wordwrap"]},
{"id": "list_style", "name": "list-style"},
{"id": "list_style_position", "name": "list-style-position"},
{"id": "list_style_type", "name": "list-style-type"},
Expand Down Expand Up @@ -167,7 +169,7 @@
"keyword": [
"all", "display", "position", "floating", "flex_direction", "flex_wrap", "align_items", "align_self",
"justify_content", "box_sizing", "vertical_align", "grid_auto_flow", "border_collapse", "table_layout", "border_style", "background_repeat",
"visibility", "pointer_events", "text_anchor", "font_smoothing", "text_align", "text_transform", "white_space", "list_style_position",
"visibility", "pointer_events", "text_anchor", "font_smoothing", "text_align", "text_transform", "white_space", "word_break", "overflow_wrap", "list_style_position",
"list_style_type", "scrollbar_width", "container", "container_type", "content_visibility"
],
"componentList": [
Expand Down Expand Up @@ -205,13 +207,13 @@
"nativeInheritedProperties": [
"all", "color", "cursor", "direction", "font", "font-family", "font-size", "font-style",
"font-weight", "letter-spacing", "line-height", "list-style", "list-style-position", "list-style-type",
"text-align", "text-transform", "visibility", "white-space", "word-spacing", "-webkit-font-smoothing"
"text-align", "text-transform", "visibility", "white-space", "word-break", "overflow-wrap", "word-spacing", "-webkit-font-smoothing"
],
"nativeMasklessPropertyIds": [
"content", "aspect_ratio", "vertical_align", "grid_auto_columns", "grid_auto_flow", "table_layout", "border_inline", "border_block",
"outline", "outline_width", "outline_color", "animation", "animation_name", "animation_duration", "animation_delay",
"animation_timing_function", "animation_iteration_count", "background_repeat", "background_position", "background_size",
"font", "text_transform", "list_style", "list_style_position", "list_style_type"
"font", "text_transform", "word_break", "overflow_wrap", "list_style", "list_style_position", "list_style_type"
],
"managedKnownProperties": [
"align-content", "align-items", "align-self", "all", "background", "background-color",
Expand All @@ -230,7 +232,7 @@
"order", "overflow", "overflow-x", "overflow-y", "padding", "padding-bottom", "padding-left",
"padding-right", "padding-top", "pointer-events", "position", "right", "row-gap", "stroke",
"stroke-linecap", "stroke-linejoin", "stroke-width", "text-align", "text-indent", "text-transform",
"top", "transform", "visibility", "white-space", "width", "word-spacing", "z-index",
"top", "transform", "visibility", "white-space", "word-break", "overflow-wrap", "width", "word-spacing", "z-index",
"outline", "outline-color", "outline-offset", "outline-style", "outline-width",
"grid-template-areas", "aspect-ratio"
],
Expand All @@ -255,7 +257,7 @@
"moz-transform", "-webkit-transform", "webkit-transform", "-webkit-font-smoothing", "webkit-font-smoothing", "orphans", "resize",
"scrollbar-color", "scrollbar-width", "table-layout", "text-anchor", "text-decoration", "text-overflow", "vertical-align", "transition", "transition-delay",
"transition-duration", "transition-property", "transition-timing-function", "transform-origin",
"widows", "zoom"
"widows", "word-wrap", "zoom"
],
"properties": [
{"name": "border-block-color", "mask": ["inline_border"], "expansion": ["border-top-color", "border-bottom-color"], "applyExpansion": true},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ inline constexpr std::array native_typed_property_identity_catalog{
native_typed_property_identity{"texttransform", css_property_id::text_transform},
native_typed_property_identity{"white-space", css_property_id::white_space},
native_typed_property_identity{"whitespace", css_property_id::white_space},
native_typed_property_identity{"word-break", css_property_id::word_break},
native_typed_property_identity{"wordbreak", css_property_id::word_break},
native_typed_property_identity{"overflow-wrap", css_property_id::overflow_wrap},
native_typed_property_identity{"word-wrap", css_property_id::overflow_wrap},
native_typed_property_identity{"overflowwrap", css_property_id::overflow_wrap},
native_typed_property_identity{"wordwrap", css_property_id::overflow_wrap},
native_typed_property_identity{"list-style", css_property_id::list_style},
native_typed_property_identity{"list-style-position", css_property_id::list_style_position},
native_typed_property_identity{"list-style-type", css_property_id::list_style_type},
Expand Down Expand Up @@ -414,6 +420,8 @@ inline constexpr std::array native_modeled_property_mask_catalog{
false,
false,
false,
false,
false,
true,
true,
true,
Expand All @@ -432,7 +440,7 @@ inline constexpr bool generated_property_has_modeled_mask(
&& native_modeled_property_mask_catalog[index];
}

inline constexpr std::array<std::string_view, 20> native_inherited_property_catalog{
inline constexpr std::array<std::string_view, 22> native_inherited_property_catalog{
"-webkit-font-smoothing",
"all",
"color",
Expand All @@ -448,10 +456,12 @@ inline constexpr std::array<std::string_view, 20> native_inherited_property_cata
"list-style",
"list-style-position",
"list-style-type",
"overflow-wrap",
"text-align",
"text-transform",
"visibility",
"white-space",
"word-break",
"word-spacing",
};

Expand Down Expand Up @@ -603,6 +613,8 @@ inline constexpr std::array native_property_grammar_catalog{
native_property_grammar::keyword,
native_property_grammar::keyword,
native_property_grammar::keyword,
native_property_grammar::keyword,
native_property_grammar::keyword,
native_property_grammar::complex,
native_property_grammar::keyword,
native_property_grammar::keyword,
Expand Down Expand Up @@ -818,6 +830,12 @@ inline css_property_id generated_property_id_lowercase(std::string_view name) no
if (name == "texttransform") return css_property_id::text_transform;
if (name == "white-space") return css_property_id::white_space;
if (name == "whitespace") return css_property_id::white_space;
if (name == "word-break") return css_property_id::word_break;
if (name == "wordbreak") return css_property_id::word_break;
if (name == "overflow-wrap") return css_property_id::overflow_wrap;
if (name == "word-wrap") return css_property_id::overflow_wrap;
if (name == "overflowwrap") return css_property_id::overflow_wrap;
if (name == "wordwrap") return css_property_id::overflow_wrap;
if (name == "list-style") return css_property_id::list_style;
if (name == "list-style-position") return css_property_id::list_style_position;
if (name == "list-style-type") return css_property_id::list_style_type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ line_height,
text_align,
text_transform,
white_space,
word_break,
overflow_wrap,
list_style,
list_style_position,
list_style_type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ inline constexpr std::array cssom_supported_property_catalog{
cssom_supported_property_metadata{"outline-style", "outlineStyle"},
cssom_supported_property_metadata{"outline-width", "outlineWidth"},
cssom_supported_property_metadata{"overflow", "overflow"},
cssom_supported_property_metadata{"overflow-wrap", "overflowWrap"},
cssom_supported_property_metadata{"overflow-x", "overflowX"},
cssom_supported_property_metadata{"overflow-y", "overflowY"},
cssom_supported_property_metadata{"padding", "padding"},
Expand Down Expand Up @@ -241,9 +242,11 @@ inline constexpr std::array cssom_supported_property_catalog{
cssom_supported_property_metadata{"white-space", "whiteSpace"},
cssom_supported_property_metadata{"widows", "widows"},
cssom_supported_property_metadata{"width", "width"},
cssom_supported_property_metadata{"word-break", "wordBreak"},
cssom_supported_property_metadata{"word-spacing", "wordSpacing"},
cssom_supported_property_metadata{"word-wrap", "wordWrap"},
cssom_supported_property_metadata{"z-index", "zIndex"},
cssom_supported_property_metadata{"zoom", "zoom"},
};

inline constexpr auto cssom_style_template_property_accessor_count = 435U;
inline constexpr auto cssom_style_template_property_accessor_count = 441U;
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ void apply_declaration(native_document& document,dom_node& node,
normalized=authored;normalized->name="transform";
} else if(authored.name=="grid-gap" || authored.name=="grid-row-gap" || authored.name=="grid-column-gap") {
normalized=authored;normalized->name=canonical_property_name(authored.name);
} else if (authored.name == "word-wrap") {
normalized = authored;
normalized->name = "overflow-wrap";
}
const auto& declaration=normalized?*normalized:authored;
if(declaration.name.starts_with("--")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ inline void reset_cascaded_style(dom_node& node,
if ((node.style.inline_property_mask & inline_white_space) == 0U) {
textual->white_space.clear();
}
textual->word_break.clear();
textual->overflow_wrap.clear();
}
if ((node.style.inline_property_mask & inline_font_weight) == 0U) node.style.font_weight = 0;
if ((node.style.inline_property_mask & inline_line_height) == 0U) node.style.line_height = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ inline void apply_all_unset(
reset.mutable_textual().white_space =
previous.textual().white_space;
}
if (has_inline({"word-break"})) {
reset.mutable_textual().word_break =
previous.textual().word_break;
}
if (has_inline({"overflow-wrap", "word-wrap"})) {
reset.mutable_textual().overflow_wrap =
previous.textual().overflow_wrap;
}

// These modeled properties do not yet have dedicated inline-mask
// bits, so preserve their applied values by authored declaration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ inline constexpr auto specified_property_samples = std::to_array<specified_prope
{"font","700 16px/1.5 system-ui"},{"font-size","16px"},{"font-family","system-ui, sans-serif"},
{"-webkit-font-smoothing","antialiased"},{"font-weight","700"},{"letter-spacing",".2px"},
{"word-spacing","1px"},{"line-height","1.5"},{"text-align","center"},{"text-transform","uppercase"},
{"white-space","nowrap"},{"list-style","inside square"},{"list-style-position","outside"},{"list-style-type","decimal"},
{"white-space","nowrap"},{"word-break","break-all"},{"overflow-wrap","anywhere"},
{"list-style","inside square"},{"list-style-position","outside"},{"list-style-type","decimal"},
{"scrollbar-width","thin"},{"scrollbar-color","#777 transparent"},
{"container","sidebar / inline-size"},{"container-type","inline-size"},
{"container-name","sidebar"},{"content-visibility","auto"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,14 @@ bool apply_text_value(dom_node& node,const std::string& name,const std::string&
node.style.mutable_textual().white_space =
value == "inherit" || value == "unset"
? std::string{} : value;
} else if (name == "word-break") {
node.style.mutable_textual().word_break =
value == "inherit" || value == "unset"
? std::string{} : value;
} else if (name == "overflow-wrap") {
node.style.mutable_textual().overflow_wrap =
value == "inherit" || value == "unset"
? std::string{} : value;
} else if (name == "list-style-position") {
node.style.mutable_textual().list_style_position = value;
} else if (name == "list-style-type") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,37 @@ bool resolved_white_space_wraps(const dom_node& node)
return true;
}

std::string_view resolved_word_break(const dom_node& node)
{
for (auto* current = &node; current != nullptr; current = current->parent) {
const auto& value = current->style.textual().word_break;
if (value.empty() || value == "inherit" || value == "unset") continue;
return value == "initial" || value == "revert"
? std::string_view{"normal"} : std::string_view{value};
}
return "normal";
}

std::string_view resolved_overflow_wrap(const dom_node& node)
{
for (auto* current = &node; current != nullptr; current = current->parent) {
const auto& value = current->style.textual().overflow_wrap;
if (value.empty() || value == "inherit" || value == "unset") continue;
return value == "initial" || value == "revert"
? std::string_view{"normal"} : std::string_view{value};
}
return "normal";
}

bool breaks_inside_words(const dom_node& node, bool token_exceeds_line)
{
const auto word_break = resolved_word_break(node);
if (word_break == "break-all" || word_break == "break-word") return true;
const auto overflow_wrap = resolved_overflow_wrap(node);
return token_exceeds_line
&& (overflow_wrap == "anywhere" || overflow_wrap == "break-word");
}

bool has_visible_text(const std::string& value)
{
return std::any_of(value.begin(), value.end(), [](unsigned char character) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,10 @@ struct node_style final {
std::string vertical_align;
std::string text_transform;
std::string white_space;
// Inherited line-breaking policy. Empty values inherit without adding
// state to descendants; the root resolves both properties to normal.
std::string word_break;
std::string overflow_wrap;
std::string contain_value;
std::string container_type{"normal"};
std::string container_name{"none"};
Expand Down Expand Up @@ -2733,6 +2737,10 @@ class native_document final {
float available_width,
const dom_node& node,
bool allow_wrap) const;
std::vector<std::pair<size_t, size_t>> break_text_segments(
std::string_view value,
float available_width,
const dom_node& node) const;
float resolve_length(
const dom_node& context,
css_length value,
Expand Down
Loading
Loading