import type { AdfAttributes, AdfDocument, AdfNode } from '../../adf/document.ts'
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'
import { foundAt, misread, readStartTag, refused, startTagName, type Place, type ReadNode, type StartTag } from './source.ts'
import { largestNesting } from '../../nesting.ts'
import { misspelledAt, unescapedValue } from '../escaping.ts'
import { misspelledText, readInline } from './inline.ts'
import { nodeAttrs } from '../../adf/document.ts'
import { notAString } from '../../not-a-string.ts'
import { positioned, success } from '../../result.ts'
import { readAnchorAttributes } from '../anchor-element.ts'
import { readCarry } from './carry-element.ts'
import { readCarryAttributes } from '../carry-attributes.ts'
import { readDataAttributes } from '../data-attributes.ts'
import { readImageAttributes } from '../image-element.ts'
import { readLanguageClass } from '../language-class.ts'
import { serializeCanonicalJson } from '../../canonical-json.ts'
import { withinEngineLimits } from '../../length-bound.ts'
type BlockReader = (html: string, tag: StartTag, place: Place, open: Open) => Result on the item's line, straight after `, so it opens a paragraph.
function tryImage(html: string, index: number, place: Place): ReadNode | undefined {
const opener = readStartTag(html, index)
const anchor = opener?.name === 'a' ? opener : undefined
const image = anchor === undefined ? opener : readStartTag(html, anchor.end)
if (image?.name !== 'img') return undefined
const node = imageNode(image, anchor)
const closer = anchor === undefined ? '
holds , or naming the language', 'no code element of the dialect, which writes or ')
return refused('unmappable-html', stop.message, html, stop.at, place)
}
const next = html.indexOf('<', code.end)
const end = next < 0 ? html.length : next
const spelled = html.slice(code.end, end)
const text = unescapedValue(spelled)
if (text === undefined) return misspelledText(html, code.end + misspelledAt(spelled), place)
if (!html.startsWith(codeCloser, end)) return refused('unmappable-html', `a code block's text ends at ${codeCloser}: ${foundAt(html, end)}`, html, end, place)
const { language } = named
return success({ end: end + codeCloser.length, kind: 'node', node: built('codeBlock', language === undefined ? data.value : { ...data.value, language }, text === '' ? [] : [{ text, type: 'text' }]) })
}
function readRule(html: string, tag: StartTag, place: Place): Result {
const data = readDataAttributes(tag.attributes, 'rule', blockNodes.rule.attributes, [])
return data.fault === undefined ? success({ end: tag.end, kind: 'node', node: built('rule', data.value, []) }) : refused(data.fault.code, data.fault.message, html, tag.start, place)
}
function readBlockCarry(html: string, tag: StartTag, place: Place, open: Open): Result {
if (readCarryAttributes(tag.attributes) === emptyDocument) return refused('unsupported-node-shape', documentCarryAlone, html, tag.start, place)
return asStep(readCarry(html, tag, 'div', largestNesting - open.depth, place))
}
function containerReader(type: ContainerType): BlockReader {
return (html, tag, place, open) => readContainer(html, tag, type, place, open)
}
// A quote's blocks, or a list's items, stand a line each between its tags; a container holding no content key holds nothing.
function readContainer(html: string, tag: StartTag, type: ContainerType, place: Place, open: Open): Result {
const [first, ...rest] = tag.attributes
const start = type === 'orderedList' && first?.[0] === 'start' ? first[1] : undefined
const order = start === undefined ? undefined : readListStart(start)
if (start !== undefined && order === undefined) {
return refused('unmappable-html', `an ordered list starts at an integer, written as JavaScript writes it: found start="${start}"`, html, tag.start, place)
}
const data = readDataAttributes(start === undefined ? tag.attributes : rest, type, blockNodes[type].attributes, type === 'orderedList' ? ['order'] : [])
if (data.fault !== undefined) return refused(data.fault.code, data.fault.message, html, tag.start, place)
const node = built(type, order === undefined ? data.value : { order, ...data.value }, [])
const closer = `${tag.name}>`
if (html.startsWith(closer, tag.end)) return success({ end: tag.end + closer.length, kind: 'node', node })
if (!html.startsWith('\n', tag.end)) return refused('unmappable-html', `<${tag.name}> holds ${closer} straight after it, or a line feed and its first ${type === 'blockquote' ? 'block' : 'item'}: ${foundAt(html, tag.end)}`, html, tag.end, place)
return success({ end: tag.end + 1, kind: 'opened', open: { closer: `\n${closer}`, content: [], depth: open.depth + 1, kind: type === 'blockquote' ? 'blocks' : 'list', node, place } })
}
function readItem(html: string, index: number, place: Place, depth: number): Result {
const tag = readStartTag(html, index)
if (tag?.name !== 'li') {
const stop = misread(html, index, 'a list holds items, one to a line', 'no list item of the dialect')
return refused('unmappable-html', stop.message, html, stop.at, place)
}
const data = readDataAttributes(tag.attributes, 'listItem', blockNodes.listItem.attributes, [])
if (data.fault !== undefined) return refused(data.fault.code, data.fault.message, html, tag.start, place)
const item = built('listItem', data.value, [])
if (html.startsWith(' ', tag.end)) return success({ end: tag.end + ''.length, kind: 'node', node: item })
const open: Open = { closer: '\n', content: [], depth, kind: 'item', node: item, place }
return html.startsWith('\n', tag.end) && isBlockStart(html, tag.end + 1) ? success({ end: tag.end + 1, kind: 'opened', open }) : readItemLine(html, tag.end, open)
}
function readItemLine(html: string, index: number, open: Open): Result {
const place = { index: 0, parent: open.place }
const line = readLineParagraph(html, index, place)
if (!line.ok) return line
const { end, node } = line.value
if (!html.startsWith('', end) && !html.startsWith('\n', end)) {
const lineEnd = "an item's line ends at , or at a line feed before the item's next block"
// After a line's ``, or before a block, no inline element can stand.
const stop = line.value.form === 'bare' && !isBlockStart(html, end) ? misread(html, end, `${lineEnd}, and its text spells < as <`, 'no inline element of the dialect') : { at: end, message: `${lineEnd}: ${foundAt(html, end)}` }
return refused('unmappable-html', stop.message, html, stop.at, open.place)
}
const paragraph = asFirstParagraph({ end, node }, line.value.form, html, index, place)
if (!paragraph.ok) return paragraph
if (html.startsWith('', end)) return success({ end: end + ''.length, kind: 'node', node: { ...open.node, content: [node] } })
return success({ end: end + 1, kind: 'opened', open: { ...open, content: [node] } })
}
// The paragraph on an item's line and the form it stands in there, up to `` or to the line feed before the item's next block.
function readLineParagraph(html: string, index: number, place: Place): Result<{ end: number; form: FirstParagraphForm; node: AdfNode }, ParseError> {
const tag = readStartTag(html, index)
if (tag?.name === 'p') {
const paragraph = readInlineBlock(html, tag, 'paragraph', blockNodes.paragraph.attributes, {}, place)
return paragraph.ok ? success({ ...paragraph.value, form: 'element' }) : paragraph
}
const inline = readInline(html, index, place)
if (!inline.ok) return inline
const { end, nodes } = inline.value
const parted = partedLine(html, end, nodes)
return success(parted === undefined ? { end, form: 'bare', node: { content: nodes, type: 'paragraph' } } : { end: end - 1, form: 'bare', node: { content: parted, type: 'paragraph' } })
}
// Only text ends in a line feed, so the text read last holds the one parting the line from the next block; `undefined` where no block follows.
function partedLine(html: string, end: number, nodes: AdfNode[]): AdfNode[] | undefined {
const last = nodes[nodes.length - 1]
if (!isBlockStart(html, end) || !html.endsWith('\n', end) || last?.text === undefined) return undefined
const text = last.text.slice(0, -1)
return [...nodes.slice(0, -1), ...(text === '' ? [] : [{ ...last, text }])]
}
function isBlockStart(html: string, index: number): boolean {
return blockReaders.has(startTagName(html, index) ?? '')
}
// The node an element holding no attribute or content reads as: `adfToHtml` writes a node holding an empty `attrs` or `content` as a carry.
function built(type: string, attrs: AdfAttributes, content: AdfNode[]): AdfNode {
return { ...(Object.keys(attrs).length === 0 ? {} : { attrs }), ...(content.length === 0 ? {} : { content }), type }
}