// Copyright (c) 2026 Probo Inc . // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package prosemirror import ( "encoding/json" "fmt" "unicode/utf8" ) type ( // NodeType identifies the type of a ProseMirror/Tiptap document node. NodeType string // MarkType identifies the type of an inline mark. MarkType string // Node is a ProseMirror/Tiptap document node. Node struct { Type NodeType `json:"type"` Content []Node `json:"content,omitempty"` Marks []Mark `json:"marks,omitempty"` Text *string `json:"text,omitempty"` Attrs json.RawMessage `json:"attrs,omitempty"` } // Mark represents inline formatting applied to a text node. Mark struct { Type MarkType `json:"type"` Attrs json.RawMessage `json:"attrs,omitempty"` } // HeadingAttrs contains attributes for heading nodes. HeadingAttrs struct { Level int `json:"level"` } // CodeBlockAttrs contains attributes for code block nodes. CodeBlockAttrs struct { Language *string `json:"language"` } // OrderedListAttrs contains attributes for ordered list nodes. OrderedListAttrs struct { Start int `json:"start"` Type *string `json:"type"` } // ImageAttrs contains attributes for image nodes. ImageAttrs struct { Src string `json:"src"` Alt *string `json:"alt"` Title *string `json:"title"` } // TableCellAttrs contains attributes for table cell and table header nodes. TableCellAttrs struct { Colspan int `json:"colspan"` Rowspan int `json:"rowspan"` Colwidth []int `json:"colwidth"` } // LinkAttrs contains attributes for link marks. LinkAttrs struct { Href string `json:"href"` Target *string `json:"target"` Rel *string `json:"rel"` Class *string `json:"class"` Title *string `json:"title"` } ) // Node type constants. String values match the Tiptap JSON the frontend produces. // Mark type constants. Go names follow ProseMirror conventions (Strong, Em); // string values match Tiptap JSON (bold, italic). const ( NodeDoc NodeType = "doc" NodeParagraph NodeType = "paragraph" NodeBlockquote NodeType = "blockquote" NodeHeading NodeType = "heading" NodeCodeBlock NodeType = "codeBlock" NodeHorizontalRule NodeType = "horizontalRule" NodeHardBreak NodeType = "hardBreak" NodeText NodeType = "text" NodeImage NodeType = "image" NodeBulletList NodeType = "bulletList" NodeOrderedList NodeType = "orderedList" NodeListItem NodeType = "listItem" NodeTable NodeType = "table" NodeTableRow NodeType = "tableRow" NodeTableCell NodeType = "tableCell" NodeTableHeader NodeType = "tableHeader" MarkStrong MarkType = "bold" MarkEm MarkType = "italic" MarkUnderline MarkType = "underline" MarkStrike MarkType = "strike" MarkCode MarkType = "code" MarkLink MarkType = "link" ) // Parse unmarshals a ProseMirror/Tiptap JSON document string into a Node tree. // Nested content and marks are unmarshaled into Go structs recursively. func Parse(s string) (Node, error) { var n Node if err := json.Unmarshal([]byte(s), &n); err != nil { return Node{}, fmt.Errorf("cannot parse prosemirror node: %w", err) } return n, nil } // HeadingAttrs parses and returns the heading attributes from a heading node. func (n Node) HeadingAttrs() (HeadingAttrs, error) { var a HeadingAttrs if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse heading attrs: %w", err) } return a, nil } // CodeBlockAttrs parses and returns the code block attributes. func (n Node) CodeBlockAttrs() (CodeBlockAttrs, error) { if len(n.Attrs) == 0 { return CodeBlockAttrs{}, nil } var a CodeBlockAttrs if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse code block attrs: %w", err) } return a, nil } // OrderedListAttrs parses and returns the ordered list attributes. func (n Node) OrderedListAttrs() (OrderedListAttrs, error) { var a OrderedListAttrs if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse ordered list attrs: %w", err) } return a, nil } // ImageAttrs parses and returns the image attributes. func (n Node) ImageAttrs() (ImageAttrs, error) { var a ImageAttrs if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse image attrs: %w", err) } return a, nil } // TableCellAttrs parses and returns the table cell/header attributes. func (n Node) TableCellAttrs() (TableCellAttrs, error) { var a TableCellAttrs if err := json.Unmarshal(n.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse table cell attrs: %w", err) } return a, nil } // TextLength returns the total number of characters (Unicode code points) // across all text nodes in the tree. Structural markup is excluded. func (n Node) TextLength() int { length := 0 if n.Text != nil { length += utf8.RuneCountInString(*n.Text) } for _, child := range n.Content { length += child.TextLength() } return length } // LinkAttrs parses and returns the link attributes from a link mark. func (m Mark) LinkAttrs() (LinkAttrs, error) { var a LinkAttrs if err := json.Unmarshal(m.Attrs, &a); err != nil { return a, fmt.Errorf("cannot parse link attrs: %w", err) } return a, nil }