Skip to content

Working with Content

notectl supports three output formats:

The canonical format — a structured tree of blocks, text nodes, and marks:

const doc = editor.getJSON();

Returns a Document object:

{
"children": [
{
"type": "heading",
"id": "abc123",
"attrs": { "level": 1 },
"children": [
{ "type": "text", "text": "Hello ", "marks": [] },
{ "type": "text", "text": "World", "marks": [{ "type": "bold" }] }
]
},
{
"type": "paragraph",
"id": "def456",
"children": [
{ "type": "text", "text": "Some text here.", "marks": [] }
]
}
]
}

Sanitized HTML output suitable for rendering or storage:

const html = await editor.getContentHTML();
// "<h1>Hello <strong>World</strong></h1><p>Some text here.</p>"

For indented, human-readable output pass the pretty option:

const pretty = await editor.getContentHTML({ pretty: true });

The HTML is sanitized with DOMPurify. The allowed tags and attributes are schema-driven — each plugin declares which HTML elements it produces. With a full preset, the allowed set includes:

  • Tags: p, div, span, br, h1-h6, strong, b, em, i, u, s, a, sup, sub, ul, ol, li, input, blockquote, hr, pre, code, table, tbody, tr, td, figure, img
  • Attributes: style, href, target, rel, colspan, rowspan, src, alt, width, height, class

If you use a subset of plugins, only the tags relevant to those plugins are allowed.

For environments with strict Content Security Policy where inline style attributes are blocked, use the cssMode: 'classes' option. Instead of inline styles, dynamic marks and alignment are emitted as CSS class names:

const { html, css } = await editor.getContentHTML({ cssMode: 'classes' });

This returns a ContentCSSResult object with two fields:

  • html — The HTML with class="..." attributes instead of style="..."
  • css — A stylesheet containing only the CSS rules used in the document

Example output:

<!-- html -->
<p class="notectl-align-center">
<strong><span class="notectl-s0">Hello World</span></strong>
</p>
/* css */
.notectl-s0 { color: #ff0000; background-color: #fff176; }
.notectl-align-center { text-align: center; }

Semantic marks (<strong>, <em>, <u>, <s>) are unaffected — they always use HTML elements. Only dynamic style marks (text color, highlight, font size, font family) and block alignment are converted to classes.

Identical style combinations are deduplicated: if multiple text spans share the same color and font size, they share a single CSS class.

The pretty option works with class mode:

const { html, css } = await editor.getContentHTML({ cssMode: 'classes', pretty: true });

See the CSP guide for how to integrate the generated CSS into your page.

Plain text content with blocks joined by newlines:

const text = editor.getText();
// "Hello World\nSome text here."
await editor.setContentHTML('<h1>Welcome</h1><p>Start editing...</p>');

The HTML is parsed into the document model. Supported elements depend on registered plugins. With a full preset:

HTMLBlock Type
<p>, <div>paragraph
<h1> - <h6>heading (level 1-6)
<ul><li>list_item (bullet)
<ol><li>list_item (ordered)
<li> with checkboxlist_item (checklist)
<hr>horizontal_rule
<blockquote>blockquote
<pre><code>code_block
<table>, <tr>, <td>table, table_row, table_cell
<figure>, <img>image

Inline formatting maps:

HTMLMark / Inline Type
<strong>, <b>bold
<em>, <i>italic
<u>underline
<s>strikethrough
<sup>superscript
<sub>subscript
<a href="...">link
<span style="color: ...">textColor
<span style="background-color: ...">highlight
<span style="font-family: ...">font
<span style="font-size: ...">fontSize
<br>hard_break (InlineNode)
import { createDocument, createBlockNode, createTextNode, nodeType } from '@notectl/core';
const doc = createDocument([
createBlockNode(nodeType('paragraph'), [
createTextNode('Hello world'),
]),
]);
editor.setJSON(doc);

Use setText() for a fast, lossless plain-text replacement. Each \n becomes a paragraph:

editor.setText('First paragraph\nSecond paragraph');

setText is preferable to setContentHTML('<p>...</p>') for plain-text input — it avoids HTML parsing and preserves block identity (see below).

When an external owner (e.g. a form binding) reads the editor content and writes it back unchanged on every keystroke, the caret must not move. notectl guarantees this by ensuring block identity survives every (getX, setX) pair:

PairIdentity carrier
getJSON / setJSONblock IDs are part of the JSON shape
getContentHTML / setContentHTMLdata-block-id attribute on every block element
getText / setTextsetText reuses existing top-level block IDs in document order

Externally pasted HTML without data-block-id continues to receive fresh IDs, so external content imports behave as before. For setText, IDs are reused by position, not by content match — this is by design (the cursor stays on the same line index when text is rewritten in place), but means plugins that reference blocks by BlockId should not assume content stability across setText calls.

This contract enables Angular signal forms, RxJS-driven sync pipelines, and any external state owner to round-trip content on every input event without disturbing the user’s cursor. See ARCHITECTURE.md §9.1 for the full contract.

if (editor.isEmpty()) {
console.log('Editor has no content');
}

The editor is considered empty when it contains a single empty paragraph.

editor.on('stateChange', async ({ oldState, newState, transaction }) => {
// Called on every state change
const html = await editor.getContentHTML();
saveToBackend(html);
});

Use the command API for common operations:

// Toggle inline marks
editor.commands.toggleBold();
editor.commands.toggleItalic();
editor.commands.toggleUnderline();
// Execute named commands from plugins
editor.executeCommand('toggleStrikethrough');
editor.executeCommand('insertHorizontalRule');
editor.executeCommand('toggleList:ordered');
editor.executeCommand('toggleList:bullet');
editor.executeCommand('toggleBlockquote');
// Undo / Redo
editor.commands.undo();
editor.commands.redo();
// Select all
editor.commands.selectAll();
const canToggle = editor.can();
if (canToggle.toggleBold()) {
editor.commands.toggleBold();
}
if (canToggle.undo()) {
editor.commands.undo();
}

For advanced use cases, you can access the editor state directly:

const state = editor.getState();
// Inspect the document
console.log(state.doc.children.length, 'blocks');
// Check selection
console.log(state.selection);
// Access schema
console.log(state.schema.nodeTypes);
console.log(state.schema.markTypes);