Document content
What the text of a chapter actually looks like when your plugin reads it, how to convert it, and how to write it back without damaging someone's manuscript.
On this page
The thing plugin authors get wrong over and over: what is in a chapter is not HTML and it is not plain text. It is ProseMirror JSON, the editor’s own way of describing a document, handed to you as a string.
const raw = await tintero.project.getFileContent(fileId);
typeof raw; // 'string'
JSON.parse(raw); // { type: 'doc', content: [ … ] }
getFileContent() and getDocContent() both give you that string, or null if there is
nothing in the file yet.
Converting
You almost never want the raw tree. convert.format gives you six converters, three in each direction:
const raw = await tintero.project.getFileContent(fileId);
const text = await tintero.convert.toText(raw);
const html = await tintero.convert.toHtml(raw);
const markdown = await tintero.convert.toMarkdown(raw);
const document = await tintero.convert.fromMarkdown('# Chapter one\n\nIt was a dark night.');
const fromHtml = await tintero.convert.fromHtml('<p>It was a dark night.</p>');
const fromText = await tintero.convert.fromText('It was a dark night.');
The to* converters take either the raw string or an object you already parsed, so you can hand
them whatever getFileContent() gave you. The from* converters give you back an object.
Writing back
Here is where it catches people:
const document = await tintero.convert.fromMarkdown('# New chapter\n\nHello.');
await tintero.project.updateFileContent(fileId, JSON.stringify(document));
// ^^^^^^^^^^^^^^^^^^^^^^^^
// a string, not the object
fromMarkdown() hands you an object; updateFileContent() wants a string. Forgetting that
JSON.stringify is the most common mistake anyone makes with this API.
Permissions: rewriting is not creating
The read side splits the same way. project.read.files gets you the list of files and folders; project.read.fileContent gets you what is inside them. A plugin that only needs an outline never has to ask for the words.
A worked example: append to a chapter
Read it, convert it, change it, convert it back, write it.
async function appendParagraph(fileId, sentence) {
const raw = await tintero.project.getFileContent(fileId);
const markdown = raw ? await tintero.convert.toMarkdown(raw) : '';
const updated = await tintero.convert.fromMarkdown(markdown + '\n\n' + sentence);
await tintero.project.updateFileContent(fileId, JSON.stringify(updated));
}
Going out to Markdown and back loses anything Markdown cannot express, which in a Tintero document is a great deal: scenes, bookmarks, character highlights, screenplay formatting and inline audio have no Markdown to be turned into. Fine when you are moving text around, wrong when you need to keep every bit of formatting. For that, walk the ProseMirror tree yourself:
async function countParagraphs(fileId) {
const raw = await tintero.project.getFileContent(fileId);
if (!raw) return 0;
const document = JSON.parse(raw);
return (document.content || []).filter((node) => node.type === 'paragraph').length;
}
The node names you can meet while walking are the
document schema, and
how ProseMirror documents work covers the shape in full: why
formatting is a marks array on a text node rather than a wrapper, and why an unknown node
type should be passed through rather than dropped.
Editor content versus file content
These are two different things and they can disagree:
tintero.projectreads what is saved on disk.tintero.editorreads what is on screen right now, unsaved changes included.
If someone is halfway through a sentence, editor.getWordCount() counts it and
project.getFileContent() does not. Use the editor calls and the editor.activeDocumentChanged
event for anything live, and the project calls for going through the whole manuscript.
The editor’s write methods take HTML, not ProseMirror JSON, because they work on the live
editor rather than on a stored file. That goes for editor.insertAt(), editor.replaceRange()
and editor.replaceSelection().
await tintero.editor.replaceSelection('<strong>replaced</strong>');
Images
project.getImages() tells you what images exist. project.getImageData() gives you a base64
data URL you can drop straight into an src. It works that way because your frame is not allowed
to load files off disk, so the app has to hand you the bytes themselves.
const images = await tintero.project.getImages();
const dataUrl = await tintero.project.getImageData(images[0].fileName);
document.getElementById('preview').src = dataUrl;