3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-13 20:28:28 +01:00

Answer principles round 2: 0 🔴, 3 🟡 (0 misread); the document carry's two clauses pinned, one test per case, the escaped set spelled once in the entities map, UUID v7 ids, the document carry's tail from carryContent

This commit is contained in:
2026-10-28 01:44:00 +01:00
committed by Lillem4n
parent 74add5c813
commit 3366841adf
5 changed files with 75 additions and 57 deletions
+3 -3
View File
@@ -14,12 +14,12 @@ import { portableMarkdownToAdf } from '../markdown/portable/portable-markdown-to
import { toEditorNormal } from '../markdown/portable/editor-normal.ts'
const gateRuns = 1600
const htmlPieces = fc.constantFrom('<', '>', '&', '"', '\n', '\r', '\0', '&amp;', '&quot;', '&#39;', '<p>', '</p>', '<li>', '</li>', '<ul>\n', '\n</ul>', '<br>', '<em>', '</em>', '<a href="x">', '</span>', '</div>', ' data-local-id="x"', ' start="2"', '<div class="adf-carry" data-adf="{&quot;type&quot;:&quot;x&quot;}">', '<img src="x">', 'é🎉')
const htmlPieces = fc.constantFrom('<', '>', '&', '"', '\n', '\r', '\0', '&amp;', '&quot;', '&#39;', '<p>', '</p>', '<li>', '</li>', '<ul>\n', '\n</ul>', '<br>', '<em>', '</em>', '<a href="x">', '</span>', '</div>', ' data-local-id="01a17eec-5ed2-75cf-904d-f4af6746ac07"', ' start="2"', '<div class="adf-carry" data-adf="{&quot;type&quot;:&quot;x&quot;}">', '<img src="x">', 'é🎉')
// What HTML text cannot hold (WHATWG HTML §13.1.3), and the line feed a carry element shows as a line break.
const unshown = /\n|(?![\t\f ])[\p{Cc}\p{Cs}\p{Noncharacter_Code_Point}]/gu
const depthIdentifier = fc.createDepthIdentifier()
const elementAttributes = [' alt=""', ' alt="y"', ' class="language-x"', ' data-local-id="x"', ' start="2"', ' title="t"']
const elementAttributes = [' alt=""', ' alt="y"', ' class="language-x"', ' data-local-id="01a17e97-ca82-785e-9333-6e8199f3b098"', ' start="2"', ' title="t"']
const elementInlinePieces = ['\n', '&amp;', '</p>', '<a href="x">a</a>', '<br>', '<code>c</code>', '<em>a</em>', '<img src="x">', '<p>', '<strong>b</strong>', 'a']
const elementLines = ['<blockquote>\n<p>a</p>\n</blockquote>\n', '<hr>\n', '<li>\n<p></p>\n</li>\n', '<li>\n<p>a</p>\n</li>\n', '<li><p>a</p></li>\n', '<li>a</li>\n', '<p></p>\n', '<p><img src="x" alt=""></p>\n', '<p><img src="x"></p>\n', '<p>a</p>\n', '<pre><code>a</code></pre>\n', '<ul>\n<li>a</li>\n</ul>\n']
const elementText = fc.string({ maxLength: 4, minLength: 1, unit: fc.constantFrom('\n', ' ', '"', '&', '<', 'a', 'é') })
@@ -27,7 +27,7 @@ const elementInline = fc.oneof(
{ arbitrary: fc.record({ marks: fc.subarray<AdfMark>([{ type: 'strong' }, { type: 'em' }, { type: 'code' }, { attrs: { href: 'x' }, type: 'link' }]), text: elementText }).map(({ marks, text }): AdfNode => (marks.length === 0 ? { text, type: 'text' } : { marks, text, type: 'text' })), weight: 4 },
{ arbitrary: fc.constant<AdfNode>({ type: 'hardBreak' }), weight: 1 },
)
const localId = fc.record({ localId: fc.constantFrom('a1', 'b2') }, { noNullPrototype: true, requiredKeys: [] })
const localId = fc.record({ localId: fc.constantFrom('01a17c28-f53a-7ae4-90e8-e3a686cc94e8', '01a17fb9-4022-7162-a34a-627985fb16cc') }, { noNullPrototype: true, requiredKeys: [] })
// The nodes `adfToHtml` writes as their own elements, so a document holding these alone writes no carry element, short of adjacent unmarked text.
const { elementBlock } = fc.letrec<{ elementBlock: AdfNode }>((tie) => {
const blocks = fc.array(tie('elementBlock'), { depthIdentifier, maxLength: 3 })
+2 -1
View File
@@ -2,6 +2,7 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts'
import { anchorOpener } from '../anchor-element.ts'
import { blockNodes } from '../../adf/block-nodes.ts'
import { carriedBlock, carryElement } from './carry-element.ts'
import { carryContent } from '../carry-content.ts'
import { containerElements, headingElement, listStart } from '../block-elements.ts'
import { dataAttributes } from '../data-attributes.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
@@ -28,7 +29,7 @@ export function adfToHtml(document: AdfDocument): Result<string> {
function writeHtml(document: AdfDocument): Result<string> {
const content = writableContent(document)
if (!content.ok) return content
if (content.value === undefined) return success(`${carryElement('div', serializeCanonicalJson(document, 'compact'), '')}\n`)
if (content.value === undefined) return success(`${carryElement('div', serializeCanonicalJson(document, 'compact'), carryContent(document, 'div'))}\n`)
const pending: Pending[] = content.value.length === 0 ? [] : [...blockPieces(content.value, [], 0), '\n'].reverse()
let html = ''
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
+7 -4
View File
@@ -4,11 +4,14 @@ const entities: ReadonlyMap<string, string> = new Map([
['&lt;', '<'],
['&quot;', '"'],
])
const entity = new RegExp([...entities.keys()].join('|'), 'g')
const escapable = new RegExp(`[${[...entities.values()].join('')}]`, 'g')
const escapes: ReadonlyMap<string, string> = new Map([...entities].map(([spelled, character]) => [character, spelled]))
// What HTML text and attribute values cannot hold (WHATWG HTML §13.1.3): any control but a tab, line feed or form feed, a carriage return among them, since a parser reads it as a line feed; a noncharacter; and a lone surrogate, which UTF-8 cannot encode.
const unwritable = /(?![\t\n\f ])[\p{Cc}\p{Cs}\p{Noncharacter_Code_Point}]/u
const unwritables = new RegExp(unwritable.source, 'gu')
// An `&` opening no entity, a character `escapeHtml` escapes standing raw, or one HTML cannot hold.
const misspelling = new RegExp(`&(?!${[...entities.keys()].map((entity) => entity.slice(1)).join('|')})|[<>"]|${unwritable.source}`, 'u')
// A character `escapeHtml` escapes, other than the `&` opening an entity, or one HTML cannot hold.
const misspelling = new RegExp(`(?!${entity.source})${escapable.source}|${unwritable.source}`, 'u')
// The carry's JSON, which spells each character HTML cannot hold as a JSON escape reading back to the same string.
export function holdableJson(json: string): string {
@@ -37,7 +40,7 @@ export function shownValue(value: string, lineFeed: string): string {
// The text or attribute value escaped HTML spells, `undefined` where `writableValue` would not write it so.
export function unescapedValue(html: string): string | undefined {
return misspelledAt(html) < 0 ? html.replace(/&(?:amp|gt|lt|quot);/g, (entity) => entities.get(entity) ?? entity) : undefined
return misspelledAt(html) < 0 ? html.replace(entity, (spelled) => entities.get(spelled) ?? spelled) : undefined
}
// `undefined` where HTML cannot hold the value, so its node rides the carry element.
@@ -46,7 +49,7 @@ export function writableValue(value: string): string | undefined {
}
function escapeHtml(text: string): string {
return text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;')
return text.replace(escapable, (character) => escapes.get(character) ?? character)
}
// An astral noncharacter spells as its two surrogates' escapes.
+59 -47
View File
@@ -21,6 +21,10 @@ function paragraph(...content: AdfNode[]): AdfNode {
return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }
}
function heading(level: number, ...content: AdfNode[]): AdfNode {
return content.length === 0 ? { attrs: { level }, type: 'heading' } : { attrs: { level }, content, type: 'heading' }
}
function text(value: string, ...marks: AdfMark[]): AdfNode {
return marks.length === 0 ? { text: value, type: 'text' } : { marks, text: value, type: 'text' }
}
@@ -42,6 +46,8 @@ function message(result: Result<AdfDocument, ParseError>): string {
}
const documentCarry = carry('div', { type: 'doc', version: 1 }, '')
const mention: AdfNode = { attrs: { id: ids.mention, text: '@Mikael' }, type: 'mention' }
const panel: AdfNode = { attrs: { panelType: 'info' }, content: [paragraph(text('Check the collation.'))], type: 'panel' }
const rule = JSON.stringify({ type: 'unknownBlock' }).replaceAll('"', '&quot;')
// The carry element of `node`, showing `shown`.
@@ -49,13 +55,18 @@ function carry(tag: 'div' | 'span', node: unknown, shown: string): string {
return `<${tag} class="adf-carry" data-adf="${JSON.stringify(node).replaceAll('"', '&quot;')}">${shown}</${tag}>`
}
function nested(value: unknown, levels: number, wrap: (inner: unknown) => unknown): unknown {
let wrapped = value
for (let level = 0; level < levels; level += 1) wrapped = wrap(wrapped)
return wrapped
}
test('reads an empty string as a document of no blocks, and the document\'s carry as one holding no content key', () => {
assert.deepEqual(read(htmlToAdf('')), document())
assert.deepEqual(read(htmlToAdf(`${documentCarry}\n`)), { type: 'doc', version: 1 })
})
test('reads paragraphs, headings and rules, a line feed after each', () => {
const heading = (level: number, ...content: AdfNode[]): AdfNode => (content.length === 0 ? { attrs: { level }, type: 'heading' } : { attrs: { level }, content, type: 'heading' })
assert.deepEqual(read(htmlToAdf('<h1>Level 1</h1>\n<p>Read the manual.</p>\n<hr>\n<h6>Level 6</h6>\n')), document(heading(1, text('Level 1')), paragraph(text('Read the manual.')), { type: 'rule' }, heading(6, text('Level 6'))))
assert.deepEqual(read(htmlToAdf('<p></p>\n<h2></h2>\n')), document(paragraph(), heading(2)))
})
@@ -123,21 +134,10 @@ test('reads an image standing alone in a paragraph, a link around it where one m
})
test('reads a carry element from its data-adf, what it shows read back as adfToHtml writes it', () => {
const panel: AdfNode = { attrs: { panelType: 'info' }, content: [paragraph(text('Check the collation.'))], type: 'panel' }
const mention: AdfNode = { attrs: { id: ids.mention, text: '@Mikael' }, type: 'mention' }
assert.deepEqual(read(htmlToAdf(`${carry('div', panel, '<p>Check the collation.</p>')}\n<p>Ask ${carry('span', mention, '@Mikael')}</p>\n`)), document(panel, paragraph(text('Ask '), mention)))
assert.deepEqual(read(htmlToAdf(`<p>${carry('span', text('a\u0085b', { type: 'underline' }), 'ab').replace('\u0085', '\\u0085')}</p>\n`)), document(paragraph(text('a\u0085b', { type: 'underline' }))))
})
test('refuses an edit to what a carry element shows, naming what it found there', () => {
const panel: AdfNode = { attrs: { panelType: 'info' }, content: [paragraph(text('Check the collation.'))], type: 'panel' }
const edited = htmlToAdf(`<p>a</p>\n${carry('div', panel, '<p>Check the <b>collation</b>.</p>')}\n`)
assert.equal(refusal(edited), 'unmappable-html at /content/1 line 2 offset 9')
assert.match(message(edited), /: found "<b>collation<\/b>\.<\/p><\/d"…$/)
const mention: AdfNode = { attrs: { id: ids.mention, text: '@Mikael' }, type: 'mention' }
assert.equal(refusal(htmlToAdf(`<p>Ask ${carry('span', mention, '')}</p>\n`)), 'unmappable-html at /content/0/content/1 line 1 offset 0')
})
test('reads a carry element holding a node HTML writes as its own element as that node', () => {
assert.deepEqual(read(htmlToAdf(`${carry('div', paragraph(text('x')), '<p>x</p>')}\n`)), document(paragraph(text('x'))))
assert.deepEqual(read(htmlToAdf(`<p>a${carry('span', text('b'), 'b')}</p>\n`)), document(paragraph(text('a'), text('b'))))
@@ -185,6 +185,21 @@ const refusals: Readonly<Record<string, readonly (readonly [string, string])[]>>
['<p><span class="adf-carry" data-adf="[]"></span></p>\n', 'unsupported-node-shape at /content/0/content/0 line 1 offset 0'],
['<p><span>a</span></p>\n', 'unmappable-html at /content/0/content/0 line 1 offset 0'],
],
'refuses an edit to what a carry element shows': [
[`<p>a</p>\n${carry('div', panel, '<p>Check the <b>collation</b>.</p>')}\n`, 'unmappable-html at /content/1 line 2 offset 9'],
[`<p>Ask ${carry('span', mention, '')}</p>\n`, 'unmappable-html at /content/0/content/1 line 1 offset 0'],
],
'refuses the document\'s carry anywhere but as the whole document, a line feed ending it': [
[documentCarry, 'unmappable-html at / line 1 offset 0'],
[`${documentCarry}\n\n`, `unmappable-html at / line 2 offset ${documentCarry.length + 1}`],
[`${documentCarry}X`, 'unmappable-html at / line 1 offset 0'],
[`${documentCarry}X<hr>\n`, 'unmappable-html at / line 1 offset 0'],
[`${documentCarry.replace('></div>', '>x</div>')}\n`, 'unmappable-html at / line 1 offset 0'],
[`${documentCarry}\n<hr>\n`, 'unsupported-node-shape at /content/0 line 1 offset 0'],
[`<hr>\n${documentCarry}\n`, 'unsupported-node-shape at /content/1 line 2 offset 5'],
[`<blockquote>\n${documentCarry}\n</blockquote>\n`, 'unsupported-node-shape at /content/0/content/0 line 2 offset 13'],
[`${carry('div', { type: 'doc', version: 2 }, '')}\n`, 'unsupported-node-shape at /content/0 line 1 offset 0'],
],
'refuses a data-* attribute out of the order adfToHtml writes as unmappable-html, and one no attribute of its node spells as unsupported-node-shape': [
['<hr data-style="a" data-color="b">\n', 'unmappable-html at /content/0 line 1 offset 0'],
['<hr data-color="a" data-color="b">\n', 'unmappable-html at /content/0 line 1 offset 0'],
@@ -245,35 +260,30 @@ for (const [group, cases] of Object.entries(refusals)) {
for (const [html, expected] of cases) test(`${group}: ${JSON.stringify(html)}`, () => assert.equal(refusal(htmlToAdf(html)), expected))
}
test('refuses a value that is not a string as not-a-string', () => {
for (const value of [null, 5, undefined, {}]) {
const notStrings: Readonly<Record<string, unknown>> = { null: null, number: 5, object: {}, undefined: undefined }
for (const [kind, value] of Object.entries(notStrings)) {
test(`refuses a value that is not a string as not-a-string: ${kind}`, () => {
// @ts-expect-error -- a caller without types can pass anything.
assert.equal(refusal(htmlToAdf(value)), 'not-a-string at / line 1 offset 0')
}
})
test('refuses the document\'s carry anywhere but as the whole document, a line feed ending it', () => {
assert.equal(refusal(htmlToAdf(documentCarry)), 'unmappable-html at / line 1 offset 0')
assert.equal(refusal(htmlToAdf(`${documentCarry}\n\n`)), `unmappable-html at / line 2 offset ${documentCarry.length + 1}`)
assert.equal(refusal(htmlToAdf(`${documentCarry.replace('></div>', '>x</div>')}\n`)), 'unmappable-html at / line 1 offset 0')
assert.equal(refusal(htmlToAdf(`${documentCarry}\n<hr>\n`)), 'unsupported-node-shape at /content/0 line 1 offset 0')
assert.equal(refusal(htmlToAdf(`<hr>\n${documentCarry}\n`)), 'unsupported-node-shape at /content/1 line 2 offset 5')
assert.equal(refusal(htmlToAdf(`<blockquote>\n${documentCarry}\n</blockquote>\n`)), 'unsupported-node-shape at /content/0/content/0 line 2 offset 13')
assert.equal(refusal(htmlToAdf(`${carry('div', { type: 'doc', version: 2 }, '')}\n`)), 'unsupported-node-shape at /content/0 line 1 offset 0')
})
})
}
test('refuses a tag of three million attributes, past what one pattern over them all holds, as a value', () => {
assert.equal(refusal(htmlToAdf(`<p${' a=""'.repeat(3_000_000)}>x</p>\n`)), 'unmappable-html at /content/0 line 1 offset 0')
})
test('names what it found where reading stopped, a character no one sees as its escape', () => {
assert.equal(message(htmlToAdf('<p>a<b>b</b></p>\n')), '<b> as written here is no inline element of the dialect: found "<b>b</b></p>\\n"')
assert.equal(message(htmlToAdf('<p>a > b</p>\n')), 'text escapes &, <, > and " as &amp;, &lt;, &gt; and &quot;, and holds no character HTML cannot hold: found "> b</p>\\n"')
assert.equal(message(htmlToAdf('<p><a href="a"b">a</a></p>\n')), '<a> holds its attributes as name="value", each value escaped as text is, then >: found "b\\">a</a></p>\\n"')
assert.equal(message(htmlToAdf('<p>a&nbsp;\u00a0</p>\n')), 'text escapes &, <, > and " as &amp;, &lt;, &gt; and &quot;, and holds no character HTML cannot hold: found "&nbsp;\\u00a0</p>\\n"')
assert.equal(message(htmlToAdf('<pre><code>a</code></pre\n')), 'a code block\'s text ends at </code></pre>: found "</code></pre\\n"')
assert.equal(message(htmlToAdf('<ul>\n<li>a</ul>\n')), 'an item\'s line ends at </li>, or at a line feed before the item\'s next block: found "</ul>\\n"')
})
const messages: readonly (readonly [string, string])[] = [
['<p>a<b>b</b></p>\n', '<b> as written here is no inline element of the dialect: found "<b>b</b></p>\\n"'],
['<p>a > b</p>\n', 'text escapes &, <, > and " as &amp;, &lt;, &gt; and &quot;, and holds no character HTML cannot hold: found "> b</p>\\n"'],
['<p><a href="a"b">a</a></p>\n', '<a> holds its attributes as name="value", each value escaped as text is, then >: found "b\\">a</a></p>\\n"'],
['<p>a&nbsp;\u00a0</p>\n', 'text escapes &, <, > and " as &amp;, &lt;, &gt; and &quot;, and holds no character HTML cannot hold: found "&nbsp;\\u00a0</p>\\n"'],
['<pre><code>a</code></pre\n', 'a code block\'s text ends at </code></pre>: found "</code></pre\\n"'],
['<ul>\n<li>a</ul>\n', 'an item\'s line ends at </li>, or at a line feed before the item\'s next block: found "</ul>\\n"'],
[`<p>a</p>\n${carry('div', panel, '<p>Check the <b>collation</b>.</p>')}\n`, 'a carry element shows its node\'s text as adfToHtml writes it, then ends at </div>; an edit to it is no edit to the node: found "<b>collation</b>.</p></d"…'],
]
for (const [html, expected] of messages) test(`names what it found where reading stopped, a character no one sees as its escape: ${JSON.stringify(html)}`, () => assert.equal(message(htmlToAdf(html)), expected))
test('reads blocks nested 500 levels, and refuses the next level, a list\'s items one level below it', () => {
const quotes = (levels: number): string => `${'<blockquote>\n'.repeat(levels)}<p>x</p>${'\n</blockquote>'.repeat(levels)}\n`
@@ -286,22 +296,24 @@ test('reads blocks nested 500 levels, and refuses the next level, a list\'s item
})
test('refuses a carried node whose JSON nests past the levels its depth leaves, and an inline carry past 500', () => {
let value: unknown = 1
for (let level = 0; level < 498; level += 1) value = [value]
const carried = carry('div', { attrs: { a: value }, type: 'unknownBlock' }, '')
const carried = carry('div', { attrs: { a: nested(1, 498, (inner) => [inner]) }, type: 'unknownBlock' }, '')
assert.equal(refusal(htmlToAdf(`${carried}\n`)), 'read')
assert.equal(refusal(htmlToAdf(`<blockquote>\n${carried}\n</blockquote>\n`)), 'unsupported-nesting-depth at /content/0/content/0 line 2 offset 13')
let inline: unknown = { text: 'x', type: 'text' }
for (let level = 0; level < 500; level += 1) inline = { content: [inline], type: 'unknownInline' }
const inline = nested({ text: 'x', type: 'text' }, 500, (inner) => ({ content: [inner], type: 'unknownInline' }))
assert.equal(refusal(htmlToAdf(`<p>${carry('span', inline, '')}</p>\n`)), 'unsupported-nesting-depth at /content/0/content/0 line 1 offset 0')
})
test('reads back what adfToHtml writes for the documents its own tests write', () => {
const bare: AdfDocument = { type: 'doc', version: 1 }
const panel: AdfNode = { attrs: { panelType: 'info' }, content: [paragraph(text('Run this:')), { attrs: { language: 'python' }, content: [text('if ready:\n deploy()')], type: 'codeBlock' }], type: 'panel' }
for (const written of [document(panel), document(paragraph(text('a'), text('b'), text('c', { type: 'em' }), text('d'), text('e'))), bare]) {
const html = adfToHtml(written)
// The documents adfToHtml's own tests write, by what each holds.
const written: Readonly<Record<string, AdfDocument>> = {
'a panel holding a code block': document({ attrs: { panelType: 'info' }, content: [paragraph(text('Run this:')), { attrs: { language: 'python' }, content: [text('if ready:\n deploy()')], type: 'codeBlock' }], type: 'panel' }),
'adjacent unmarked text': document(paragraph(text('a'), text('b'), text('c', { type: 'em' }), text('d'), text('e'))),
'no content key': { type: 'doc', version: 1 },
}
for (const [holding, adf] of Object.entries(written)) {
test(`reads back what adfToHtml writes for a document holding ${holding}`, () => {
const html = adfToHtml(adf)
assert.ok(html.ok)
assert.deepEqual(read(htmlToAdf(html.value)), written)
}
})
assert.deepEqual(read(htmlToAdf(html.value)), adf)
})
}
+4 -2
View File
@@ -3,6 +3,7 @@ import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
import type { FirstParagraphForm } from '../first-paragraph.ts'
import type { ParseError, Result } from '../../result.ts'
import { blockNodes } from '../../adf/block-nodes.ts'
import { carryContent } from '../carry-content.ts'
import { centeredImage, externalMedia } from '../../adf/plain-image.ts'
import { containerElements, headingElements, headingLevel, readListStart, type ContainerType } from '../block-elements.ts'
import { firstParagraphForm } from '../first-paragraph.ts'
@@ -42,6 +43,7 @@ const blockReaders: ReadonlyMap<string, BlockReader> = new Map([
])
const codeCloser = '</code></pre>'
const documentCarryAlone = 'a carry element holding the document stands alone, as the whole document'
const documentCarryEnd = `${carryContent({ type: 'doc' }, 'div')}</div>`
const emptyDocument = serializeCanonicalJson({ type: 'doc', version: 1 }, 'compact')
const formRefusals: Readonly<Record<FirstParagraphForm, string>> = {
bare: "an item's first paragraph holding content and no attribute stands bare on the item's line: <li>Bolt</li>",
@@ -67,8 +69,8 @@ function readHtml(html: string): Result<AdfDocument> {
function readDocumentCarry(html: string): Result<AdfDocument, ParseError> | undefined {
const tag = readStartTag(html, 0)
if (tag?.name !== 'div' || readCarryAttributes(tag.attributes) !== emptyDocument) return undefined
if (!html.startsWith('</div>', tag.end)) return refused('unmappable-html', `a carry element holding the document shows nothing: ${foundAt(html, tag.end)}`, html, tag.end, undefined)
const end = tag.end + '</div>'.length
if (!html.startsWith(documentCarryEnd, tag.end)) return refused('unmappable-html', `a carry element holding the document shows nothing: ${foundAt(html, tag.end)}`, html, tag.end, undefined)
const end = tag.end + documentCarryEnd.length
if (html.startsWith('\n', end) && isBlockStart(html, end + 1)) return refused('unsupported-node-shape', documentCarryAlone, html, 0, { index: 0, parent: undefined })
return end === html.length - 1 && html.endsWith('\n') ? success({ type: 'doc', version: 1 }) : partedRefusal(html, end, undefined)
}