Tiptap Studio

Text

Extract plain text from a document and count characters and words.

Four methods read the text of a subtree. Call them on a Document for the whole document, or on any node for its subtree.

use tiptap_rusty_parser::Document;

let doc = Document::from_json_str(r#"{"type":"doc","content":[
    {"type":"paragraph","content":[
        {"type":"text","text":"Hello "},
        {"type":"text","text":"brave","marks":[{"type":"bold"}]},
        {"type":"text","text":" world"}
    ]},
    {"type":"paragraph","content":[{"type":"text","text":"second line"}]}
]}"#)?;

assert_eq!(doc.text_content(), "Hello brave worldsecond line");
assert_eq!(doc.text_content_with_separator("\n\n"), "Hello brave world\n\nsecond line");
assert_eq!(doc.char_count(), 28);
assert_eq!(doc.word_count(), 5);
MethodReturns
text_content()Every text field in the subtree, in document order, with nothing between them
text_content_with_separator(sep)The same, with sep between adjacent blocks
char_count()The number of Unicode scalar values (Rust chars) in all text
word_count()The number of words, using Unicode word segmentation

text_content

text_content matches ProseMirror's node.textContent: it concatenates every text field with no separators, so two paragraphs run together. Marks do not matter. Inline nodes without text, such as hardBreak, add nothing.

text_content_with_separator

text_content_with_separator puts sep between two adjacent siblings when both are blocks. Text inside a block stays contiguous. A block here is any non-text node that has a content field, even an empty one.

use tiptap_rusty_parser::{doc, Node};

let d = doc([
    Node::element("heading").with_text("Title"),
    Node::element("paragraph")
        .with_children([Node::text("a"), Node::element("hardBreak"), Node::text("b")]),
]);
assert_eq!(d.text_content_with_separator("\n"), "Title\nab");

The test uses the JSON, not a schema. A node with no content key is not a block, so it gets no separator on either side. Tiptap writes an empty paragraph as {"type":"paragraph"}, with no content, so the text around one runs together:

use tiptap_rusty_parser::Document;

let doc = Document::from_json_str(r#"{"type":"doc","content":[
    {"type":"paragraph","content":[{"type":"text","text":"Hello"}]},
    {"type":"paragraph"},
    {"type":"paragraph","content":[{"type":"text","text":"world"}]}
]}"#)?;
assert_eq!(doc.text_content_with_separator("\n"), "Helloworld");
assert_eq!(doc.word_count(), 1);

If the separator matters, give empty paragraphs an empty content list before you extract the text.

char_count

char_count counts Unicode scalar values, the same unit that flat positions and range offsets use. It is not a byte count and not a UTF-16 count, so it can differ from JavaScript's string.length:

use tiptap_rusty_parser::Node;

let t = Node::text("café 🦀");
assert_eq!(t.char_count(), 6);
assert_eq!("café 🦀".len(), 10); // bytes

word_count

word_count joins blocks with a space, so words do not merge across block boundaries, then counts words with Unicode word segmentation (UAX #29, from the unicode-segmentation crate). Punctuation is not a word. Each CJK ideograph counts as one word:

use tiptap_rusty_parser::Node;

assert_eq!(Node::text("Hello, brave new world!").word_count(), 4);
assert_eq!(Node::text("你好世界").word_count(), 4);

Because it uses the same block rule as text_content_with_separator, a contentless node between two blocks can merge their words, as shown above.

On this page