feat(diagram-engine): flex knobs (grow/align/pad) + inline rich-text labels

The expressiveness gap between engine output and hand-written XML came down
to two missing capabilities, both generic:

- Block layout inside a box: nested containers already existed, but there was
  no way to split space by weight, pin a child to an edge, or tighten padding.
  Added grow (flex-grow over the parent's leftover flow-axis space, TeX's
  glue), align (start/center/end/stretch on the cross axis) and pad
  (per-group interior padding). All three round-trip via dai_grow/dai_align/
  dai_pad markers.

- Inline rich text: labels already render HTML (html=1 on every style, esc()
  entities decode back), but the measure pass counted markup as text. The
  visibleText() strip makes autoBoxSize measure what draw.io draws: <br> is a
  line, other inline tags are invisible.

Graphviz (HTML-like table labels), D2 (grid containers + markdown-in-shape)
and TeX (box+glue) converged on exactly this design: nested boxes for block
structure, proportional glue, a small inline set for text — never full HTML.

Prompts teach the composition with a comparison-card recipe; verified by
rebuilding the CoT poster end to end in the real editor.
This commit is contained in:
dayuan.jiang
2026-08-09 20:55:12 +09:00
parent e78322ca52
commit db9db1ff4e
9 changed files with 432 additions and 23 deletions

View File

@@ -29,6 +29,7 @@ import {
isPinned,
MARKER,
type NodeKind,
readAlign,
readCell,
readDir,
readIntMarker,
@@ -360,6 +361,19 @@ function zoneOf(style: string): string | undefined {
return v ? decodeURIComponent(v) : undefined
}
/** The flex fields stamped on a cell, as sparse properties ready to spread. */
function flexOf(style: string): {
grow?: number
align?: "start" | "end" | "stretch"
} {
const grow = readIntMarker(style, MARKER.grow)
const align = readAlign(style)
return {
...(grow !== null && grow > 0 ? { grow } : {}),
...(align ? { align } : {}),
}
}
function boxShape(style: string): BoxShape | undefined {
const shape = styleValue(style, "shape")
if (shape === "parallelogram") return "data"
@@ -1070,6 +1084,7 @@ export function parseDiagram(xml: string, pageIndex = 0): ParseResult {
shape: boxShape(c.style),
role: roleOf(c.style),
group: zoneOf(c.style),
...flexOf(c.style),
// A lifeline's style is chrome the renderer rebuilds, so keeping it verbatim
// would re-emit a lifeline that no longer matches the new message count.
style: lifeline ? undefined : c.style,
@@ -1215,12 +1230,15 @@ export function parseDiagram(xml: string, pageIndex = 0): ParseResult {
}
return node
}
const markedPad = readIntMarker(c.style, MARKER.pad)
const node: GroupNode = {
kind: "group",
...common,
dir: dir === "col" ? "col" : "row",
role: roleOf(c.style),
group: zoneOf(c.style),
...flexOf(c.style),
...(markedPad !== null ? { pad: markedPad } : {}),
}
return node
}