Automatic-style registry (write) and resolver (read): emphasis, list numbering, table grids.
Module textStyleRegistry | Source packages/front/office/odf/src/text/style-registry.js | Deps xml | Worker-safe yes
Two halves of one seam in content.xml:
- write —
createRegistry()hands out deterministicawa-t-*/awa-l-*/awa-c-*/awa-tb-*automatic-style names for a set of semantic flags, then materialises them as astyleAutomaticmodel plus anoffice:font-face-declselement. - read —
createResolver()answers what atext:style-name(or a table/celltable:style-name) reference means, and records which content automatic styles it consumed so the orchestrator can drop them from the surfaced passthrough model.
Resolve
const styleRegistry = runtime.resolve('textStyleRegistry');
// → { createRegistry, createResolver }
API
| Method | Signature | Returns |
|---|---|---|
createRegistry |
(reserved?: {styles?: Set<string>, fontFaces?: Set<string>}) => registry |
write-side registry |
registry.textStyle |
({bold?, italic?, strike?, monospace?}) => string |
automatic-style name |
registry.listStyle |
({ordered: boolean, numFormat?: string}) => string |
list-style name |
registry.cellStyle |
({bordered: true}) => string |
table-cell automatic-style name (awa-c-b) |
registry.tableStyle |
({align: 'margins'}) => string |
table automatic-style name (awa-tb-m) |
registry.toAutomaticStyles |
() => object | null |
styleAutomatic model, null when nothing was requested |
registry.toFontFaceDecls |
() => object | null |
office:font-face-decls element, null without monospace |
createResolver |
(contentAutoStyles, fontFaces, namedStyles) => resolver |
read-side resolver |
resolver.textFlags |
(styleName: string) => object | null |
{bold?, italic?, strike?, monospace?, source} |
resolver.listNumbering |
(styleName: string) => object | null |
{ordered, numFormat?, source} |
resolver.cellBorders |
(styleName: string) => object | null |
{bordered: true, source} |
resolver.tableAlign |
(styleName: string) => object | null |
{align: 'margins', source} |
resolver.consumed |
Set<string> |
names resolved with source: 'auto' |
Naming
textStyle builds 'awa-t-' plus the flag letters in the fixed order
b, i, s, m — {italic: true, bold: true} and {bold: true, italic: true} both give awa-t-bi, and the same combination always returns the same
name. listStyle gives 'awa-l-n' + numFormat (default '1') when ordered,
'awa-l-b' for a bullet list. cellStyle({bordered: true}) gives awa-c-b
and tableStyle({align: 'margins'}) gives awa-tb-m — the only spec each
accepts today; both are deduplicated (one style per registry, however many
grid tables request it). A name already present in reserved.styles
gets the first free -2, -3, … suffix; likewise awa-mono against
reserved.fontFaces. Callers of textStyle guarantee at least one flag.
Attribute mapping
| flag | style:text-properties attribute |
|---|---|
bold |
fo:font-weight = bold |
italic |
fo:font-style = italic |
strike |
style:text-line-through-style = solid (read: any value ≠ none) |
monospace |
style:font-name = a face name resolving to a monospace-declared face |
The grid seam maps two more families:
| semantic | family | properties bag (write) | read rule |
|---|---|---|---|
bordered |
table-cell |
tableCell: fo:border = 0.5pt solid #000000, fo:padding = 0.097cm |
keys ⊆ {fo:border, fo:padding}; fo:border present, neither empty nor none |
align: 'margins' |
table |
table: table:align = margins |
the bag is exactly {'table:align': 'margins'} |
toAutomaticStyles() emits these after every awa-t-* entry, cell first
then table, each only when requested; it still returns null when nothing at
all was requested.
Monospace emission always declares one face, referenced by name:
xml.el('style:font-face', {
'style:name': 'awa-mono',
'svg:font-family': 'monospace',
'style:font-family-generic': 'modern',
'style:font-pitch': 'fixed'
}, [])
On read, a style:font-name value counts as monospace only when it names
a face in the supplied fontFaces carrying style:font-pitch="fixed" or
style:font-family-generic="modern" — either marker is sufficient. A face
that is missing, or carries neither marker, leaves the key unrecognised.
fo:font-family is not part of the mapping in either direction.
List styles are emitted as raw text:list-style elements with levels 1..10,
so nested lists work without a per-level style. Every level — bullet and
number alike — carries exactly one style:list-level-properties child with a
label-alignment indent:
<text:list-style style:name="…">
<!-- ordered level L -->
<text:list-level-style-number text:level="L" style:num-format="…" style:num-suffix=".">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab"
text:list-tab-stop-position="ML" fo:text-indent="-0.635cm" fo:margin-left="ML"/>
</style:list-level-properties>
</text:list-level-style-number>
<!-- bullet level L: same child -->
<text:list-level-style-bullet text:level="L" text:bullet-char="•">…</text:list-level-style-bullet>
</text:list-style>
ML is a literal per level (declared, never computed at runtime):
| level | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| ML | 1.27cm |
1.905cm |
2.54cm |
3.175cm |
3.81cm |
4.445cm |
5.08cm |
5.715cm |
6.35cm |
6.985cm |
The bullet stays • (U+2022) and no level carries style:text-properties or
a forced font: the paragraph font draws the bullet.
The honesty rule
textFlags resolves only a fully-mapped style: family === 'text', no
parentStyleName, properties holding the text bag and nothing else, and
every attribute key in that bag one of the four above with a matching
value. Anything else returns null, so the caller keeps the opaque
styleName passthrough instead of guessing — a converter's loss report stays
truthful. An empty bag resolves nothing and is therefore null too.
cellBorders and tableAlign follow the same rule over their own family and
bag (see the grid table above): no parentStyleName, the one bag and nothing
else, no extra element child.
listNumbering looks at the first element child of the text:list-style:
text:list-level-style-number → {ordered: true, numFormat} (default '1'),
text:list-level-style-bullet → {ordered: false}, anything else or no
element child at all → null.
Resolution order and sources
textFlags / listNumbering / cellBorders / tableAlign try
contentAutoStyles first, then namedStyles. The three arguments are:
| argument | content |
|---|---|
contentAutoStyles |
styleAutomatic model of content.xml's <office:automatic-styles> (or null) |
fontFaces |
raw style:font-face elements — the union of content.xml's and styles.xml's declarations |
namedStyles |
raw elements of the styles.xml <office:styles> bucket (or null) |
styles.xml's own <office:automatic-styles> bucket is deliberately
excluded: a content.xml body element may only reference a content automatic
style or a common (named) style.
Named-style resolution applies the same fully-mapped rule with two
deviations: the raw element is typed on the fly, and the metadata-only
attributes style:display-name and style:class are tolerated (ignored).
Any other extra attribute, any extra element child, or a
style:parent-style-name rejects the style.
Parent chains are not resolved. A styles.xml style deriving from a
fully-mapped root stays unresolved; inheritance merging is a tracked
follow-up, not a silent approximation.
Consumption asymmetry
resolver.consumed collects 'auto'-source resolutions only. A content
automatic style that was proven is consumed, and the orchestrator drops it
from the surfaced model — its meaning now lives in the typed body. A named
style is kept and gained: the reference stays, the flags are extra
information, and styles.xml material is never consumed or stripped.
Examples
const styleRegistry = runtime.resolve('textStyleRegistry');
// Write side
const reg = styleRegistry.createRegistry({ styles: new Set(['P1']) });
reg.textStyle({ bold: true, italic: true }); // 'awa-t-bi'
reg.listStyle({ ordered: true, numFormat: 'a' }); // 'awa-l-na'
reg.cellStyle({ bordered: true }); // 'awa-c-b'
reg.tableStyle({ align: 'margins' }); // 'awa-tb-m'
reg.toAutomaticStyles(); // → { styles: [...], _extras: { children: [...] } }
reg.toFontFaceDecls(); // → null (no monospace requested)
// Read side
const res = styleRegistry.createResolver(reg.toAutomaticStyles(), [], null);
res.textFlags('awa-t-bi'); // { bold: true, italic: true, source: 'auto' }
res.listNumbering('awa-l-na'); // { ordered: true, numFormat: 'a', source: 'auto' }
res.cellBorders('awa-c-b'); // { bordered: true, source: 'auto' }
res.tableAlign('awa-tb-m'); // { align: 'margins', source: 'auto' }
[...res.consumed]; // ['awa-t-bi', 'awa-l-na', 'awa-c-b', 'awa-tb-m']
Worker Usage
// Pure object/XML-node construction — no I/O, no clock, no captures.
// Registry state is per `createRegistry()` call, so a worker can build
// its own registry and post back the resulting model.
const worker = fw.createWorker(/* … */);
Notes
- Registry state is per-
createRegistrycall; the factory holds no mutable module-scope state, so two documents never share generated names. - The raw-style typing used for named styles mirrors
styleAutomatic.parseStyle; the mirror is pinned by a drift test rather than imported, because this factory declares onlyxml.
See also
- style/automaticStyles — the model
toAutomaticStyles()produces andcreateResolver()consumes - odt/odt — wires both halves into
content.xml - text/content — threads the seam object down to the body nodes