Tiptap Studio

Parsing and serializing

Read a document from a string, a JSON value or a reader, write it back, and map nodes to byte spans.

Document reads and writes Tiptap JSON. Parsing builds the whole tree in one pass with serde_json. Serializing writes it back with the key order the data model describes.

Parse

use tiptap_rusty_parser::Document;
use serde_json::json;

// From a string.
let a = Document::from_json_str(r#"{"type":"doc","content":[]}"#)?;

// From a serde_json::Value you already have.
let b = Document::from_value(json!({"type": "doc", "content": []}))?;

// From any std::io::Read: a file, a socket, a byte slice.
let c = Document::from_reader(&br#"{"type":"doc","content":[]}"#[..])?;

assert_eq!(a, b);
assert_eq!(b, c);
MethodInputReturns
Document::from_json_str(s)&strResult<Document, ParseError>
Document::from_value(v)serde_json::ValueResult<Document, ParseError>
Document::from_reader(r)impl std::io::ReadResult<Document, ParseError>

The root does not have to be a doc. Any JSON object parses as a node, so you can parse a single paragraph or a fragment the same way.

Node and Mark implement serde's Deserialize and Serialize, so you can also use serde_json directly or embed them in your own types:

use tiptap_rusty_parser::Node;

let p: Node = serde_json::from_str(r#"{"type":"paragraph"}"#).unwrap();
assert_eq!(p.node_type.as_deref(), Some("paragraph"));

Serialize

use tiptap_rusty_parser::Document;

let doc = Document::from_json_str(r#"{"type":"doc","content":[{"type":"paragraph"}]}"#)?;

let compact: String = doc.to_json_str()?;
let pretty: String = doc.to_string_pretty()?;
let value: serde_json::Value = doc.to_value()?;

assert_eq!(compact, r#"{"type":"doc","content":[{"type":"paragraph"}]}"#);
assert!(pretty.starts_with("{\n  \"type\": \"doc\""));
assert_eq!(value["content"][0]["type"], "paragraph");
MethodReturns
to_json_str()Compact JSON, Result<String, ParseError>
to_string_pretty()Indented JSON (two spaces), Result<String, ParseError>
to_value()Result<serde_json::Value, ParseError>

Errors

Every parse and serialize method returns tiptap_rusty_parser::Result<T>, an alias for Result<T, ParseError>. ParseError has two variants:

VariantWhen
ParseError::Json(serde_json::Error)The input is not valid JSON or does not have the node shape
ParseError::Io(std::io::Error)The reader failed

A JSON error carries the line and column, from serde_json:

use tiptap_rusty_parser::{Document, ParseError};

let err = Document::from_json_str("{\"content\":[{\"type\":\"p\",\n  \"text\":7}]}").unwrap_err();
assert!(matches!(err, ParseError::Json(_)));
assert_eq!(
    err.to_string(),
    "json error: invalid type: integer `7`, expected a string at line 2 column 10"
);

Source maps

A SourceMap records the byte span of every node object in a JSON text. Use it to point at a node in an editor, highlight it in a debugger, or turn a cursor offset back into a node.

Paths in a source map are index paths, the same addressing as node_at. A node is the root object or an object inside a node's content array. Objects inside attrs or unknown fields are not nodes, even when they have a content key.

use tiptap_rusty_parser::{Document, SourceMap};

let doc = Document::from_json_str(
    r#"{"type":"doc","content":[{"type":"callout","x":1,"content":[{"type":"text","text":"hi"}]}]}"#,
)?;

// Serialize with pretty formatting and map the result.
let (text, map) = SourceMap::pretty(doc.root());
assert_eq!(text, doc.to_string_pretty()?);

// Path to span: the bytes from `{` to `}` of the text node.
let span = map.span(&[0, 0]).unwrap();
assert!(text[span.clone()].starts_with('{'));
assert!(text[span.clone()].contains("\"hi\""));

// Offset to path: the deepest node whose span contains the byte.
assert_eq!(map.path_at(span.start + 3), Some(vec![0, 0]));
assert_eq!(map.path_at(0), Some(vec![]));
assert_eq!(map.path_at(text.len()), None);

assert_eq!(map.len(), 3);
assert_eq!(map.child_count(&[0]), Some(1));

SourceMap::build maps text you already have, in any formatting, including hand-written JSON:

use tiptap_rusty_parser::SourceMap;

let text = r#"{"type":"doc","content":[ {"type":"paragraph"} ]}"#;
let map = SourceMap::build(text).unwrap();
assert_eq!(&text[map.span(&[0]).unwrap()], r#"{"type":"paragraph"}"#);
MethodReturnsCost
SourceMap::build(text)Result<SourceMap, SourceMapError>One pass over the text
SourceMap::pretty(node)(String, SourceMap): the to_string_pretty text and its mapSerialize plus one pass
span(path)Option<Range<usize>>, None when the path does not resolveO(depth)
path_at(offset)Option<Vec<usize>>, None when the offset is outside the root objectO(log nodes + depth)
child_count(path)Option<usize>O(depth)
len() / is_empty()Number of mapped nodesO(1)

A byte between two children, such as the comma, resolves to their parent. The scanner does not build a serde_json::Value, so it is exact for unknown node types and fields.

SourceMapError

build fails when the text is not a JSON node tree. The error has a byte offset and a static message, and displays as "<message> at byte <offset>".

MessageCause
expected a JSON objectThe text does not start with {
trailing charactersSomething follows the root object
expected a node object in `content`A content array holds something other than an object
unterminated stringA string has no closing quote
nesting too deepObjects or arrays nest more than 1024 levels
expected a key, expected `:`, expected `,` or `}`, expected `,` or `]`, expected a valueMalformed JSON
use tiptap_rusty_parser::SourceMap;

let err = SourceMap::build("[]").unwrap_err();
assert_eq!(err.offset, 0);
assert_eq!(err.message, "expected a JSON object");
assert!(SourceMap::build(r#"{"content":[1]}"#).is_err());

The scanner checks structure, not every detail of JSON. Parse the text with Document when you need it validated.

On this page