From 731b10fdea4ebba90b82315991b65b59f3cb52e3 Mon Sep 17 00:00:00 2001
From: Lilleman auf Larv ` |
| `rule` | ` ` |
+| an attribute its type holds that its element does not write, such as `localId`, on a paragraph, heading, quote, list, list item, code block, rule, emoji, status, date or card | `data-*` named as HTML's `dataset` names it: ` ` |
| any other node, or a node holding what its element cannot | ` its content Ask @Mikael See π See <3 See https://example.com/spec?a=1&b=2 x a "b ${carry('span', emoji, 'π')} a Check the collation. Ask ${carry('span', mention, '')} \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'],
[' ab a \n', ' holds its text, which is not empty: found "
` |
| `text` marked `code`, `em`, `strong` | ``, ``, ``, the first mark outermost |
-| an attribute its type holds that its element does not write, such as `localId`, on a paragraph, heading, quote, list, list item, code block or rule | `data-*` named as HTML's `dataset` names it: ``.
| ADF | HTML |
| --- | --- |
-| `date` | ``, the day in UTC; its other attributes after the class, as below |
+| `date` | ``, the day in UTC |
| `em`, `strong`, `code` | ``, ``, `
` around the text |
| `emoji` | `π`, its `text` the content |
| `hardBreak` | `
` |
@@ -45,10 +45,7 @@ block's `data-*` attributes stand on its ``.
| `text` | its text, escaped |
A `date`, `emoji`, `inlineCard` or `status` writes each other attribute its type holds as a
-`data-*` attribute, named and sorted as on a block, `data-local-id` among them. It rides the carry
-where it holds a mark, `content`, a key or value its type does not hold, or an empty `attrs`; where
-it has no text to show (an emoji or status with no `text`, a date whose `timestamp` names no day, a
-card with no `url`); and where a card holds `data`. A `mention` rides the carry.
+`data-*` attribute, named and sorted as on a block, `data-local-id` among them.
A text node's marks nest in the order its `marks` array lists them, the first outermost:
`manual`. Each text node writes its own elements, so two text nodes a link
@@ -59,7 +56,7 @@ marks are two `` elements.
Text and attribute values escape `&`, `<`, `>` and `"` as `&`, `<`, `>` and `"`.
Attribute values are double-quoted. Every URL is written as the ADF holds it, whatever its scheme
(`docs/decisions.md` Β§HTML writes a URL as the ADF holds it, whatever its scheme): a link's `href`,
-an image's `src`, and the `href` of each link a carry element shows.
+an image's `src`, a card's `href`, and the `href` of each link a carry element shows.
HTML text and attribute values cannot hold these (WHATWG HTML Β§13.1.3): any control but a tab, line
feed or form feed, so a carriage return too, which a parser reads as a line feed; a noncharacter;
@@ -118,6 +115,8 @@ later release writes it as its own element.
non-empty `alt`;
- a heading whose `level` is not 1 to 6, and an ordered list whose `order` is not a safe integer
or is `-0`;
+ - an emoji or status with no `text`, a date whose `timestamp` names no day from year 1 to 9999,
+ and a card with no `url` or holding `data`;
- a code block holding more than one text node, a marked one, or a language holding ASCII
whitespace;
- a rule or hard break holding `content`, and a hard break holding an attribute;
@@ -148,9 +147,9 @@ that node in either spelling: an edit there would be lost, so it is `unmappable-
- Any other HTML is `unmappable-html`: another element, attribute or escape, attributes in another
order, a block on the line of another, a carriage return not followed by a line feed, or
non-empty HTML not ending in a line feed.
-- An emoji, status, date or card element reads back as its node only when it shows what `adfToHtml`
- writes for it: the text is not empty, a date's `datetime` and text are the day its `data-timestamp`
- names, and a card's `href` and text are one URL. Another shape is `unmappable-html`.
+- An emoji, status, date or card element with empty text, a date whose `datetime` or text is not the
+ day its `data-timestamp` names, and a card whose `href` and text are not one URL are
+ `unmappable-html`.
- A `data-*` attribute that its element does not take, such as `data-level` on `
`, or that holds
a value outside its attribute's kind or not written as compact JSON, is `unsupported-node-shape`.
- A carry element's `data-adf` holding invalid JSON is `unmappable-html`. JSON that is no ADF node,
diff --git a/src/html/emit/adf-to-html.test.ts b/src/html/emit/adf-to-html.test.ts
index 921c65b..51cf4b2 100644
--- a/src/html/emit/adf-to-html.test.ts
+++ b/src/html/emit/adf-to-html.test.ts
@@ -140,7 +140,7 @@ test('carries every other node in an adf-carry element holding its JSON', () =>
assert.equal(html(adfToHtml(document(paragraph(text('Ask '), mention)))), '
\n
\n`)), document({ content: [item(paragraph(text('x')))], type: 'bulletList' }))
})
-test('reads an emoji, status, date and URL card from their elements', () => {
+test('reads an emoji, status, date and card holding a URL from their elements', () => {
const inline = (node: AdfNode): AdfDocument | string => read(htmlToAdf(adfToHtmlText(document(paragraph(text('See '), node)))))
const nodes: AdfNode[] = [
{ attrs: { id: '1f389', localId: 'e1', shortName: ':tada:', text: 'π' }, type: 'emoji' },
@@ -204,7 +204,7 @@ test('reads an emoji, status, date and URL card from their elements', () => {
assert.deepEqual(read(htmlToAdf('\n
\n', ' takes start once, before its data-* attributes: found start after data-local-id'],
['
\n
\n', ' takes start once, before its data-* attributes: found start after start'],
['
\n', 'data-wrap holds no boolean written as compact JSON'],
+ ['x
a
\n', ' takes no data-size attribute'], + ['a&b
\n', 'text escapes & as &, < as < and > as >, and holds no character HTML cannot hold: found "&b\\n"'], ] 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)) diff --git a/src/html/parse/node-element.ts b/src/html/parse/node-element.ts index c229ac2..e8e1db9 100644 --- a/src/html/parse/node-element.ts +++ b/src/html/parse/node-element.ts @@ -1,30 +1,30 @@ import type { AdfAttributes, AdfNode } from '../../adf/document.ts' import { foundAt, misspelledText, refused, type Place, type ReadNode, type StartTag } from './source.ts' -import { nodeTypeOfElement, type NodeElement } from '../node-element.ts' +import { htmlDay, nodeTypeOfElement, type NodeElement } from '../node-element.ts' import { readDataAttributes } from '../data-attributes.ts' import { shownText } from '../../adf/shown-text.ts' import { success } from '../../result.ts' -import { unescapedValue } from '../escaping.ts' +import { misspelledAt, unescapedValue } from '../escaping.ts' -// `undefined` where the tag opens no emoji, status, date or URL card element; one that does reads as the node `nodeElement` wrote it from, or is refused. +// `undefined` where the tag opens no emoji, status, date or card element; one that does reads as the node `nodeElement` wrote it from, or is refused. export function readNodeElement(html: string, tag: StartTag, place: Place): ReadNode | undefined { const [kind, ...rest] = tag.attributes const found = kind?.[0] === 'class' ? nodeTypeOfElement(tag.name, kind[1]) : undefined 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, model.vocabulary, model.content === undefined ? [] : [model.content], model.leading) + const data = readDataAttributes(rest, `${tag.name} class="${model.class}"`, model.vocabulary, model.content === undefined ? [] : [model.content], 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) const end = stop < 0 ? html.length : stop const shown = unescapedValue(html.slice(tag.end, end), 'text') - if (shown === undefined) return misspelledText(html, tag.end, place) + 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 ? shownText(node) : shown + const expected = model.content === undefined ? htmlDay(shownText(node)) : shown if (expected === undefined || expected === '' || shown !== expected || (model.leading !== undefined && rest[0]?.[1] !== expected)) { - return refused('unmappable-html', `<${tag.name} class="${model.class}"> shows what adfToHtml writes for its node, ${model.leading === undefined ? '' : `${model.leading} the same, `}then ${closer}; to change the node, edit its data-* attributes: ${foundAt(html, tag.end)}`, html, tag.end, place) + return refused('unmappable-html', `${mismatch(tag.name, model)}: ${foundAt(html, tag.end)}`, html, tag.end, place) } return success({ end: end + closer.length, node }) } @@ -32,3 +32,10 @@ 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 } } + +function mismatch(name: string, model: NodeElement): string { + const element = `<${name} class="${model.class}">` + if (model.leading === 'datetime') return `${element} shows the day its data-timestamp names, in datetime and as its text; to change the date, change all three` + if (model.leading === 'href') return `${element} holds one URL, as its href and as its text; to change the card, change both` + return `${element} holds its text, which is not empty` +}