Working with Content
Output Formats
Section titled “Output Formats”notectl supports three output formats:
JSON (Document Model)
Section titled “JSON (Document Model)”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.
HTML with CSS Classes (CSP-Compliant)
Section titled “HTML with CSS Classes (CSP-Compliant)”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 withclass="..."attributes instead ofstyle="..."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
Section titled “Plain Text”Plain text content with blocks joined by newlines:
const text = editor.getText();// "Hello World\nSome text here."Setting Content
Section titled “Setting Content”From HTML
Section titled “From HTML”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:
| HTML | Block Type |
|---|---|
<p>, <div> | paragraph |
<h1> - <h6> | heading (level 1-6) |
<ul><li> | list_item (bullet) |
<ol><li> | list_item (ordered) |
<li> with checkbox | list_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:
| HTML | Mark / 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) |
From JSON
Section titled “From JSON”import { createDocument, createBlockNode, createTextNode, nodeType } from '@notectl/core';
const doc = createDocument([ createBlockNode(nodeType('paragraph'), [ createTextNode('Hello world'), ]),]);
editor.setJSON(doc);From Plain Text
Section titled “From Plain Text”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).
Round-Trip Identity
Section titled “Round-Trip Identity”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:
| Pair | Identity carrier |
|---|---|
getJSON / setJSON | block IDs are part of the JSON shape |
getContentHTML / setContentHTML | data-block-id attribute on every block element |
getText / setText | setText 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.
Checking Empty State
Section titled “Checking Empty State”if (editor.isEmpty()) { console.log('Editor has no content');}The editor is considered empty when it contains a single empty paragraph.
Listening for Changes
Section titled “Listening for Changes”editor.on('stateChange', async ({ oldState, newState, transaction }) => { // Called on every state change const html = await editor.getContentHTML(); saveToBackend(html);});Programmatic Editing
Section titled “Programmatic Editing”Use the command API for common operations:
// Toggle inline markseditor.commands.toggleBold();editor.commands.toggleItalic();editor.commands.toggleUnderline();
// Execute named commands from pluginseditor.executeCommand('toggleStrikethrough');editor.executeCommand('insertHorizontalRule');editor.executeCommand('toggleList:ordered');editor.executeCommand('toggleList:bullet');editor.executeCommand('toggleBlockquote');
// Undo / Redoeditor.commands.undo();editor.commands.redo();
// Select alleditor.commands.selectAll();Check Command Availability
Section titled “Check Command Availability”const canToggle = editor.can();
if (canToggle.toggleBold()) { editor.commands.toggleBold();}
if (canToggle.undo()) { editor.commands.undo();}Advanced: Direct State Access
Section titled “Advanced: Direct State Access”For advanced use cases, you can access the editor state directly:
const state = editor.getState();
// Inspect the documentconsole.log(state.doc.children.length, 'blocks');
// Check selectionconsole.log(state.selection);
// Access schemaconsole.log(state.schema.nodeTypes);console.log(state.schema.markTypes);