siyuan-note/siyuan · error

table cell rich text projection structure changed

Error message

table cell rich text projection structure changed

What it means

During projection of a table cell's rich text from an inline source (e.g. code block or math block content), the kernel re-syncs the projected inline nodes back onto the origin nodes. This error means the projected node sequence no longer matches the expected structure: a node at the same position has a different ast.Node type, so the walker cannot map projection nodes back to their origin.

Solutions

  1. Reload the document in the current SiYuan version so the cell is re-parsed and re-projected from source
  2. Inspect the table cell's .sy JSON and fix the projected inline nodes so their types match the expected projection
  3. If caused by a recent Lute/parser upgrade, roll back or update to a version where the projection format is unchanged
  4. Restore the document from sync history or a snapshot

Example fix

// before (hand-edited cell projection)
{"Type":"NodeText"}   // where a NodeCodeBlockCode projection was expected
// after
re-parse the cell so the projection matches:
expected, sources := parse.ProjectTableCellRichWithSources(fragment)
Defensive patterns

Strategy: try-catch

Validate before calling

// Regenerate the projection immediately before syncing
expected, sources := parse.ProjectTableCellRichWithSources(fragment)
if expected.ChildCount() != cell.ChildCount() { reparse() }

Type guard

func isSyncable(expected, actual *ast.Node) bool {
  for w, a := expected.FirstChild, actual.FirstChild; w != nil && a != nil; w, a = w.Next, a.Next {
    if w.Type != a.Type { return false }
  }
  return true
}

Try / catch

if err := syncTableProjection(cell); err != nil {
  if strings.Contains(err.Error(), "projection structure") {
    reparseCellAndRetry(cell)
  }
}

Prevention

When it happens

Trigger: Calling the table-cell rich text sync path (parse.ProjectTableCellRichWithSources + recursive sync) when the cell's inline projection was edited in a way that inserted, removed, or retyped nodes relative to what the projection generated — e.g. a plugin or direct .sy edit changed node types inside a table cell that projects code/math content.

Common situations: Editing exported/projected table content with an external tool or older client; a Lute parser change altering how inline markdown inside cells is tokenized; corrupted or hand-edited .sy JSON.

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@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/6b0c2e2ded755e98. Report an issue: GitHub.

Appendix: source

Thrown at kernel/treenode/table_cell_rich.go:155

// SyncTableCellRichInlineChanges 将内核对资源、引用和行内内容的改写同步到源,再刷新派生内容。
func SyncTableCellRichInlineChanges(root *ast.Node) (err error) {
	updates := map[*ast.Node]string{}
	ast.Walk(root, func(cell *ast.Node, entering bool) ast.WalkStatus {
		if !entering || nil == cell.TableCellRich {
			return ast.WalkContinue
		}
		var fragment *parse.Tree
		if fragment, err = av.ParseTableCellRich(cell.TableCellRich); nil != err {
			return ast.WalkStop
		}
		expected, sources := parse.ProjectTableCellRichWithSources(fragment)
		changed := false
		var sync func(*ast.Node, *ast.Node) error
		sync = func(want, actual *ast.Node) error {
			for ; want != nil && actual != nil; want, actual = want.Next, actual.Next {
				if want.Type != actual.Type {
					return fmt.Errorf("table cell rich text projection structure changed")
				}
				if !bytes.Equal(tableCellInlineSignature(want), tableCellInlineSignature(actual)) {
					origin := sources[want]
					if nil == origin {
						return fmt.Errorf("table cell rich text projection marker changed")
					}
					switch origin.Type {
					case ast.NodeCodeBlockCode:
						origin.Tokens = []byte(html.UnescapeString(actual.TextMarkTextContent) + "\n")
					case ast.NodeMathBlockContent:
						origin.Tokens = []byte(html.UnescapeString(actual.TextMarkInlineMathContent))
					default:
						parent, previous, next, first, last := origin.Parent, origin.Previous, origin.Next, origin.FirstChild, origin.LastChild
						*origin = *parse.CloneTableCellInline(actual)
						origin.Parent, origin.Previous, origin.Next, origin.FirstChild, origin.LastChild = parent, previous, next, first, last
					}
					changed = true
				}

View on GitHub (pinned to 9f775e8a12)