diff --git a/CHANGELOG.md b/CHANGELOG.md index bc86b51..edffe78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,17 +9,18 @@ libraries and Atlassian's own paths. - `adfToHtml` writes ADF as HTML: paragraphs, headings, lists, quotes, code blocks, rules, hard breaks, a centered, uncaptioned external image holding no attribute but its URL and alt, and - `em`, `strong`, `code` and links as their elements. An unmarked emoji, status, date and card - holding a URL and no `data` are ``, ``, - `
\\n"'], ['\n', ' holds its attributes as name="value", each value escaping & as & and " as ", then >: found "&b=2\\">x"…'], [`a
\n${carry('div', panel, 'Check the collation.
')}\n`, 'a carry element shows what adfToHtml writes for its node, as it or a browser spells it, then ; to change the node, edit its ADF and write the HTML again: found "collation.a\n', 'a 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 "a\\n"'], + ['a
\n', 'a of the dialect is a carry element, holding class="adf-carry" and then data-adf alone, or opens with class="adf-emoji", "adf-mention" or "adf-status": found "a\\n"'], ['\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}\nx\n', 'data-wrap holds no boolean written as compact JSON'],
['\n', ' holds its text, which is not empty: found "\\n"'], - ['\n', ' holds one URL, as its href and as its text; to change the card, change both: found "Spec\\n"'], + ['\n', ' holds one URL, as its href and as its text, or the name and URL its data-data holds, as its text and its href; to change the card, change them together: found "Spec\\n"'], + [`\n`, ' holds one URL, as its href and as its text, or the name and URL its data-data holds, as its text and its href; to change the card, change them together: found "Plan\\n"'], ['
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"'], diff --git a/src/html/parse/node-element.ts b/src/html/parse/node-element.ts index 72bc8b0..8cafaa6 100644 --- a/src/html/parse/node-element.ts +++ b/src/html/parse/node-element.ts @@ -1,41 +1,29 @@ -import type { AdfAttributes, AdfNode } from '../../adf/document.ts' +import type { AdfNode } from '../../adf/document.ts' import { foundAt, misspelledText, refused, type Place, type ReadNode, type StartTag } from './source.ts' -import { htmlDay, nodeTypeOfElement, type NodeElement } from '../node-element.ts' +import { nodeTypeOfElement } from '../node-element.ts' import { readDataAttributes } from '../data-attributes.ts' -import { shownText } from '../../adf/shown-text.ts' import { success } from '../../result.ts' import { misspelledAt, unescapedValue } from '../escaping.ts' -// `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. +// `undefined` where the tag opens no emoji, mention, 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} class="${model.class}"`, model.vocabulary, model.textAttribute === undefined ? [] : [model.textAttribute], model.leading) + const name = `<${tag.name} class="${model.class}">` + if (model.leading !== undefined && rest[0]?.[0] !== model.leading.name) return refused('unmappable-html', `${name} holds ${model.leading.name} 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.written, model.leading?.name) 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 + 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.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) + if (!html.startsWith(closer, end)) return refused('unmappable-html', `${name} holds its text, then ${closer}: ${foundAt(html, end)}`, html, end, place) + const node: AdfNode = { attrs: model.build(data.value, shown), type } + if (model.shown(node) !== shown || (model.leading !== undefined && model.leading.value(node) !== rest[0]?.[1])) { + return refused('unmappable-html', `${name} ${model.mismatch}: ${foundAt(html, tag.end)}`, html, tag.end, place) } return success({ end: end + closer.length, node }) } - -function nodeOf(type: string, model: NodeElement, data: AdfAttributes, shown: string): AdfNode { - return { attrs: model.textAttribute === undefined ? data : { ...data, [model.textAttribute]: 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` -} diff --git a/todo.md b/todo.md index df6787b..47a08fe 100644 --- a/todo.md +++ b/todo.md @@ -12,7 +12,7 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| -| 133 | 0.3.0 | | question | **Write mentions, and inline cards holding `data` or a link mark, as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 4 | 6 by 3 | 3.0 | +| 133 | 0.3.0 | | question | **Write an inline card a link marks as semantic HTML in `adfToHtml` and read it back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 4 | 6 by 3 | 3.0 | | 163 | 0.3.0 | | | **Gather a corpus of real-world ADF documents from public Jira and Confluence sites, each recording where it came from and under what licence.** | 3 | 2 | 4 | 7 | 4 | 6 by 3 | 3.0 | | 164 | 0.3.0 | 163 | | **Publish a feature table setting chosen corpus examples beside what each call writes for them, readable at a glance by people and parseable by LLMs, and kept current by the gate.** | 3 | 2 | 1 | 4 | 5 | 3 by 6, 6 by 4 | 6.2 | | 165 | 0.3.0 | 164 | | **Have a reader panel seated from `## Audience` judge each feature-table row reasonable or not, and file an item for each row it rejects.** | 2 | 2 | 1 | 3 | 3 | 6 by 5 | 7.5 | @@ -69,23 +69,18 @@ ## Details -### 133. Write mentions, and inline cards holding `data` or a link mark, as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element. +### 133. Write an inline card a link marks as semantic HTML in `adfToHtml` and read it back in `htmlToAdf`, in place of the carry element. -The fifth writer panel (`docs/decisions.md` §The HTML dialect) did not reach 5 of 7 on two -spellings, so mentions and cards holding `data` ride the carry until the maintainer settles them. -Class: the HTML dialect's spellings. Earlier entries: `data-*` named as HTML's `dataset` names it, -`data-local-id` over `data-adf-local-id`. Candidates, each following those entries: +Mentions and cards holding `data` are elements since the maintainer took the fifth writer panel's +leading spellings (`docs/decisions.md` §The HTML dialect). A card a link marks still rides the carry, +which shows the link's target as the link and the card's URL as its text, so both stay in sight. +Atlassian's schema gives no card a link mark, so only a hand-built document meets this. -- a mention as `@Mikael` (4 of 7; `data-mention-id` drew 2); -- a card holding `data` and no URL as `Release plan`, `href` the URL its `data` holds (3 of 7; `data-card` drew 2, a `` without `href` 2). - -Where a link marks a card, both the link's target and the card's URL stay in sight, a card showing -only its name included. - -Candidate entry for §The HTML dialect, if the leading spellings stand: "A mention is -`` showing its text. A card -holding `data` and no URL is ``, `href` the URL its `data` -holds, showing its name." The instance it yields for the example above: `Release plan`. +Class: the HTML dialect's spellings. Earlier entries: a card is ``, +and HTML nests no `` in an ``, so the card's element has no room for the link beside it. The +nearest candidate keeps the carry: "A card a link marks rides the carry, which keeps both targets in +sight; no document Atlassian's editor writes holds one." Its instance is today's output, so the +choice is the maintainer's: this item ends with that entry, or with a spelling the maintainer names. ### 163. Gather a corpus of real-world ADF documents from public Jira and Confluence sites, each recording where it came from and under what licence.