siyuan-note/siyuan · error

attribute view rich text normalization did not converge

Error message

attribute view rich text normalization did not converge

What it means

normalizeValueTextRichTreeSource repeatedly re-parses and re-renders the rich text tree, expecting the Kramdown serialization to reach a fixed point. It allows 4 iterations; if the content keeps changing between renders (style normalization and Lute rendering never stabilize), it gives up with this error rather than persisting non-idempotent content. This indicates a parser/renderer oscillation rather than a single malformed token.

Source

Thrown at kernel/av/value.go:2127

	for iteration := 0; iteration < 4; iteration++ {
		if err = normalizeValueTextRichTreeStyles(normalizedTree); nil != err {
			return "", nil, err
		}
		luteEngine := newValueTextRichLute()
		blockDOM := luteEngine.Tree2BlockDOM(normalizedTree, luteEngine.RenderOptions, luteEngine.ParseOptions)
		content = valueTextRichBlockDOM2Kramdown(luteEngine, blockDOM)
		if 0 < iteration && previous == content {
			return content, normalizedTree, nil
		}
		previous = content
		candidate := &ValueTextRich{
			Spec: ValueTextRichSpec, Format: ValueTextRichFormatKramdown, Content: content,
		}
		if _, normalizedTree, err = parseValueTextRich(candidate); nil != err {
			return "", nil, err
		}
	}
	return "", nil, fmt.Errorf("attribute view rich text normalization did not converge")
}

// NormalizeRichContent 校验富文本载荷,并根据富文本源刷新纯文本投影。
func (value *ValueText) NormalizeRichContent() (err error) {
	if !value.IsRich() {
		return
	}
	tree, err := NormalizeValueTextRich(value.Rich)
	if nil != err {
		return err
	}
	value.Content = valueTextRichPlainContent(tree)
	return
}

func valueTextRichPlainContent(tree *parse.Tree) string {
	if nil == tree || nil == tree.Root {
		return ""

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Simplify the rich text content — remove unusual style attributes or nested markup until normalization succeeds
  2. Identify the oscillating construct by rendering the content twice and diffing the output, then reauthor that portion
  3. Update Lute (rebuild lute.min.js) if the oscillation is caused by a parser/serializer mismatch in the bundled engine
Defensive patterns

Strategy: retry

Validate before calling

// detect oscillation before submitting: render twice and compare
const a = lute.BlockDOM2Kramdown(lute.Kramdown2BlockDOM(content))
const b = lute.BlockDOM2Kramdown(lute.Kramdown2BlockDOM(a))
if (a !== b) throw new Error("content does not round-trip stably")

Try / catch

try {
  await api.normalizeAvValueText(payload)
} catch (e) {
  if (String(e).includes("normalization did not converge")) {
    payload = { ...payload, rich: { ...payload.rich, content: simplifyStyles(payload.rich.content) } }
    await api.normalizeAvValueText(payload)
  }
}

Prevention

When it happens

Trigger: Calling av.NormalizeValueTextRich (or saving a ValueText rich payload via NormalizeRichContent) with Kramdown whose styles or markup are rewritten differently on each parse/render cycle — e.g. style attributes that Lute re-orders or re-escapes differently every pass.

Common situations: Exotic or adversarial style attribute values (nested entities, mixed quoting) that round-trip unstably; a Lute engine version that serializes some construct differently than it parses it; plugin-authored rich text using constructs outside the supported subset.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/8579d8f51078ab8f. Report an issue: GitHub.