mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer stability round 1: 0 🔴, 5 🟡 (0 misread); name the span classes in the carry refusal, call the shown-text attribute textAttribute, pin the content, whitespace and wrong-element cases
This commit is contained in:
@@ -159,6 +159,7 @@ test('carries an emoji, status, date or card holding a URL the element cannot ho
|
||||
assert.equal(carried({ attrs: { size: 'big', text: '🎉' }, type: 'emoji' }), true)
|
||||
assert.equal(carried({ attrs: { id: 5, text: '🎉' }, type: 'emoji' }), true)
|
||||
assert.equal(carried({ attrs: { text: '🎉' }, content: [], type: 'emoji' }), true)
|
||||
assert.equal(carried({ attrs: { text: '🎉' }, content: [text('x')], type: 'emoji' }), true)
|
||||
assert.equal(carried({ attrs: { color: 'red' }, type: 'status' }), true)
|
||||
assert.equal(carried({ attrs: { timestamp: 'soon' }, type: 'date' }), true)
|
||||
assert.equal(carried({ attrs: { timestamp: 1756080000000 }, type: 'date' }), true)
|
||||
|
||||
@@ -9,8 +9,8 @@ import { writableValue } from '../escaping.ts'
|
||||
export function nodeElement(node: AdfNode): string | undefined {
|
||||
const model = elementOfNodeType(node.type)
|
||||
if (model === undefined || node.content !== undefined) return undefined
|
||||
const data = dataAttributes(node, model.vocabulary, model.content === undefined ? [] : [model.content])
|
||||
const held = model.content === undefined ? htmlDay(shownText(node)) : nodeAttrs(node)[model.content]
|
||||
const data = dataAttributes(node, model.vocabulary, model.textAttribute === undefined ? [] : [model.textAttribute])
|
||||
const held = model.textAttribute === undefined ? htmlDay(shownText(node)) : nodeAttrs(node)[model.textAttribute]
|
||||
const shown = typeof held === 'string' && held !== '' ? writableValue(held) : undefined
|
||||
if (data === undefined || shown === undefined) return undefined
|
||||
const leading = model.leading === undefined ? '' : ` ${model.leading}="${shown}"`
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import { inlineNodeModel } from '../adf/inline-nodes.ts'
|
||||
|
||||
// `content` names the attribute holding the shown text, none for a date, which shows the day its `timestamp` falls on; `leading` names the attribute right after the class, holding that same text.
|
||||
// `textAttribute` names the attribute holding the shown text, none for a date, which shows the day its `timestamp` falls on; `leading` names the attribute right after the class, holding that same text.
|
||||
export type NodeElement = {
|
||||
class: string
|
||||
content: string | undefined
|
||||
element: 'a' | 'span' | 'time'
|
||||
leading: 'datetime' | 'href' | undefined
|
||||
textAttribute: string | undefined
|
||||
vocabulary: AttributeVocabulary
|
||||
}
|
||||
|
||||
const nodeElements: Readonly<Record<string, NodeElement>> = {
|
||||
date: { class: 'adf-date', content: undefined, element: 'time', leading: 'datetime', vocabulary: attributesOf('date') },
|
||||
emoji: { class: 'adf-emoji', content: 'text', element: 'span', leading: undefined, vocabulary: attributesOf('emoji') },
|
||||
date: { class: 'adf-date', element: 'time', leading: 'datetime', textAttribute: undefined, vocabulary: attributesOf('date') },
|
||||
emoji: { class: 'adf-emoji', element: 'span', leading: undefined, textAttribute: inlineNodeModel('emoji')?.textAttribute, vocabulary: attributesOf('emoji') },
|
||||
// Leaves out `data`, whose spelling is open (todo.md item 133), so a card holding it rides the carry.
|
||||
inlineCard: { class: 'adf-inline-card', content: 'url', element: 'a', leading: 'href', vocabulary: { localId: 'string', url: 'string' } },
|
||||
status: { class: 'adf-status', content: 'text', element: 'span', leading: undefined, vocabulary: attributesOf('status') },
|
||||
inlineCard: { class: 'adf-inline-card', element: 'a', leading: 'href', textAttribute: 'url', vocabulary: { localId: 'string', url: 'string' } },
|
||||
status: { class: 'adf-status', element: 'span', leading: undefined, textAttribute: inlineNodeModel('status')?.textAttribute, vocabulary: attributesOf('status') },
|
||||
}
|
||||
|
||||
function attributesOf(type: string): AttributeVocabulary {
|
||||
return inlineNodeModel(type)?.attributes ?? {}
|
||||
}
|
||||
|
||||
export function classesOfElement(element: string): string[] {
|
||||
return Object.values(nodeElements).filter((candidate) => candidate.element === element).map((candidate) => candidate.class)
|
||||
}
|
||||
|
||||
export function elementOfNodeType(type: string): NodeElement | undefined {
|
||||
return Object.hasOwn(nodeElements, type) ? nodeElements[type] : undefined
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Read } from '../../result.ts'
|
||||
import { carriedNesting, carriedNode, carriedValue } from '../../adf/carried-json.ts'
|
||||
import { carryContent } from '../carry-content.ts'
|
||||
import { carryJsonOffset, readCarryAttributes } from '../carry-attributes.ts'
|
||||
import { classesOfElement } from '../node-element.ts'
|
||||
import { foundAt, refused, type Place, type ReadNode, type StartTag } from './source.ts'
|
||||
import { holdableJson, readsAsShown } from '../escaping.ts'
|
||||
import { spellsCanonicalJson } from '../../canonical-json.ts'
|
||||
@@ -13,7 +14,11 @@ const holder = "a carry element's data-adf"
|
||||
// What the element shows must read back as `adfToHtml` shows its node, so an edit to it is refused rather than dropped (README goal 7).
|
||||
export function readCarry(html: string, tag: StartTag, element: 'div' | 'span', levels: number, place: Place): ReadNode {
|
||||
const json = readCarryAttributes(tag.attributes)
|
||||
if (json === undefined) return refused('unmappable-html', `a <${tag.name}> of the dialect is a carry element, holding class="adf-carry" and then data-adf alone: ${foundAt(html, tag.start)}`, html, tag.start, place)
|
||||
if (json === undefined) {
|
||||
const others = classesOfElement(tag.name)
|
||||
const alternative = others.length === 0 ? '' : `, or opens with class="${others.join('" or "')}"`
|
||||
return refused('unmappable-html', `a <${tag.name}> of the dialect is a carry element, holding class="adf-carry" and then data-adf alone${alternative}: ${foundAt(html, tag.start)}`, html, tag.start, place)
|
||||
}
|
||||
const jsonAt = tag.start + carryJsonOffset(tag.name)
|
||||
const node = heldNode(json, levels, html, jsonAt)
|
||||
if (node.fault !== undefined) return refused(node.fault.code, node.fault.message, html, jsonAt, place)
|
||||
|
||||
@@ -199,6 +199,9 @@ test('reads an emoji, status, date and card holding a URL from their elements',
|
||||
{ attrs: { color: 'yellow', localId: 's1', style: 'bold', text: 'In "review"' }, type: 'status' },
|
||||
{ attrs: { localId: 'd1', timestamp: '1756080000000' }, type: 'date' },
|
||||
{ attrs: { localId: 'c1', url: 'https://example.com/spec?a=1&b=2' }, type: 'inlineCard' },
|
||||
{ attrs: { text: ' ' }, type: 'emoji' },
|
||||
{ attrs: { text: ' ' }, type: 'status' },
|
||||
{ attrs: { url: ' ' }, type: 'inlineCard' },
|
||||
]
|
||||
for (const node of nodes) assert.deepEqual(inline(node), document(paragraph(text('See '), node)))
|
||||
assert.deepEqual(read(htmlToAdf('<p><span class="adf-emoji">a "b</span></p>\n')), document(paragraph({ attrs: { text: 'a\u00a0"b' }, type: 'emoji' })))
|
||||
@@ -282,6 +285,9 @@ const refusals: Readonly<Record<string, readonly (readonly [string, string])[]>>
|
||||
['<p><time datetime="2025-08-25">2025-08-25</time></p>\n', 'unmappable-html at /content/0 line 1 offset 0'],
|
||||
['<p><a class="adf-inline-card" href="https://example.com/">Spec</a></p>\n', 'unmappable-html at /content/0/content/0 line 1 offset 0'],
|
||||
['<p><a class="adf-inline-card">https://example.com/</a></p>\n', 'unmappable-html at /content/0/content/0 line 1 offset 0'],
|
||||
['<p><a class="adf-status">x</a></p>\n', 'unmappable-html at /content/0 line 1 offset 0'],
|
||||
['<p><time class="adf-emoji">x</time></p>\n', 'unmappable-html at /content/0 line 1 offset 0'],
|
||||
['<p><a class="adf-inline-card" data-local-id="x">x</a></p>\n', 'unmappable-html at /content/0/content/0 line 1 offset 0'],
|
||||
['<p><a class="adf-inline-card" href="">a</a></p>\n', 'unmappable-html at /content/0/content/0 line 1 offset 0'],
|
||||
['<p><a class="adf-inline-card" href="https://example.com/" title="t">https://example.com/</a></p>\n', 'unmappable-html at /content/0/content/0 line 1 offset 0'],
|
||||
['<p><a class="adf-inline-card" href="https://example.com/" data-data="{}">https://example.com/</a></p>\n', 'unsupported-node-shape at /content/0/content/0 line 1 offset 0'],
|
||||
@@ -392,7 +398,7 @@ const messages: readonly (readonly [string, string])[] = [
|
||||
['<p><em></em></p>\n', 'a mark element holds text, or one other mark element around text: found "</em></p>\\n"'],
|
||||
['<p><strong><a href="https://x.test/?a=1&b=2">x</a></strong></p>\n', '<a> holds its attributes as name="value", each value escaping & as & and " as ", then >: found "&b=2\\">x</a></strong></p>"…'],
|
||||
[`<p>a</p>\n${carry('div', panel, '<p>Check the <b>collation</b>.</p>')}\n`, 'a carry element shows what adfToHtml writes for its node, as it or a browser spells it, then </div>; to change the node, edit its ADF and write the HTML again: found "<b>collation</b>.</p></d"…'],
|
||||
['<p><span>a</span></p>\n', 'a <span> of the dialect is a carry element, holding class="adf-carry" and then data-adf alone: found "<span>a</span></p>\\n"'],
|
||||
['<p><span>a</span></p>\n', 'a <span> of the dialect is a carry element, holding class="adf-carry" and then data-adf alone, or opens with class="adf-emoji" or "adf-status": found "<span>a</span></p>\\n"'],
|
||||
['<div class="adf-carry" data-adf="{ "type":"x"}"></div>\n', 'a carry element spells its node\'s JSON canonically: compact, keys sorted, each character HTML cannot hold spelled as a JSON escape: found "{ "type":""…'],
|
||||
[`${documentCarry}\n<hr>\n`, 'a carry element holding the document stands alone, as the whole document: found "\\n<hr>\\n"'],
|
||||
[`<hr>\n${documentCarry}\n`, 'a carry element holding the document stands alone, as the whole document: found "<div class=\\"adf-carry\\" d"…'],
|
||||
|
||||
@@ -13,7 +13,7 @@ export function readNodeElement(html: string, tag: StartTag, place: Place): Read
|
||||
if (found === undefined) return undefined
|
||||
const [type, model] = found
|
||||
if (model.leading !== undefined && rest[0]?.[0] !== model.leading) return refused('unmappable-html', `<${tag.name} class="${model.class}"> holds ${model.leading} first, before its data-* attributes: ${foundAt(html, tag.start)}`, html, tag.start, place)
|
||||
const data = readDataAttributes(rest, `${tag.name} class="${model.class}"`, model.vocabulary, model.content === undefined ? [] : [model.content], model.leading)
|
||||
const data = readDataAttributes(rest, `${tag.name} class="${model.class}"`, model.vocabulary, model.textAttribute === undefined ? [] : [model.textAttribute], model.leading)
|
||||
if (data.fault !== undefined) return refused(data.fault.code, data.fault.message, html, tag.start, place)
|
||||
const closer = `</${tag.name}>`
|
||||
const stop = html.indexOf('<', tag.end)
|
||||
@@ -22,7 +22,7 @@ export function readNodeElement(html: string, tag: StartTag, place: Place): Read
|
||||
if (shown === undefined) return misspelledText(html, tag.end + misspelledAt(html.slice(tag.end, end), 'text'), place)
|
||||
if (!html.startsWith(closer, end)) return refused('unmappable-html', `<${tag.name} class="${model.class}"> holds its text, then ${closer}: ${foundAt(html, end)}`, html, end, place)
|
||||
const node = nodeOf(type, model, data.value, shown)
|
||||
const expected = model.content === undefined ? htmlDay(shownText(node)) : shown
|
||||
const expected = model.textAttribute === undefined ? htmlDay(shownText(node)) : shown
|
||||
if (expected === undefined || expected === '' || shown !== expected || (model.leading !== undefined && rest[0]?.[1] !== expected)) {
|
||||
return refused('unmappable-html', `${mismatch(tag.name, model)}: ${foundAt(html, tag.end)}`, html, tag.end, place)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export function readNodeElement(html: string, tag: StartTag, place: Place): Read
|
||||
}
|
||||
|
||||
function nodeOf(type: string, model: NodeElement, data: AdfAttributes, shown: string): AdfNode {
|
||||
return { attrs: model.content === undefined ? data : { ...data, [model.content]: shown }, type }
|
||||
return { attrs: model.textAttribute === undefined ? data : { ...data, [model.textAttribute]: shown }, type }
|
||||
}
|
||||
|
||||
function mismatch(name: string, model: NodeElement): string {
|
||||
|
||||
Reference in New Issue
Block a user