@unito/integration-sdk
Version:
Integration SDK
1,225 lines (1,224 loc) • 52.4 kB
JavaScript
import { describe, it } from 'node:test';
import assertStrict from 'node:assert/strict';
import { encodeSentinels, decodeSentinels } from '../src/richTextCodec.js';
describe('encodeSentinels', () => {
it('passes through when the predicate accepts every node', () => {
const tree = {
type: 'root',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'hi' }] }],
};
assertStrict.deepEqual(encodeSentinels(tree, () => true), tree);
});
it('replaces unsupported nodes with paired markers, serializing data as space-separated key="value"', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'a' },
{
type: 'jira_panel',
data: { panelType: 'info', color: '#deebff' },
children: [{ type: 'text', value: 'x' }],
},
{ type: 'text', value: 'b' },
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'jira_panel'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'a' },
{ type: 'text', value: '【 jira_panel panelType="info" color="#deebff" 】' },
{ type: 'text', value: 'x' },
{ type: 'text', value: '【 end jira_panel 】' },
{ type: 'text', value: 'b' },
],
},
],
});
});
it('emits a self-closing marker for childless unsupported nodes', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'see ' },
{ type: 'asana_macro' },
{ type: 'text', value: ', then ' },
{ type: 'asana_macro', data: { kind: 'attachment', id: 42 } },
{ type: 'text', value: '.' },
],
},
{ type: 'thematicBreak' },
{ type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'asana_macro' && node.type !== 'thematicBreak'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'see ' },
{ type: 'text', value: '【 asana_macro / 】' },
{ type: 'text', value: ', then ' },
{ type: 'text', value: '【 asana_macro kind="attachment" id=42 / 】' },
{ type: 'text', value: '.' },
],
},
{ type: 'paragraph', children: [{ type: 'text', value: '【 thematicBreak / 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
],
});
});
it('recursively encodes children inside an encoded parent (depth-first)', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'jira_panel',
children: [{ type: 'asana_macro', children: [{ type: 'text', value: 'inside' }] }],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => !['jira_panel', 'asana_macro'].includes(node.type)), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 jira_panel 】' },
{ type: 'text', value: '【 asana_macro 】' },
{ type: 'text', value: 'inside' },
{ type: 'text', value: '【 end asana_macro 】' },
{ type: 'text', value: '【 end jira_panel 】' },
],
},
],
});
});
it('passes the ancestor chain to the predicate, innermost-first with root last', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'strong', children: [{ type: 'text', value: 'bold' }] }],
},
],
};
const seen = [];
encodeSentinels(tree, (node, ancestors) => {
seen.push([node.type, ...ancestors.map(a => a.type)]);
return true;
});
assertStrict.deepEqual(seen.find(s => s[0] === 'strong'), ['strong', 'paragraph', 'root']);
});
it('places markers as inline text siblings when the parent holds inline content', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'a ' },
{ type: 'strong', children: [{ type: 'text', value: 'bold' }] },
{ type: 'text', value: ' b' },
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'strong'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'a ' },
{ type: 'text', value: '【 strong 】' },
{ type: 'text', value: 'bold' },
{ type: 'text', value: '【 end strong 】' },
{ type: 'text', value: ' b' },
],
},
],
});
});
it('wraps each marker in its own paragraph when encoding under a block-content parent', () => {
const tree = {
type: 'root',
children: [
{ type: 'paragraph', children: [{ type: 'text', value: 'before' }] },
{
type: 'jira_panel',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'inside' }] }],
},
{ type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'jira_panel'), {
type: 'root',
children: [
{ type: 'paragraph', children: [{ type: 'text', value: 'before' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'inside' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
],
});
});
it('emits non-string primitive data values as JSON literals', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'checkbox', data: { checked: true, indent: 2 } }],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'checkbox'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'text', value: '【 checkbox checked=true indent=2 / 】' }],
},
],
});
});
it('drops individual data values that cannot survive the wire format, keeping the rest', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'jira_panel',
data: {
safe: 'ok',
hasQuote: 'has "quote"',
hasBracket: 'has 】 inside',
nonPrimitive: { x: 1 },
},
children: [{ type: 'text', value: 'x' }],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'jira_panel'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 jira_panel safe="ok" 】' },
{ type: 'text', value: 'x' },
{ type: 'text', value: '【 end jira_panel 】' },
],
},
],
});
});
});
describe('encodeSentinels — table substitution', () => {
it('renders an unsupported table as a single markdown paragraph with break-delimited rows', () => {
const tree = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Name' }] },
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Status' }] },
],
},
{
type: 'tableRow',
children: [
{ type: 'tableCell', children: [{ type: 'text', value: 'Alice' }] },
{ type: 'tableCell', children: [{ type: 'text', value: 'Active' }] },
],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'table'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '| Name | Status |' },
{ type: 'break' },
{ type: 'text', value: '| --- | --- |' },
{ type: 'break' },
{ type: 'text', value: '| Alice | Active |' },
],
},
],
});
});
it('emits the header separator after row 1 unconditionally', () => {
const tree = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{ type: 'tableCell', children: [{ type: 'text', value: 'a' }] },
{ type: 'tableCell', children: [{ type: 'text', value: 'b' }] },
],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'table'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '| a | b |' },
{ type: 'break' },
{ type: 'text', value: '| --- | --- |' },
],
},
],
});
});
it('preserves supported wrappers as canonical nodes inside cells', () => {
const tree = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{
type: 'tableCell',
children: [
{ type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
],
},
{ type: 'tableCell', children: [{ type: 'text', value: 'Status' }] },
],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'table'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '| ' },
{ type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
{ type: 'text', value: ' | Status |' },
{ type: 'break' },
{ type: 'text', value: '| --- | --- |' },
],
},
],
});
});
it('escapes backslashes, pipes, and newlines in cell text', () => {
const tree = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{ type: 'tableCell', children: [{ type: 'text', value: 'a | b' }] },
{ type: 'tableCell', children: [{ type: 'text', value: 'line1\nline2' }] },
{ type: 'tableCell', children: [{ type: 'text', value: 'back\\slash' }] },
],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'table'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '| a \\| b | line1 line2 | back\\\\slash |' },
{ type: 'break' },
{ type: 'text', value: '| --- | --- | --- |' },
],
},
],
});
});
it('drops empty tables', () => {
const tree = {
type: 'root',
children: [
{ type: 'table', children: [] },
{ type: 'paragraph', children: [{ type: 'text', value: 'after' }] },
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'table'), {
type: 'root',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'after' }] }],
});
});
it('preserves unsupported nodes in cells via sentinel markers', () => {
const tree = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{
type: 'tableCell',
children: [
{ type: 'image', data: { src: 'thumb.png' } },
{ type: 'text', value: ' ' },
{ type: 'mention', data: { id: '123' }, children: [{ type: 'text', value: '@alice' }] },
],
},
],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, node => node.type !== 'table' && node.type !== 'image' && node.type !== 'mention'), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'text',
value: '| 【 image src="thumb.png" / 】 【 mention id="123" 】@alice【 end mention 】 |',
},
{ type: 'break' },
{ type: 'text', value: '| --- |' },
],
},
],
});
});
it('keeps the table when supported', () => {
const tree = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [{ type: 'tableCell', children: [{ type: 'text', value: 'x' }] }],
},
],
},
],
};
assertStrict.deepEqual(encodeSentinels(tree, () => true), tree);
});
});
describe('decodeSentinels', () => {
it('passes through when no markers are present', () => {
const tree = {
type: 'root',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'hi' }] }],
};
assertStrict.deepEqual(decodeSentinels(tree), tree);
});
it('reconstructs paired markers, with and without attrs', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 jira_panel 】' },
{ type: 'text', value: 'plain' },
{ type: 'text', value: '【 end jira_panel 】' },
{ type: 'text', value: ' ' },
{ type: 'text', value: '【 jira_panel panelType="info" color="#deebff" 】' },
{ type: 'text', value: 'with attrs' },
{ type: 'text', value: '【 end jira_panel 】' },
],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'jira_panel', children: [{ type: 'text', value: 'plain' }] },
{ type: 'text', value: ' ' },
{
type: 'jira_panel',
data: { panelType: 'info', color: '#deebff' },
children: [{ type: 'text', value: 'with attrs' }],
},
],
},
],
});
});
it('reconstructs nested markers innermost-first', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 jira_panel 】' },
{ type: 'text', value: '【 asana_macro 】' },
{ type: 'text', value: 'inside' },
{ type: 'text', value: '【 end asana_macro 】' },
{ type: 'text', value: '【 end jira_panel 】' },
],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'jira_panel',
children: [{ type: 'asana_macro', children: [{ type: 'text', value: 'inside' }] }],
},
],
},
],
});
});
it('merges open markers fragmented across adjacent text nodes', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 jira_' },
{ type: 'text', value: 'panel 】' },
{ type: 'text', value: 'inside' },
{ type: 'text', value: '【 end jira_panel 】' },
],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'jira_panel', children: [{ type: 'text', value: 'inside' }] }],
},
],
});
});
it('reconstructs across paragraph siblings when open and close land in different blocks', () => {
const tree = {
type: 'root',
children: [
{ type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'inside' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'jira_panel',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'inside' }] }],
},
],
});
});
it('leaves an orphan open marker as plain text', () => {
const tree = {
type: 'root',
children: [{ type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】unmatched' }] }],
};
assertStrict.deepEqual(decodeSentinels(tree), tree);
});
it('leaves an orphan close marker as plain text', () => {
const tree = {
type: 'root',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'unmatched 【 end jira_panel 】' }] }],
};
assertStrict.deepEqual(decodeSentinels(tree), tree);
});
it('splits a text node when markers wrap inline content mid-text', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'text', value: 'before 【 strong 】middle【 end strong 】 after' }],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'before ' },
{ type: 'strong', children: [{ type: 'text', value: 'middle' }] },
{ type: 'text', value: ' after' },
],
},
],
});
});
it('handles multiple paired and cross-sibling sentinels in one tree', () => {
const tree = {
type: 'root',
children: [
// Multiple paired sentinels packed into a single text node.
{
type: 'paragraph',
children: [{ type: 'text', value: '【 strong 】a【 end strong 】 and 【 emphasis 】b【 end emphasis 】' }],
},
// Cross-sibling outer (jira_panel) wrapping a paragraph that itself contains a paired inner (strong).
{ type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
{
type: 'paragraph',
children: [{ type: 'text', value: '【 strong 】Hello【 end strong 】 world' }],
},
{ type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
// Back-to-back cross-sibling sentinels.
{ type: 'paragraph', children: [{ type: 'text', value: '【 asana_macro 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'macro' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end asana_macro 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 notion_block 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'block' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end notion_block 】' }] },
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'strong', children: [{ type: 'text', value: 'a' }] },
{ type: 'text', value: ' and ' },
{ type: 'emphasis', children: [{ type: 'text', value: 'b' }] },
],
},
{
type: 'jira_panel',
children: [
{
type: 'paragraph',
children: [
{ type: 'strong', children: [{ type: 'text', value: 'Hello' }] },
{ type: 'text', value: ' world' },
],
},
],
},
{
type: 'asana_macro',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'macro' }] }],
},
{
type: 'notion_block',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'block' }] }],
},
],
});
});
it('decodes legacy production forms — positional args and pre-RFC tag spellings', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 highlight #ff0000 】' },
{ type: 'text', value: 'hot1' },
{ type: 'text', value: '【 end highlight 】' },
{ type: 'text', value: ' ' },
{ type: 'text', value: '【 highlighted #00ff00 】' },
{ type: 'text', value: 'hot2' },
{ type: 'text', value: '【 end highlighted 】' },
{ type: 'text', value: ' ' },
{ type: 'text', value: '【 header 2 】' },
{ type: 'text', value: 'Title' },
{ type: 'text', value: '【 end header 】' },
{ type: 'text', value: ' ' },
{ type: 'text', value: '【 code python 】' },
{ type: 'text', value: 'print("hi")' },
{ type: 'text', value: '【 end code 】' },
],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'highlight',
data: { color: '#ff0000' },
children: [{ type: 'text', value: 'hot1' }],
},
{ type: 'text', value: ' ' },
{
type: 'highlight',
data: { color: '#00ff00' },
children: [{ type: 'text', value: 'hot2' }],
},
{ type: 'text', value: ' ' },
{ type: 'heading2', children: [{ type: 'text', value: 'Title' }] },
{ type: 'text', value: ' ' },
{
type: 'code',
data: { lang: 'python' },
children: [{ type: 'text', value: 'print("hi")' }],
},
],
},
],
});
});
it('decodes self-closing markers with varied spacing around the slash', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 a/ 】' },
{ type: 'text', value: ' ' },
{ type: 'text', value: '【 b /】' },
{ type: 'text', value: ' ' },
{ type: 'text', value: '【 c/】' },
{ type: 'text', value: ' ' },
{ type: 'text', value: '【 d kind="x"/ 】' },
],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'a' },
{ type: 'text', value: ' ' },
{ type: 'b' },
{ type: 'text', value: ' ' },
{ type: 'c' },
{ type: 'text', value: ' ' },
{ type: 'd', data: { kind: 'x' } },
],
},
],
});
});
it('decodes nested same-tag sentinels by tracking marker depth', () => {
const tree = {
type: 'root',
children: [
// Inline: outer foo and inner foo packed into one paragraph's text.
{
type: 'paragraph',
children: [{ type: 'text', value: '【 foo 】outer 【 foo 】inner【 end foo 】 more【 end foo 】' }],
},
// Cross-paragraph: outer bar's open and close in different paragraph
// siblings, with an inner bar pair inside the middle paragraph.
{ type: 'paragraph', children: [{ type: 'text', value: '【 bar 】' }] },
{
type: 'paragraph',
children: [{ type: 'text', value: '【 bar 】content【 end bar 】' }],
},
{ type: 'paragraph', children: [{ type: 'text', value: '【 end bar 】' }] },
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'foo',
children: [
{ type: 'text', value: 'outer ' },
{ type: 'foo', children: [{ type: 'text', value: 'inner' }] },
{ type: 'text', value: ' more' },
],
},
],
},
{
type: 'bar',
children: [
{
type: 'paragraph',
children: [{ type: 'bar', children: [{ type: 'text', value: 'content' }] }],
},
],
},
],
});
});
});
describe('decodeSentinels — markdown table reconstruction', () => {
it('reconstructs a markdown table from a single text node with newline-delimited rows', () => {
const tree = {
type: 'root',
children: [{ type: 'text', value: '| Name | Status |\n| --- | --- |\n| Alice | Active |' }],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Name' }] },
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Status' }] },
],
},
{
type: 'tableRow',
children: [
{ type: 'tableCell', children: [{ type: 'text', value: 'Alice' }] },
{ type: 'tableCell', children: [{ type: 'text', value: 'Active' }] },
],
},
],
},
],
});
});
it('reconstructs a markdown table from break-delimited inline content', () => {
const tree = {
type: 'root',
children: [
{ type: 'text', value: '| Name | Status |' },
{ type: 'break' },
{ type: 'text', value: '| --- | --- |' },
{ type: 'break' },
{ type: 'text', value: '| Alice | Active |' },
],
};
const result = decodeSentinels(tree);
assertStrict.equal(result.children[0]?.type, 'table');
assertStrict.equal(result.children[0]?.children?.length, 2);
});
it('unwraps a paragraph wrapping the table content', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'text', value: '| Name | Status |\n| --- | --- |\n| Alice | Active |' }],
},
],
};
const result = decodeSentinels(tree);
assertStrict.equal(result.children[0]?.type, 'table');
});
it('recovers cell content with supported wrappers preserved as canonical nodes', () => {
const tree = {
type: 'root',
children: [
{ type: 'text', value: '| ' },
{ type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
{ type: 'text', value: ' | Status |\n| --- | --- |' },
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{
type: 'tableCell',
data: { header: true },
children: [
{ type: 'link', data: { url: 'https://x' }, children: [{ type: 'text', value: 'click' }] },
],
},
{
type: 'tableCell',
data: { header: true },
children: [{ type: 'text', value: 'Status' }],
},
],
},
],
},
],
});
});
it('unescapes pipes and backslashes in cell text', () => {
const tree = {
type: 'root',
children: [{ type: 'text', value: '| a \\| b | back\\\\slash |\n| --- | --- |' }],
};
const root = decodeSentinels(tree);
const cells = (root.children[0]?.children?.[0]?.children ?? []);
assertStrict.equal(cells[0]?.children?.[0]?.value, 'a | b');
assertStrict.equal(cells[1]?.children?.[0]?.value, 'back\\slash');
});
it('rejects when the separator is not exactly ---', () => {
for (const value of ['| h |\n| -- |', '| h |\n| ---- |', '| h |\n| :---: |']) {
const tree = { type: 'root', children: [{ type: 'text', value }] };
assertStrict.deepEqual(decodeSentinels(tree), tree);
}
});
it('rejects column-count mismatches', () => {
for (const value of ['| a | b |\n| --- |', '| a |\n| --- |\n| x | y |']) {
const tree = { type: 'root', children: [{ type: 'text', value }] };
assertStrict.deepEqual(decodeSentinels(tree), tree);
}
});
it('leaves regular text alone', () => {
const tree = {
type: 'root',
children: [
{ type: 'text', value: 'Hello | world' },
{ type: 'text', value: '| Note: see above |' },
],
};
assertStrict.deepEqual(decodeSentinels(tree), tree);
});
it('recurses into block containers', () => {
const tree = {
type: 'root',
children: [
{
type: 'blockquote',
children: [{ type: 'text', value: '| a | b |\n| --- | --- |' }],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'blockquote',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'a' }] },
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'b' }] },
],
},
],
},
],
},
],
});
});
});
describe('encodeSentinels + decodeSentinels round-trip', () => {
it('preserves childless unsupported nodes via the self-closing marker form', () => {
const original = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'see ' },
{ type: 'asana_macro', data: { kind: 'attachment', id: 42 } },
{ type: 'text', value: '.' },
],
},
{ type: 'thematicBreak' },
],
};
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'asana_macro' && node.type !== 'thematicBreak')), {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'see ' },
{ type: 'asana_macro', data: { kind: 'attachment', id: 42 } },
{ type: 'text', value: '.' },
],
},
{ type: 'paragraph', children: [{ type: 'thematicBreak' }] },
],
});
});
it('preserves typed primitive data values (boolean, number) — not just stringified copies', () => {
const original = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'tableCell',
data: { header: true, indent: 2, kind: 'summary' },
children: [{ type: 'text', value: 'cell' }],
},
],
},
],
};
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'tableCell')), original);
});
it('decodes a quoted-string attribute as a string even when the contents read like a JSON literal', () => {
const tree = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: '【 checkbox checked="true" indent="2" 】' },
{ type: 'text', value: '【 end checkbox 】' },
],
},
],
};
assertStrict.deepEqual(decodeSentinels(tree), {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'checkbox', data: { checked: 'true', indent: '2' } }],
},
],
});
});
it('preserves same-tag nesting (custom node containing itself)', () => {
const original = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'jira_panel',
children: [
{ type: 'text', value: 'outer ' },
{ type: 'jira_panel', children: [{ type: 'text', value: 'inner' }] },
{ type: 'text', value: ' more' },
],
},
],
},
],
};
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'jira_panel')), original);
});
it('preserves supported children verbatim inside an encoded parent (block-context placement)', () => {
const original = {
type: 'root',
children: [
{
type: 'jira_panel',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'a ' },
{ type: 'strong', children: [{ type: 'text', value: 'bold' }] },
{ type: 'text', value: ' b' },
],
},
],
},
],
};
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'jira_panel')), original);
});
it('preserves a tree mixing custom nodes with data, childless nodes, nested rejection, and block-context placement', () => {
const original = {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'before ' },
{ type: 'image', data: { src: 'https://example.com/x.png', alt: 'x' } },
{ type: 'text', value: ' after' },
],
},
{
type: 'jira_panel',
data: { panelType: 'info' },
children: [
{
type: 'paragraph',
children: [
{
type: 'asana_macro',
data: { id: '42' },
children: [{ type: 'text', value: 'nested' }],
},
],
},
],
},
],
};
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => !['image', 'jira_panel', 'asana_macro'].includes(node.type))), original);
});
it('preserves nested unsupported block nodes — each marker pair wrapped at block level, no bare text at block scope', () => {
const original = {
type: 'root',
children: [
{
type: 'jira_panel',
children: [
{
type: 'asana_panel',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'x' }] }],
},
],
},
],
};
const reject = (node) => !['jira_panel', 'asana_panel'].includes(node.type);
assertStrict.deepEqual(encodeSentinels(original, reject), {
type: 'root',
children: [
{ type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 asana_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'x' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end asana_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
],
});
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, reject)), original);
});
it('preserves nested unsupported block nodes inside a non-root block container (blockquote)', () => {
const original = {
type: 'root',
children: [
{
type: 'blockquote',
children: [
{
type: 'jira_panel',
children: [
{
type: 'asana_panel',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'x' }] }],
},
],
},
],
},
],
};
const reject = (node) => !['jira_panel', 'asana_panel'].includes(node.type);
assertStrict.deepEqual(encodeSentinels(original, reject), {
type: 'root',
children: [
{
type: 'blockquote',
children: [
{ type: 'paragraph', children: [{ type: 'text', value: '【 jira_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 asana_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: 'x' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end asana_panel 】' }] },
{ type: 'paragraph', children: [{ type: 'text', value: '【 end jira_panel 】' }] },
],
},
],
});
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, reject)), original);
});
it('round-trips a markdown-encoded table whose first row declares headers', () => {
const original = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Name' }] },
{ type: 'tableCell', data: { header: true }, children: [{ type: 'text', value: 'Status' }] },
],
},
{
type: 'tableRow',
children: [
{ type: 'tableCell', children: [{ type: 'text', value: 'Alice' }] },
{ type: 'tableCell', children: [{ type: 'text', value: 'Active' }] },
],
},
],
},
],
};
assertStrict.deepEqual(decodeSentinels(encodeSentinels(original, node => node.type !== 'table')), original);
});
it('round-trips a table whose cells contain unsupported types', () => {
const original = {
type: 'root',
children: [
{
type: 'table',
children: [
{
type: 'tableRow',
children: [
{