Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b39525cad7 | |||
| d0cb63fcf2 | |||
| 1d3a020926 | |||
| 4579f32fcd | |||
| 6df17a7069 | |||
| 4349df9017 | |||
| d68f311a14 |
@@ -48,6 +48,7 @@ In `docs/decisions.md`:
|
|||||||
- Firefox reads the build
|
- Firefox reads the build
|
||||||
- The coverage floors
|
- The coverage floors
|
||||||
- The size ratchet
|
- The size ratchet
|
||||||
|
- The comprehension floor ships under 7 until items 59, 60, 61 and 62 land
|
||||||
- Properties on a fixed seed
|
- Properties on a fixed seed
|
||||||
- The CommonMark suite checks three ways
|
- The CommonMark suite checks three ways
|
||||||
- The flavour spec is read as a source
|
- The flavour spec is read as a source
|
||||||
@@ -175,8 +176,8 @@ settle it; otherwise four more read, five of seven settle it, and less is a miss
|
|||||||
The verdict lands in `docs/decisions.md`.
|
The verdict lands in `docs/decisions.md`.
|
||||||
|
|
||||||
A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
|
A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
|
||||||
are the people who read and write that format (README `## Audience`). A question about what an app
|
are the people who read and write that format (README `## Audience`). A panel of the developer
|
||||||
relies on goes to the developer personas instead.
|
personas settles a question about what an app relies on.
|
||||||
|
|
||||||
### Stated numbers
|
### Stated numbers
|
||||||
|
|
||||||
|
|||||||
+7
-9
@@ -5,20 +5,18 @@
|
|||||||
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
|
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
|
||||||
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
|
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
|
||||||
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms: text holding an unescaped
|
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms: text holding an unescaped
|
||||||
`!adf:` is claimed, and `adf` is an ordinary code block language. Convert stored markdown per
|
`!adf:` is claimed. Convert stored markdown per `MIGRATION.md`.
|
||||||
`MIGRATION.md`.
|
|
||||||
- **Breaking:** the block carry is a code fence whose info string `adf:<type>` names the node's
|
- **Breaking:** the block carry is a code fence whose info string `adf:<type>` names the node's
|
||||||
type, its body the node's JSON without `type`, and a code fence whose info string opens `adf:` is
|
type, its body the node's JSON without `type`, and a code fence whose info string opens `adf:` is
|
||||||
claimed. Convert stored markdown per `MIGRATION.md`.
|
claimed, and `adf` is an ordinary code block language. Convert stored markdown per `MIGRATION.md`.
|
||||||
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a
|
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a
|
||||||
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
|
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
|
||||||
spells a document holding no `content` key.
|
spells a document holding no `content` key.
|
||||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects as
|
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as `JSON.parse` builds it: two adjacent text
|
||||||
`JSON.parse` builds them: two adjacent text nodes CommonMark would read back as one are parted by
|
nodes CommonMark would read back as one are parted by `!adf:textBreak{}`, an empty `attrs`,
|
||||||
`!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled `{attrs=empty}`,
|
`content` or `marks` is spelled `{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is
|
||||||
`{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock` of several text
|
spelled `-0`, and a `codeBlock` of several text nodes is a fence per node. A `codeBlock` holding
|
||||||
nodes is a fence per node. A `codeBlock` holding other than plain text nodes rides the block
|
other than plain text nodes rides the block carry, where it was refused.
|
||||||
carry, where it was refused.
|
|
||||||
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
|
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
|
||||||
CommonMark escape spells is written as `!adf:link[text]{attrs}`.
|
CommonMark escape spells is written as `!adf:link[text]{attrs}`.
|
||||||
- **Breaking:** some directive refusals carry `malformed-directive` where they carried
|
- **Breaking:** some directive refusals carry `malformed-directive` where they carried
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ turning each directive into text and each carried node into an `adf` code block.
|
|||||||
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
||||||
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by the
|
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by the
|
||||||
tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`.
|
tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`.
|
||||||
`0.1.0` read empty markdown to that shape, meaning the empty document.
|
`0.1.0` built that shape from empty markdown, meaning the empty document.
|
||||||
|
|
||||||
### Convert markdown
|
### Convert markdown
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ emit refuses:
|
|||||||
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
||||||
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
||||||
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
||||||
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive (`!adf:textBreak{}`, `!adf:listBreak`, `!adf:doc`) stands where it parts nothing | make the edit the message opens with; `spec/flavour.md` lists every type's attributes and body |
|
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive stands out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` beside another block | fix what the message names, or give the node the shape it names; `spec/flavour.md` lists every type's attributes and body |
|
||||||
|
|
||||||
## The guarantees
|
## The guarantees
|
||||||
|
|
||||||
|
|||||||
+24
-5
@@ -110,9 +110,9 @@ node's JSON without `type`: ```` ```adf:blockCard ````. A type no info string ca
|
|||||||
rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every
|
rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every
|
||||||
info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the
|
info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the
|
||||||
`language` attribute, and `carry` is an ordinary language. A body holding `type` under a named type,
|
`language` attribute, and `carry` is an ordinary language. A body holding `type` under a named type,
|
||||||
or a bare `adf:` fence whose body's `type` an info string could carry, is `unsupported-node-shape`.
|
or a fence whose info string is `adf:` alone while its body's `type` could be spelled in the info
|
||||||
The reservation claims a fence CommonMark reads as code until `todo.md` item 43 gives CommonMark its
|
string, is `unsupported-node-shape`. The reservation claims a fence CommonMark reads as code until
|
||||||
own reader.
|
`todo.md` item 43 gives CommonMark its own reader.
|
||||||
|
|
||||||
## A code block is a fence per text node
|
## A code block is a fence per text node
|
||||||
|
|
||||||
@@ -496,6 +496,22 @@ config gone missing fails the leg instead of falling back to oxlint's own defaul
|
|||||||
`--deny-warnings`, since a rule from a category this config never names arrives as a warning it
|
`--deny-warnings`, since a rule from a category this config never names arrives as a warning it
|
||||||
exits 0 on.
|
exits 0 on.
|
||||||
|
|
||||||
|
## The comprehension floor ships under 7 until items 59, 60, 61 and 62 land
|
||||||
|
|
||||||
|
2026-10-03, the maintainer. KISS, a technical principle, and its comprehension floor of 7. Valid
|
||||||
|
while `todo.md` items 59, 60, 61 and 62 are open.
|
||||||
|
|
||||||
|
A four-seat comprehension panel scored the project 6 against the floor of 7, and the project ships
|
||||||
|
under it while the restructures it named land: `todo.md` items 59, 60, 61 and 62 end this. The
|
||||||
|
scores are the baseline — a later panel may not score lower on any dimension:
|
||||||
|
|
||||||
|
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Junior | 6.5 | 5.5 | 6 | 5.5 | 6 |
|
||||||
|
| Mid | 7 | 5.5 | 6 | 5 | 5.5 |
|
||||||
|
| Senior | 7 | 5.5 | 6.5 | 5.5 | 6 |
|
||||||
|
| Architect | 6 | 5.5 | 5.5 | 6 | 6 |
|
||||||
|
|
||||||
## Properties on a fixed seed
|
## Properties on a fixed seed
|
||||||
|
|
||||||
2026-09-14, the maintainer. Goal 1. Valid while a red gate must reproduce.
|
2026-09-14, the maintainer. Goal 1. Valid while a red gate must reproduce.
|
||||||
@@ -642,8 +658,8 @@ is the markdown flavour's choice, not ADF's.
|
|||||||
|
|
||||||
## The source parts by ADF and format
|
## The source parts by ADF and format
|
||||||
|
|
||||||
2026-08-27, placement 2026-09-18, `adf/`'s bar 2026-09-21, the maintainer. Goal 2. Valid while
|
2026-08-27, placement 2026-09-18, `adf/`'s bar 2026-09-21, `plain/` 2026-10-03, the maintainer.
|
||||||
each format has a reader and a writer through ADF.
|
Goal 2. Valid while each format has a reader and a writer through ADF.
|
||||||
|
|
||||||
`src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats read
|
`src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats read
|
||||||
lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a content
|
lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a content
|
||||||
@@ -666,3 +682,6 @@ conservative the copy would be. Where the rule is the emitter's own choice, inpu
|
|||||||
rather than restating it, and that is the only import `parse/` takes from `emit/` —
|
rather than restating it, and that is the only import `parse/` takes from `emit/` —
|
||||||
`commonMarkSpelling` and `openingLinkTakesDirective` — so no fixture the emitter writes can be
|
`commonMarkSpelling` and `openingLinkTakesDirective` — so no fixture the emitter writes can be
|
||||||
refused, and a spelling the emitter refuses gives its own error rather than a second name for it.
|
refused, and a spelling the emitter refuses gives its own error rather than a second name for it.
|
||||||
|
`markdown/plain/` holds the plain flavour's own code — the writer's reduction to editor-normal ADF
|
||||||
|
and the conventions and task ids the reader takes — so neither direction reaches into the other
|
||||||
|
for it.
|
||||||
|
|||||||
+3
-2
@@ -181,8 +181,9 @@ product). Block and inline positions canonicalize differently, each fitting wher
|
|||||||
- **Block position**: a fenced code block with info string `adf:` and the node's type, body = the
|
- **Block position**: a fenced code block with info string `adf:` and the node's type, body = the
|
||||||
node's JSON without its `type` — two-space indent, object keys sorted: ```` ```adf:blockCard ````.
|
node's JSON without its `type` — two-space indent, object keys sorted: ```` ```adf:blockCard ````.
|
||||||
A type no info string carries back, by the rule a `codeBlock`'s language follows, leaves the info
|
A type no info string carries back, by the rule a `codeBlock`'s language follows, leaves the info
|
||||||
string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or a bare
|
string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or a fence
|
||||||
`adf:` fence whose body's `type` an info string could carry, is a named error.
|
whose info string is `adf:` alone while its body's `type` could be spelled in the info string, is
|
||||||
|
a named error.
|
||||||
- **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace),
|
- **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace),
|
||||||
JSON-string-escaped into the attribute.
|
JSON-string-escaped into the attribute.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ export function attributeNestingMessage(key: string, type: string): string {
|
|||||||
return `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
return `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
export function holdsOnlyAttributes(node: AdfNode, attributes: readonly string[]): boolean {
|
||||||
if (nodeMarks(node).length > 0 || node.text !== undefined || emptyKeys(node).length > 0) return false
|
if (nodeMarks(node).length > 0 || node.text !== undefined || emptyKeys(node).length > 0) return false
|
||||||
return holdsOnly(nodeAttrs(node), attributes)
|
return holdsOnly(nodeAttrs(node), attributes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './document.ts'
|
|
||||||
import type { JsonValue } from '../json-value.ts'
|
|
||||||
import { identicalMark, identicalMarks, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from './document.ts'
|
|
||||||
|
|
||||||
type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
|
|
||||||
|
|
||||||
type NodeHolder = { content?: AdfNode[] }
|
|
||||||
|
|
||||||
export function sameMark(left: AdfMark, right: AdfMark): boolean {
|
|
||||||
return identicalMark(normalMark(left), normalMark(right))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function joinsNormally(previous: AdfNode, node: AdfNode): boolean {
|
|
||||||
if (!mergesText(previous) || !mergesText(node)) return false
|
|
||||||
return identicalMarks(nodeMarks(previous).map(normalMark), nodeMarks(node).map(normalMark))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toEditorNormal(document: AdfDocument): AdfDocument {
|
|
||||||
const normal: AdfDocument = { type: document.type, version: Object.is(document.version, -0) ? 0 : document.version }
|
|
||||||
const pending: { holder: NodeHolder; source: NodeHolder }[] = [{ holder: normal, source: document }]
|
|
||||||
for (let entry = pending.pop(); entry !== undefined; entry = pending.pop()) {
|
|
||||||
const content = mergeAdjacentText(nodeContent(entry.source), joinsNormally)
|
|
||||||
if (content.length === 0) continue
|
|
||||||
entry.holder.content = content.map((source) => {
|
|
||||||
const holder = normalNode(source)
|
|
||||||
pending.push({ holder, source })
|
|
||||||
return holder
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return normal
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalNode(node: AdfNode): AdfNode {
|
|
||||||
const normal: AdfNode = { type: node.type }
|
|
||||||
const attrs = normalAttributes(nodeAttrs(node))
|
|
||||||
if (attrs !== undefined) normal.attrs = attrs
|
|
||||||
const marks = nodeMarks(node).map(normalMark)
|
|
||||||
if (marks.length > 0) normal.marks = marks
|
|
||||||
if (node.text !== undefined) normal.text = node.text
|
|
||||||
return normal
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalMark(mark: AdfMark): AdfMark {
|
|
||||||
const attrs = normalAttributes(nodeAttrs(mark))
|
|
||||||
return attrs === undefined ? { type: mark.type } : { attrs, type: mark.type }
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalAttributes(attrs: AdfAttributes): AdfAttributes | undefined {
|
|
||||||
if (Object.keys(attrs).length === 0) return undefined
|
|
||||||
const normal = { ...attrs }
|
|
||||||
const pending: JsonContainer[] = [normal]
|
|
||||||
for (let held = pending.pop(); held !== undefined; held = pending.pop()) {
|
|
||||||
if (Array.isArray(held)) for (const [index, value] of held.entries()) held[index] = normalValue(value, pending)
|
|
||||||
else for (const [key, value] of Object.entries(held)) held[key] = normalValue(value, pending)
|
|
||||||
}
|
|
||||||
return normal
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalValue(value: JsonValue, pending: JsonContainer[]): JsonValue {
|
|
||||||
if (Object.is(value, -0)) return 0
|
|
||||||
if (value === null || typeof value !== 'object') return value
|
|
||||||
const copy = Array.isArray(value) ? [...value] : { ...value }
|
|
||||||
pending.push(copy)
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergesText(node: AdfNode): boolean {
|
|
||||||
return node.type === 'text' && Object.keys(nodeAttrs(node)).length === 0
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,10 @@ import test from 'node:test'
|
|||||||
import type { AdfNode } from '../adf/document.ts'
|
import type { AdfNode } from '../adf/document.ts'
|
||||||
import { adfDocument, propertyRuns, propertyTimeout } from './property-harness.ts'
|
import { adfDocument, propertyRuns, propertyTimeout } from './property-harness.ts'
|
||||||
import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts'
|
import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts'
|
||||||
import { adfToPlainMarkdown, reduceToPlain } from '../markdown/emit/plain-reduction.ts'
|
import { adfToPlainMarkdown, reduceToPlain } from '../markdown/plain/adf-to-plain-markdown.ts'
|
||||||
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
||||||
import { markdownToAdf, plainMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts'
|
import { markdownToAdf, plainMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts'
|
||||||
import { toEditorNormal } from '../adf/editor-normal.ts'
|
import { toEditorNormal } from '../markdown/plain/editor-normal.ts'
|
||||||
|
|
||||||
const gateRuns = 1600
|
const gateRuns = 1600
|
||||||
const renamedPrefix = '!adg:'
|
const renamedPrefix = '!adg:'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { blockNodes } from '../adf/block-nodes.ts'
|
|||||||
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
||||||
import { emptyKeys, mergeAdjacentText } from '../adf/document.ts'
|
import { emptyKeys, mergeAdjacentText } from '../adf/document.ts'
|
||||||
import { inlineNodes } from '../adf/inline-nodes.ts'
|
import { inlineNodes } from '../adf/inline-nodes.ts'
|
||||||
import { joinsNormally } from '../adf/editor-normal.ts'
|
import { joinsWhenEditorNormal } from '../markdown/adjacent-text.ts'
|
||||||
import { markAttributes } from '../adf/mark-attributes.ts'
|
import { markAttributes } from '../adf/mark-attributes.ts'
|
||||||
|
|
||||||
type Positions = { block: AdfNode; inline: AdfNode }
|
type Positions = { block: AdfNode; inline: AdfNode }
|
||||||
@@ -78,7 +78,7 @@ function heldAttributes(held: Readonly<Record<string, JsonValue | undefined>>):
|
|||||||
return attrs
|
return attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// An empty attrs, content or marks key, and adjacent text a reader would merge, stay occasional: each takes a spelling outside CommonMark.
|
// An empty attrs, content or marks key, and adjacent text CommonMark would read back as one, stay occasional: each takes a spelling outside CommonMark.
|
||||||
// The copy gives fast-check's null-prototype records the prototype a parsed node has.
|
// The copy gives fast-check's null-prototype records the prototype a parsed node has.
|
||||||
function occasionallyEmpty<T extends AdfMark | AdfNode>(arbitrary: Arbitrary<T>): Arbitrary<T> {
|
function occasionallyEmpty<T extends AdfMark | AdfNode>(arbitrary: Arbitrary<T>): Arbitrary<T> {
|
||||||
return fc.tuple(arbitrary, fc.nat({ max: 9 })).map(([held, roll]) => (roll === 0 ? { ...held } : withoutEmptyKeys(held)))
|
return fc.tuple(arbitrary, fc.nat({ max: 9 })).map(([held, roll]) => (roll === 0 ? { ...held } : withoutEmptyKeys(held)))
|
||||||
@@ -91,7 +91,7 @@ function withoutEmptyKeys<T extends AdfMark | AdfNode>(held: T): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function occasionallyApart(arbitrary: Arbitrary<AdfNode[]>): Arbitrary<AdfNode[]> {
|
function occasionallyApart(arbitrary: Arbitrary<AdfNode[]>): Arbitrary<AdfNode[]> {
|
||||||
return fc.tuple(arbitrary, fc.nat({ max: 5 })).map(([nodes, roll]) => (roll === 0 ? nodes : mergeAdjacentText(nodes, joinsNormally)))
|
return fc.tuple(arbitrary, fc.nat({ max: 5 })).map(([nodes, roll]) => (roll === 0 ? nodes : mergeAdjacentText(nodes, joinsWhenEditorNormal)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function pipeTable({ body, header }: { body: AdfNode[][]; header: AdfNode[] }): AdfNode {
|
function pipeTable({ body, header }: { body: AdfNode[][]; header: AdfNode[] }): AdfNode {
|
||||||
|
|||||||
+1
-1
@@ -3,6 +3,6 @@ export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/documen
|
|||||||
export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts'
|
export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts'
|
||||||
export type { JsonValue } from './json-value.ts'
|
export type { JsonValue } from './json-value.ts'
|
||||||
export { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
export { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||||
export { adfToPlainMarkdown } from './markdown/emit/plain-reduction.ts'
|
export { adfToPlainMarkdown } from './markdown/plain/adf-to-plain-markdown.ts'
|
||||||
export { isAdfDocument } from './adf/document.ts'
|
export { isAdfDocument } from './adf/document.ts'
|
||||||
export { markdownToAdf, plainMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
export { markdownToAdf, plainMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { AdfAttributes, AdfMark, AdfNode } from '../adf/document.ts'
|
||||||
|
import type { JsonValue } from '../json-value.ts'
|
||||||
|
import { identicalMark, identicalMarks, isPlainText, nodeAttrs, nodeMarks } from '../adf/document.ts'
|
||||||
|
import { spellInlineLeafDirective } from './directive-syntax.ts'
|
||||||
|
|
||||||
|
type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
|
||||||
|
|
||||||
|
export const textBreakName = 'textBreak'
|
||||||
|
|
||||||
|
export const textBreakSpelling = spellInlineLeafDirective(textBreakName, '')
|
||||||
|
|
||||||
|
// The lossless reader's rule: CommonMark reads the pair back as one text run (spec/flavour.md, Inline nodes).
|
||||||
|
export function joinsWhenRead(previous: AdfNode, node: AdfNode): boolean {
|
||||||
|
return isPlainText(previous) && isPlainText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The editor's rule, parting from the reader's only on shapes editor-normal ADF erases: an empty attrs, content or marks, and -0 in a mark.
|
||||||
|
export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean {
|
||||||
|
if (!joinsAsEditorText(previous) || !joinsAsEditorText(node)) return false
|
||||||
|
return identicalMarks(nodeMarks(previous).map(normalMark), nodeMarks(node).map(normalMark))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sameMarkWhenEditorNormal(left: AdfMark, right: AdfMark): boolean {
|
||||||
|
return identicalMark(normalMark(left), normalMark(right))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalMark(mark: AdfMark): AdfMark {
|
||||||
|
const attrs = normalAttributes(nodeAttrs(mark))
|
||||||
|
return attrs === undefined ? { type: mark.type } : { attrs, type: mark.type }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalAttributes(attrs: AdfAttributes): AdfAttributes | undefined {
|
||||||
|
if (Object.keys(attrs).length === 0) return undefined
|
||||||
|
const normal = { ...attrs }
|
||||||
|
const pending: JsonContainer[] = [normal]
|
||||||
|
for (let held = pending.pop(); held !== undefined; held = pending.pop()) {
|
||||||
|
if (Array.isArray(held)) for (const [index, value] of held.entries()) held[index] = normalValue(value, pending)
|
||||||
|
else for (const [key, value] of Object.entries(held)) held[key] = normalValue(value, pending)
|
||||||
|
}
|
||||||
|
return normal
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalValue(value: JsonValue, pending: JsonContainer[]): JsonValue {
|
||||||
|
if (Object.is(value, -0)) return 0
|
||||||
|
if (value === null || typeof value !== 'object') return value
|
||||||
|
const copy = Array.isArray(value) ? [...value] : { ...value }
|
||||||
|
pending.push(copy)
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinsAsEditorText(node: AdfNode): boolean {
|
||||||
|
return node.type === 'text' && Object.keys(nodeAttrs(node)).length === 0
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
import type { Flavour } from '../plain-conventions.ts'
|
import type { Flavour } from '../plain/conventions.ts'
|
||||||
import { adfDocumentFault, carriesOnly, isPlainText, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
import { adfDocumentFault, holdsOnlyAttributes, isPlainText, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
import { alertMarker, foldedAlertMarker, leadingMarker, readAlertMarker, readTaskMarker, taskMarker } from '../plain-conventions.ts'
|
import { alertMarker, foldedAlertMarker, leadingMarker, readAlertMarker, readTaskMarker, taskMarker } from '../plain/conventions.ts'
|
||||||
import { blockDirectiveForm, documentSpelling, listBreakSpelling } from '../block-directive.ts'
|
import { blockDirectiveForm, documentSpelling, listBreakSpelling } from '../block-directive.ts'
|
||||||
import { blockNodeModel, blockNodes } from '../../adf/block-nodes.ts'
|
import { blockNodeModel, blockNodes } from '../../adf/block-nodes.ts'
|
||||||
import { carriedBlock } from '../opaque-carry.ts'
|
import { carriedBlock } from '../opaque-carry.ts'
|
||||||
@@ -22,10 +22,10 @@ type BlockSpelling = 'commonmark' | 'directive' | 'list'
|
|||||||
type EmittedBlock = { headroom: number; spelling: BlockSpelling; text: string }
|
type EmittedBlock = { headroom: number; spelling: BlockSpelling; text: string }
|
||||||
type KeptSpelling = { block: EmittedBlock | undefined; depth: number }
|
type KeptSpelling = { block: EmittedBlock | undefined; depth: number }
|
||||||
type PlacedBlock = Omit<EmittedBlock, 'headroom'> & { node: AdfNode }
|
type PlacedBlock = Omit<EmittedBlock, 'headroom'> & { node: AdfNode }
|
||||||
|
type PlacedBlocks = { blocks: readonly PlacedBlock[]; headroom: number }
|
||||||
// Keyed by reference: only a caller building one object per position (the parse, the plain reduction) passes one; a consumer's document may share a node.
|
// Keyed by reference: only a caller building one object per position (the parse, the plain reduction) passes one; a consumer's document may share a node.
|
||||||
export type SpellingMemo = Map<AdfNode, KeptSpelling>
|
export type SpellingMemo = Map<AdfNode, KeptSpelling>
|
||||||
type Walk = { blocks: readonly PlacedBlock[]; headroom: number }
|
type WalkedItem = { node: AdfNode; walk: PlacedBlocks }
|
||||||
type WalkedItem = { node: AdfNode; walk: Walk }
|
|
||||||
export type Writing = { flavour: Flavour; memo: SpellingMemo | undefined }
|
export type Writing = { flavour: Flavour; memo: SpellingMemo | undefined }
|
||||||
|
|
||||||
export const largestListMarker = 999999999
|
export const largestListMarker = 999999999
|
||||||
@@ -48,7 +48,7 @@ export function writeMarkdown(document: AdfDocument, flavour: Flavour): Result<s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// headroom: the least slack any depth guard below the walk has.
|
// headroom: the least slack any depth guard below the walk has.
|
||||||
function walkBlocks(nodes: readonly AdfNode[], path: ConvertErrorPath, depth: number, writing: Writing): Result<Walk> {
|
function walkBlocks(nodes: readonly AdfNode[], path: ConvertErrorPath, depth: number, writing: Writing): Result<PlacedBlocks> {
|
||||||
let headroom = largestNesting - depth
|
let headroom = largestNesting - depth
|
||||||
if (headroom < 0) return tooDeep(path)
|
if (headroom < 0) return tooDeep(path)
|
||||||
const blocks: PlacedBlock[] = []
|
const blocks: PlacedBlock[] = []
|
||||||
@@ -76,15 +76,15 @@ function joinBlocks(blocks: readonly PlacedBlock[], container: BlockContainer):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function separationBetween(previous: PlacedBlock, next: PlacedBlock, container: BlockContainer): string {
|
function separationBetween(previous: PlacedBlock, next: PlacedBlock, container: BlockContainer): string {
|
||||||
const plainPair = previous.spelling !== 'directive' && next.spelling !== 'directive'
|
const bothCommonMark = previous.spelling !== 'directive' && next.spelling !== 'directive'
|
||||||
if (plainPair && next.spelling === 'list') {
|
if (bothCommonMark && next.spelling === 'list') {
|
||||||
if (previous.spelling === 'list' && previous.node.type === next.node.type) {
|
if (previous.spelling === 'list' && previous.node.type === next.node.type) {
|
||||||
const gap = container === 'directive' ? '\n' : '\n\n'
|
const gap = container === 'directive' ? '\n' : '\n\n'
|
||||||
return `${gap}${listBreakSpelling}${gap}`
|
return `${gap}${listBreakSpelling}${gap}`
|
||||||
}
|
}
|
||||||
if (container === 'list-item') return interruptsParagraph(next.node) ? '\n' : '\n\n'
|
if (container === 'list-item') return interruptsParagraph(next.node) ? '\n' : '\n\n'
|
||||||
}
|
}
|
||||||
return container === 'directive' && !plainPair ? '\n' : '\n\n'
|
return container === 'directive' && !bothCommonMark ? '\n' : '\n\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
function interruptsParagraph(node: AdfNode): boolean {
|
function interruptsParagraph(node: AdfNode): boolean {
|
||||||
@@ -181,13 +181,13 @@ function tryTaskList(node: AdfNode, path: ConvertErrorPath, depth: number, writi
|
|||||||
return lines.includes(undefined) ? failure('unsupported-node-shape', 'a task holds blocks no list item spells', path) : success({ headroom, spelling: 'list', text: lines.join('\n') })
|
return lines.includes(undefined) ? failure('unsupported-node-shape', 'a task holds blocks no list item spells', path) : success({ headroom, spelling: 'list', text: lines.join('\n') })
|
||||||
}
|
}
|
||||||
|
|
||||||
function placedBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<Walk> {
|
function placedBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<PlacedBlocks> {
|
||||||
const block = emitBlock(node, path, depth, writing)
|
const block = emitBlock(node, path, depth, writing)
|
||||||
return block.ok ? success({ blocks: [{ ...block.value, node }], headroom: block.value.headroom }) : block
|
return block.ok ? success({ blocks: [{ ...block.value, node }], headroom: block.value.headroom }) : block
|
||||||
}
|
}
|
||||||
|
|
||||||
// The marker leads the first paragraph, or stands as one where the blocks open with another.
|
// The marker leads the first paragraph, or stands as one where the blocks open with another.
|
||||||
function taskBlocks(task: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<Walk> {
|
function taskBlocks(task: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<PlacedBlocks> {
|
||||||
const marker = taskMarker(nodeAttrs(task)['state'])
|
const marker = taskMarker(nodeAttrs(task)['state'])
|
||||||
const markerBlock: PlacedBlock = { node: { type: 'paragraph' }, spelling: 'commonmark', text: marker }
|
const markerBlock: PlacedBlock = { node: { type: 'paragraph' }, spelling: 'commonmark', text: marker }
|
||||||
if (task.type === 'taskItem') {
|
if (task.type === 'taskItem') {
|
||||||
@@ -220,7 +220,7 @@ function directivePair(node: AdfNode, opener: string, body: string, headroom: nu
|
|||||||
return { headroom, spelling: 'directive', text: `${opener}\n${body === '' ? '' : `${body}\n`}${spellDirectiveCloser(node.type)}` }
|
return { headroom, spelling: 'directive', text: `${opener}\n${body === '' ? '' : `${body}\n`}${spellDirectiveCloser(node.type)}` }
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitDirectiveBlock(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, depth: number, walkBody: () => Result<Walk>): Result<EmittedBlock> {
|
function emitDirectiveBlock(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, depth: number, walkBody: () => Result<PlacedBlocks>): Result<EmittedBlock> {
|
||||||
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
||||||
if (blockDirectiveForm(node.type) === 'leaf' && nodeContent(node).length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
if (blockDirectiveForm(node.type) === 'leaf' && nodeContent(node).length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
||||||
if (model.contentModel === 'code') return emitCodeDirective(node, model, path, depth)
|
if (model.contentModel === 'code') return emitCodeDirective(node, model, path, depth)
|
||||||
@@ -230,7 +230,7 @@ function emitDirectiveBlock(node: AdfNode, model: BlockNodeModel, path: ConvertE
|
|||||||
return emitDirectiveBody(node, model, opener.value, path, walkBody)
|
return emitDirectiveBody(node, model, opener.value, path, walkBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string, path: ConvertErrorPath, walkBody: () => Result<Walk>): Result<EmittedBlock> {
|
function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string, path: ConvertErrorPath, walkBody: () => Result<PlacedBlocks>): Result<EmittedBlock> {
|
||||||
if (blockDirectiveForm(node.type) === 'leaf') return success({ headroom: Number.POSITIVE_INFINITY, spelling: 'directive', text: opener })
|
if (blockDirectiveForm(node.type) === 'leaf') return success({ headroom: Number.POSITIVE_INFINITY, spelling: 'directive', text: opener })
|
||||||
if (model.contentModel === 'inline') {
|
if (model.contentModel === 'inline') {
|
||||||
const line = emitInlineLine(nodeContent(node), 'paragraph', path, 'lossless')
|
const line = emitInlineLine(nodeContent(node), 'paragraph', path, 'lossless')
|
||||||
@@ -243,7 +243,7 @@ function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> | undefined {
|
function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, [])) return undefined
|
if (!holdsOnlyAttributes(node, [])) return undefined
|
||||||
const inner = walkBlocks(nodeContent(node), path, depth + 1, writing)
|
const inner = walkBlocks(nodeContent(node), path, depth + 1, writing)
|
||||||
if (!inner.ok) return inner
|
if (!inner.ok) return inner
|
||||||
const text = joinBlocks(inner.value.blocks, 'document')
|
const text = joinBlocks(inner.value.blocks, 'document')
|
||||||
@@ -252,7 +252,7 @@ function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, wri
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, ['language'])) return undefined
|
if (!holdsOnlyAttributes(node, ['language'])) return undefined
|
||||||
const slot = languageSlot(nodeAttrs(node)['language'])
|
const slot = languageSlot(nodeAttrs(node)['language'])
|
||||||
if (slot.kind === 'attribute') return undefined
|
if (slot.kind === 'attribute') return undefined
|
||||||
const texts = fencedTexts(node, path)
|
const texts = fencedTexts(node, path)
|
||||||
@@ -291,7 +291,7 @@ function fencedTexts(node: AdfNode, path: ConvertErrorPath): Result<string[] | u
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tryHeading(node: AdfNode, path: ConvertErrorPath, flavour: Flavour): Result<EmittedBlock> | undefined {
|
function tryHeading(node: AdfNode, path: ConvertErrorPath, flavour: Flavour): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, ['level'])) return undefined
|
if (!holdsOnlyAttributes(node, ['level'])) return undefined
|
||||||
const level = nodeAttrs(node)['level']
|
const level = nodeAttrs(node)['level']
|
||||||
if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined
|
if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined
|
||||||
const hashes = '#'.repeat(level)
|
const hashes = '#'.repeat(level)
|
||||||
@@ -304,11 +304,11 @@ function tryHeading(node: AdfNode, path: ConvertErrorPath, flavour: Flavour): Re
|
|||||||
|
|
||||||
function tryList(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> | undefined {
|
function tryList(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> | undefined {
|
||||||
const ordered = node.type === 'orderedList'
|
const ordered = node.type === 'orderedList'
|
||||||
if (!carriesOnly(node, ordered ? ['order'] : [])) return undefined
|
if (!holdsOnlyAttributes(node, ordered ? ['order'] : [])) return undefined
|
||||||
const items = nodeContent(node)
|
const items = nodeContent(node)
|
||||||
const start = listStart(node, items.length)
|
const start = listStart(node, items.length)
|
||||||
if (start === undefined || items.length === 0) return undefined
|
if (start === undefined || items.length === 0) return undefined
|
||||||
if (items.some((item) => item.type !== 'listItem' || !carriesOnly(item, []))) return undefined
|
if (items.some((item) => item.type !== 'listItem' || !holdsOnlyAttributes(item, []))) return undefined
|
||||||
const walked: WalkedItem[] = []
|
const walked: WalkedItem[] = []
|
||||||
let headroom = Number.POSITIVE_INFINITY
|
let headroom = Number.POSITIVE_INFINITY
|
||||||
for (const [offset, item] of items.entries()) {
|
for (const [offset, item] of items.entries()) {
|
||||||
@@ -356,13 +356,13 @@ function tryListItemLines(inner: string, marker: string): string | undefined {
|
|||||||
|
|
||||||
function tryParagraph(node: AdfNode, path: ConvertErrorPath, flavour: Flavour): Result<EmittedBlock> | undefined {
|
function tryParagraph(node: AdfNode, path: ConvertErrorPath, flavour: Flavour): Result<EmittedBlock> | undefined {
|
||||||
const content = nodeContent(node)
|
const content = nodeContent(node)
|
||||||
if (content.length === 0 || !carriesOnly(node, [])) return undefined
|
if (content.length === 0 || !holdsOnlyAttributes(node, [])) return undefined
|
||||||
const line = emitInlineLine(content, 'paragraph', path, flavour)
|
const line = emitInlineLine(content, 'paragraph', path, flavour)
|
||||||
if (!line.ok) return line
|
if (!line.ok) return line
|
||||||
return success(commonMarkText(line.value))
|
return success(commonMarkText(line.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryRule(node: AdfNode): string | undefined {
|
function tryRule(node: AdfNode): string | undefined {
|
||||||
if (!carriesOnly(node, []) || nodeContent(node).length > 0) return undefined
|
if (!holdsOnlyAttributes(node, []) || nodeContent(node).length > 0) return undefined
|
||||||
return '---'
|
return '---'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { ConvertErrorPath } from '../../result.ts'
|
import type { ConvertErrorPath } from '../../result.ts'
|
||||||
import { carriesOnly, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
import { holdsOnlyAttributes, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
||||||
import { tryImageLine } from './inline-line.ts'
|
import { tryImageLine } from './inline-line.ts'
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ export function tryImage(node: AdfNode, path: ConvertErrorPath): string | undefi
|
|||||||
function imageShape(node: AdfNode): { alt: string | undefined; url: string } | undefined {
|
function imageShape(node: AdfNode): { alt: string | undefined; url: string } | undefined {
|
||||||
const content = nodeContent(node)
|
const content = nodeContent(node)
|
||||||
const media = content[0]
|
const media = content[0]
|
||||||
if (!carriesOnly(node, ['layout']) || serializeCanonicalJson(nodeAttrs(node), 'compact') !== centeredMediaSingle) return undefined
|
if (!holdsOnlyAttributes(node, ['layout']) || serializeCanonicalJson(nodeAttrs(node), 'compact') !== centeredMediaSingle) return undefined
|
||||||
if (media === undefined || content.length !== 1 || media.type !== 'media' || !carriesOnly(media, imageAttributes) || nodeContent(media).length > 0) return undefined
|
if (media === undefined || content.length !== 1 || media.type !== 'media' || !holdsOnlyAttributes(media, imageAttributes) || nodeContent(media).length > 0) return undefined
|
||||||
const attrs = nodeAttrs(media)
|
const attrs = nodeAttrs(media)
|
||||||
const alt = attrs['alt']
|
const alt = attrs['alt']
|
||||||
const url = attrs['url']
|
const url = attrs['url']
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { Flavour } from '../plain-conventions.ts'
|
import type { Flavour } from '../plain/conventions.ts'
|
||||||
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
|
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import type { LineContainer } from '../line-container.ts'
|
import type { LineContainer } from '../line-container.ts'
|
||||||
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type MarkRun, type NodeRange } from './line-escaping.ts'
|
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type MarkRun, type NodeRange } from './line-escaping.ts'
|
||||||
@@ -9,11 +9,11 @@ import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../
|
|||||||
import { identicalMark, isPlainText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { identicalMark, isPlainText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts'
|
import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { highlightDelimiter } from '../plain-conventions.ts'
|
import { highlightDelimiter } from '../plain/conventions.ts'
|
||||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
|
import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { longestBacktickRun } from '../commonmark/backtick-runs.ts'
|
import { longestBacktickRun } from '../commonmark/backtick-runs.ts'
|
||||||
import { readsAsOne, textBreakSpelling } from '../text-break.ts'
|
|
||||||
import { slotLineEndingFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
|
import { slotLineEndingFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
|
||||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||||
import { spellTextDirective } from '../text-directive.ts'
|
import { spellTextDirective } from '../text-directive.ts'
|
||||||
@@ -114,7 +114,7 @@ function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path:
|
|||||||
const emission = emitRun(nodes, 0, 0, context)
|
const emission = emitRun(nodes, 0, 0, context)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
if (emission.value.carry !== undefined) return emission
|
if (emission.value.carry !== undefined) return emission
|
||||||
return success({ segments: carryStrippedWhitespace(emission.value.segments) })
|
return success({ segments: spellEdgeWhitespace(emission.value.segments) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath, flavour: Flavour): Result<LineAttempt> {
|
function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath, flavour: Flavour): Result<LineAttempt> {
|
||||||
@@ -137,32 +137,32 @@ function lineVerdict(segments: readonly InlineSegment[], container: LineContaine
|
|||||||
}
|
}
|
||||||
|
|
||||||
// spec/flavour.md, Inline nodes.
|
// spec/flavour.md, Inline nodes.
|
||||||
function carryStrippedWhitespace(segments: readonly InlineSegment[]): InlineSegment[] {
|
function spellEdgeWhitespace(segments: readonly InlineSegment[]): InlineSegment[] {
|
||||||
const carried: InlineSegment[] = []
|
const spelled: InlineSegment[] = []
|
||||||
for (const [index, segment] of segments.entries()) {
|
for (const [index, segment] of segments.entries()) {
|
||||||
const previous = segments[index - 1]
|
const previous = segments[index - 1]
|
||||||
const next = segments[index + 1]
|
const next = segments[index + 1]
|
||||||
const leading = previous === undefined || previous.text.includes('\n')
|
const leading = previous === undefined || previous.text.includes('\n')
|
||||||
const trailing = next === undefined || next.text.includes('\n')
|
const trailing = next === undefined || next.text.includes('\n')
|
||||||
carried.push(...carryEdges(segment, leading, trailing))
|
spelled.push(...edgeWhitespaceSegments(segment, leading, trailing))
|
||||||
}
|
}
|
||||||
return carried
|
return spelled
|
||||||
}
|
}
|
||||||
|
|
||||||
function carryEdges(segment: InlineSegment, leading: boolean, trailing: boolean): InlineSegment[] {
|
function edgeWhitespaceSegments(segment: InlineSegment, leading: boolean, trailing: boolean): InlineSegment[] {
|
||||||
if (segment.escaping !== 'backslash' && segment.escaping !== 'bracketed') return [segment]
|
if (segment.escaping !== 'backslash' && segment.escaping !== 'bracketed') return [segment]
|
||||||
const head = leading ? (/^[ \t]+/.exec(segment.text)?.[0] ?? '') : ''
|
const head = leading ? (/^[ \t]+/.exec(segment.text)?.[0] ?? '') : ''
|
||||||
const body = segment.text.slice(head.length)
|
const body = segment.text.slice(head.length)
|
||||||
const middle = trailing ? trimTrailingSpace(body) : body
|
const middle = trailing ? trimTrailingSpace(body) : body
|
||||||
const tail = body.slice(middle.length)
|
const tail = body.slice(middle.length)
|
||||||
const edges: InlineSegment[] = []
|
const edges: InlineSegment[] = []
|
||||||
if (head !== '') edges.push(carriedText(head))
|
if (head !== '') edges.push(textDirectiveSegment(head))
|
||||||
if (middle !== '') edges.push({ escaping: segment.escaping, text: middle })
|
if (middle !== '') edges.push({ escaping: segment.escaping, text: middle })
|
||||||
if (tail !== '') edges.push(carriedText(tail))
|
if (tail !== '') edges.push(textDirectiveSegment(tail))
|
||||||
return edges
|
return edges
|
||||||
}
|
}
|
||||||
|
|
||||||
function carriedText(text: string): InlineSegment {
|
function textDirectiveSegment(text: string): InlineSegment {
|
||||||
return syntax(spellTextDirective(text))
|
return syntax(spellTextDirective(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ function emitRun(nodes: readonly AdfNode[], depth: number, firstIndex: number, c
|
|||||||
const runs = inlineRuns(nodes, depth, firstIndex, context.carried)
|
const runs = inlineRuns(nodes, depth, firstIndex, context.carried)
|
||||||
const segments: InlineSegment[] = []
|
const segments: InlineSegment[] = []
|
||||||
for (const [offset, run] of runs.entries()) {
|
for (const [offset, run] of runs.entries()) {
|
||||||
if (partsText(runs[offset - 1], run, context.carried)) segments.push(syntax(textBreakSpelling))
|
if (takesTextBreak(runs[offset - 1], run, context.carried)) segments.push(syntax(textBreakSpelling))
|
||||||
const runContext = { ...context, atBlockEnd: context.atBlockEnd && offset === runs.length - 1 }
|
const runContext = { ...context, atBlockEnd: context.atBlockEnd && offset === runs.length - 1 }
|
||||||
const emitted = run.kind === 'plain' ? emitLeaf(run.node, runContext, run.index) : emitMarkedRun(run.nodes, run.mark, depth, run.index, runContext)
|
const emitted = run.kind === 'plain' ? emitLeaf(run.node, runContext, run.index) : emitMarkedRun(run.nodes, run.mark, depth, run.index, runContext)
|
||||||
if (!emitted.ok) return emitted
|
if (!emitted.ok) return emitted
|
||||||
@@ -213,10 +213,10 @@ function inlineRuns(nodes: readonly AdfNode[], depth: number, firstIndex: number
|
|||||||
return runs
|
return runs
|
||||||
}
|
}
|
||||||
|
|
||||||
function partsText(previous: InlineRun | undefined, run: InlineRun, carried: ReadonlySet<number>): boolean {
|
function takesTextBreak(previous: InlineRun | undefined, run: InlineRun, carried: ReadonlySet<number>): boolean {
|
||||||
if (previous?.kind !== 'plain' || run.kind !== 'plain') return false
|
if (previous?.kind !== 'plain' || run.kind !== 'plain') return false
|
||||||
if (carries(previous.node, carried, previous.index) || carries(run.node, carried, run.index)) return false
|
if (carries(previous.node, carried, previous.index) || carries(run.node, carried, run.index)) return false
|
||||||
return readsAsOne(previous.node, run.node)
|
return joinsWhenRead(previous.node, run.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodePath(context: InlineContext, index: number): ConvertErrorPath {
|
function nodePath(context: InlineContext, index: number): ConvertErrorPath {
|
||||||
@@ -275,7 +275,7 @@ function emitText(node: AdfNode, context: InlineContext, index: number, path: Co
|
|||||||
if (holdsNullCharacter(node.text)) return failure('unspellable-character', 'a text node holds a null character CommonMark replaces', path)
|
if (holdsNullCharacter(node.text)) return failure('unspellable-character', 'a text node holds a null character CommonMark replaces', path)
|
||||||
const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash'
|
const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash'
|
||||||
const parts = node.text.split(/(\n+)/).filter((part) => part !== '')
|
const parts = node.text.split(/(\n+)/).filter((part) => part !== '')
|
||||||
return success({ segments: parts.map((part) => (part.startsWith('\n') ? carriedText(part) : { escaping, text: part })) })
|
return success({ segments: parts.map((part) => (part.startsWith('\n') ? textDirectiveSegment(part) : { escaping, text: part })) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number, index: number, context: InlineContext): Result<Emission> {
|
function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number, index: number, context: InlineContext): Result<Emission> {
|
||||||
@@ -300,11 +300,11 @@ function emitEmphasis(nodes: readonly AdfNode[], spelling: string, depth: number
|
|||||||
const inner = emitRun(nodes, depth + 1, range.first, context)
|
const inner = emitRun(nodes, depth + 1, range.first, context)
|
||||||
if (!inner.ok) return inner
|
if (!inner.ok) return inner
|
||||||
if (inner.value.carry !== undefined) return inner
|
if (inner.value.carry !== undefined) return inner
|
||||||
const carried = carryStrippedWhitespace(inner.value.segments)
|
const spelled = spellEdgeWhitespace(inner.value.segments)
|
||||||
return success({
|
return success({
|
||||||
segments: [
|
segments: [
|
||||||
{ emphasis: 'open', escaping: 'none', nodes: { ...range, depth }, text: spelling },
|
{ emphasis: 'open', escaping: 'none', nodes: { ...range, depth }, text: spelling },
|
||||||
...carried,
|
...spelled,
|
||||||
{ emphasis: 'close', escaping: 'none', nodes: { ...range, depth }, text: spelling },
|
{ emphasis: 'close', escaping: 'none', nodes: { ...range, depth }, text: spelling },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Flavour } from '../plain-conventions.ts'
|
import type { Flavour } from '../plain/conventions.ts'
|
||||||
import type { LineContainer } from '../line-container.ts'
|
import type { LineContainer } from '../line-container.ts'
|
||||||
import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark/grammar.ts'
|
import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark/grammar.ts'
|
||||||
import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
|
import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
|
||||||
import { claimsDirectivePrefix } from '../directive-syntax.ts'
|
import { claimsDirectivePrefix } from '../directive-syntax.ts'
|
||||||
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
||||||
import { highlightDelimiter, highlightFlanking } from '../plain-conventions.ts'
|
import { highlightDelimiter, highlightFlanking } from '../plain/conventions.ts'
|
||||||
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
|
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
|
||||||
import { opensLinkDefinition } from '../commonmark/link-reference-definitions.ts'
|
import { opensLinkDefinition } from '../commonmark/link-reference-definitions.ts'
|
||||||
import { readEntityReference } from '../commonmark/entity-references.ts'
|
import { readEntityReference } from '../commonmark/entity-references.ts'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { ConvertErrorPath } from '../../result.ts'
|
import type { ConvertErrorPath } from '../../result.ts'
|
||||||
import type { Flavour } from '../plain-conventions.ts'
|
import type { Flavour } from '../plain/conventions.ts'
|
||||||
import { carriesOnly, nodeContent } from '../../adf/document.ts'
|
import { holdsOnlyAttributes, nodeContent } from '../../adf/document.ts'
|
||||||
import { spellPipeDelimiter, spellPipeRow } from '../pipe-table-syntax.ts'
|
import { spellPipeDelimiter, spellPipeRow } from '../pipe-table-syntax.ts'
|
||||||
import { tryPipeCell } from './inline-line.ts'
|
import { tryPipeCell } from './inline-line.ts'
|
||||||
|
|
||||||
@@ -26,16 +26,16 @@ export function tryPipeTable(node: AdfNode, path: ConvertErrorPath, flavour: Fla
|
|||||||
function pipeRows(node: AdfNode): AdfNode[][] | undefined {
|
function pipeRows(node: AdfNode): AdfNode[][] | undefined {
|
||||||
const rows = nodeContent(node)
|
const rows = nodeContent(node)
|
||||||
const columns = rows[0] === undefined ? 0 : nodeContent(rows[0]).length
|
const columns = rows[0] === undefined ? 0 : nodeContent(rows[0]).length
|
||||||
if (!carriesOnly(node, []) || columns === 0) return undefined
|
if (!holdsOnlyAttributes(node, []) || columns === 0) return undefined
|
||||||
const grid: AdfNode[][] = []
|
const grid: AdfNode[][] = []
|
||||||
for (const [index, row] of rows.entries()) {
|
for (const [index, row] of rows.entries()) {
|
||||||
const cells = nodeContent(row)
|
const cells = nodeContent(row)
|
||||||
if (row.type !== 'tableRow' || !carriesOnly(row, []) || cells.length !== columns) return undefined
|
if (row.type !== 'tableRow' || !holdsOnlyAttributes(row, []) || cells.length !== columns) return undefined
|
||||||
const wanted = index === 0 ? 'tableHeader' : 'tableCell'
|
const wanted = index === 0 ? 'tableHeader' : 'tableCell'
|
||||||
const paragraphs: AdfNode[] = []
|
const paragraphs: AdfNode[] = []
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
const paragraph = plainParagraph(cell)
|
const paragraph = plainParagraph(cell)
|
||||||
if (paragraph === undefined || cell.type !== wanted || !carriesOnly(cell, [])) return undefined
|
if (paragraph === undefined || cell.type !== wanted || !holdsOnlyAttributes(cell, [])) return undefined
|
||||||
paragraphs.push(paragraph)
|
paragraphs.push(paragraph)
|
||||||
}
|
}
|
||||||
grid.push(paragraphs)
|
grid.push(paragraphs)
|
||||||
@@ -46,6 +46,6 @@ function pipeRows(node: AdfNode): AdfNode[][] | undefined {
|
|||||||
function plainParagraph(cell: AdfNode): AdfNode | undefined {
|
function plainParagraph(cell: AdfNode): AdfNode | undefined {
|
||||||
const content = nodeContent(cell)
|
const content = nodeContent(cell)
|
||||||
const paragraph = content[0]
|
const paragraph = content[0]
|
||||||
if (paragraph === undefined || content.length !== 1 || paragraph.type !== 'paragraph' || !carriesOnly(paragraph, [])) return undefined
|
if (paragraph === undefined || content.length !== 1 || paragraph.type !== 'paragraph' || !holdsOnlyAttributes(paragraph, [])) return undefined
|
||||||
return paragraph
|
return paragraph
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ export function readEmptyKeys(type: string, attributes: DirectiveAttributes, key
|
|||||||
empty.add(key)
|
empty.add(key)
|
||||||
rest.delete(key)
|
rest.delete(key)
|
||||||
}
|
}
|
||||||
if (empty.has('attrs') && (held || [...rest.keys()].some((key) => key !== 'marks'))) return { fault: unsupportedNodeShape(`${type} spells attrs=empty beside another attribute, an argument or content: drop attrs=empty or the rest`) }
|
if (empty.has('attrs') && (held || [...rest.keys()].some((key) => key !== 'marks'))) return { fault: unsupportedNodeShape(`${type} spells attrs=empty beside another attribute, an argument or a [content] slot: drop attrs=empty, or the rest`) }
|
||||||
return { value: { empty, rest } }
|
return { value: { empty, rest } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
|||||||
import { readEmptyKeys, spellsEmpty } from '../empty-keys.ts'
|
import { readEmptyKeys, spellsEmpty } from '../empty-keys.ts'
|
||||||
import { readVocabulary } from './directive-attributes.ts'
|
import { readVocabulary } from './directive-attributes.ts'
|
||||||
import { slotLineEndingFault } from '../directive-syntax.ts'
|
import { slotLineEndingFault } from '../directive-syntax.ts'
|
||||||
import { textBreakName } from '../text-break.ts'
|
import { textBreakName } from '../adjacent-text.ts'
|
||||||
import { textDirectiveName } from '../text-directive.ts'
|
import { textDirectiveName } from '../text-directive.ts'
|
||||||
|
|
||||||
export type BlockDirectiveNode = { contentModel: BlockNodeModel['contentModel']; node: AdfNode }
|
export type BlockDirectiveNode = { contentModel: BlockNodeModel['contentModel']; node: AdfNode }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { DirectiveSpan, NestedSpans } from '../directive-syntax.ts'
|
import type { DirectiveSpan, NestedSpans } from '../directive-syntax.ts'
|
||||||
import type { EmphasisPairing } from '../commonmark/emphasis-matching.ts'
|
import type { EmphasisPairing } from '../commonmark/emphasis-matching.ts'
|
||||||
import type { Flavour } from '../plain-conventions.ts'
|
import type { Flavour } from '../plain/conventions.ts'
|
||||||
import type { LineContainer } from '../line-container.ts'
|
import type { LineContainer } from '../line-container.ts'
|
||||||
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
||||||
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark/grammar.ts'
|
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||||
@@ -9,9 +9,10 @@ import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
|
|||||||
import { commonMarkLink, linkHref } from '../mark-spellings.ts'
|
import { commonMarkLink, linkHref } from '../mark-spellings.ts'
|
||||||
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { highlightDelimiter, highlightFlanking } from '../plain-conventions.ts'
|
import { highlightDelimiter, highlightFlanking } from '../plain/conventions.ts'
|
||||||
import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
|
import { joinsWhenRead, textBreakName, textBreakSpelling } from '../adjacent-text.ts'
|
||||||
import { noSpans, readInlineDirective } from '../directive-syntax.ts'
|
import { noSpans, readInlineDirective } from '../directive-syntax.ts'
|
||||||
import { normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts'
|
import { normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts'
|
||||||
import { openingLinkTakesDirective } from '../emit/inline-line.ts'
|
import { openingLinkTakesDirective } from '../emit/inline-line.ts'
|
||||||
@@ -19,7 +20,6 @@ import { readCarriedInline } from '../opaque-carry.ts'
|
|||||||
import { readDirectiveMark } from './directive-marks.ts'
|
import { readDirectiveMark } from './directive-marks.ts'
|
||||||
import { readInlineDirectiveNode } from './directive-nodes.ts'
|
import { readInlineDirectiveNode } from './directive-nodes.ts'
|
||||||
import { readTextDirective } from '../text-directive.ts'
|
import { readTextDirective } from '../text-directive.ts'
|
||||||
import { readsAsOne, textBreakName, textBreakSpelling } from '../text-break.ts'
|
|
||||||
|
|
||||||
export type InlineContent = { carry?: undefined; image: AdfNode; nodes?: undefined } | { carry: boolean; image?: undefined; nodes: AdfNode[] }
|
export type InlineContent = { carry?: undefined; image: AdfNode; nodes?: undefined } | { carry: boolean; image?: undefined; nodes: AdfNode[] }
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ function partText(items: readonly Inline[], scan: Scan): Result<AdfNode[]> {
|
|||||||
}
|
}
|
||||||
const previous = items[index - 1]
|
const previous = items[index - 1]
|
||||||
const next = items[index + 1]
|
const next = items[index + 1]
|
||||||
if (previous === undefined || next === undefined || !isNode(previous) || !isNode(next) || !readsAsOne(previous, next)) {
|
if (previous === undefined || next === undefined || !isNode(previous) || !isNode(next) || !joinsWhenRead(previous, next)) {
|
||||||
return failure('unsupported-node-shape', `delete ${textBreakSpelling} here: it stands only between two runs of text with the same formatting, which would otherwise read as one`, scan.path)
|
return failure('unsupported-node-shape', `delete ${textBreakSpelling} here: it stands only between two runs of text with the same formatting, which would otherwise read as one`, scan.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,11 +508,11 @@ function resolveNodes(pieces: readonly Piece[], scan: Scan, highlights: boolean)
|
|||||||
writeUnpaired(nodes, runs, pairings)
|
writeUnpaired(nodes, runs, pairings)
|
||||||
if (!markPairings(pieces, nodes, pairings)) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
if (!markPairings(pieces, nodes, pairings)) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
||||||
markHighlights(pieces, nodes, highlights ? pairedHighlights(pieces, nodes) : [])
|
markHighlights(pieces, nodes, highlights ? pairedHighlights(pieces, nodes) : [])
|
||||||
return success(mergeText(nodes.flat()))
|
return success(mergeReadText(nodes.flat()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A text break and a carried node are walls: the text on either side joins only its own side.
|
// A text break and a carried node are walls: the text on either side joins only its own side.
|
||||||
function mergeText(items: readonly Inline[]): Inline[] {
|
function mergeReadText(items: readonly Inline[]): Inline[] {
|
||||||
const merged: Inline[] = []
|
const merged: Inline[] = []
|
||||||
let run: AdfNode[] = []
|
let run: AdfNode[] = []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
@@ -520,11 +520,11 @@ function mergeText(items: readonly Inline[]): Inline[] {
|
|||||||
run.push(item)
|
run.push(item)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (const node of mergeAdjacentText(run, readsAsOne)) merged.push(node)
|
for (const node of mergeAdjacentText(run, joinsWhenRead)) merged.push(node)
|
||||||
merged.push(item)
|
merged.push(item)
|
||||||
run = []
|
run = []
|
||||||
}
|
}
|
||||||
for (const node of mergeAdjacentText(run, readsAsOne)) merged.push(node)
|
for (const node of mergeAdjacentText(run, joinsWhenRead)) merged.push(node)
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,11 @@ test('reads the codeBlock directive body as the node content, the info string it
|
|||||||
|
|
||||||
test('names the slot a codeBlock spells its language outside of', () => {
|
test('names the slot a codeBlock spells its language outside of', () => {
|
||||||
const slot = 'unsupported-node-shape: codeBlock spells its language in the fence info string, or in the attribute where no info string carries it back'
|
const slot = 'unsupported-node-shape: codeBlock spells its language in the fence info string, or in the attribute where no info string carries it back'
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n')), "unsupported-node-shape: move language=rust to the fence's info string: a fence carries the language wherever its info string can")
|
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n')), "unsupported-node-shape: move language=rust to the fences' info strings: they can spell this language")
|
||||||
|
assert.equal(
|
||||||
|
content(markdownToAdf('!adf:codeBlock {language="c sharp"}\n```\nx\n```\n```\ny\n```\n!adf:/codeBlock\n')),
|
||||||
|
"unsupported-node-shape: move language=\"c sharp\" to the fences' info strings: they can spell this language",
|
||||||
|
)
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust}\n```sql\nx\n```\n!adf:/codeBlock\n')), slot)
|
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust}\n```sql\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```adf:x\nx\n```\n!adf:/codeBlock\n')), slot)
|
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```adf:x\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```a\\b\nx\n```\n!adf:/codeBlock\n')), slot)
|
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```a\\b\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||||
@@ -1116,6 +1120,6 @@ test('leads a refusal ordinary editing meets with the edit that fixes it', () =>
|
|||||||
)
|
)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
content(markdownToAdf('!adf:panel info {attrs=empty}\nText.\n!adf:/panel\n')),
|
content(markdownToAdf('!adf:panel info {attrs=empty}\nText.\n!adf:/panel\n')),
|
||||||
'unsupported-node-shape: panel spells attrs=empty beside another attribute, an argument or content: drop attrs=empty or the rest',
|
'unsupported-node-shape: panel spells attrs=empty beside another attribute, an argument or a [content] slot: drop attrs=empty, or the rest',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
|||||||
import type { Block, DirectiveBlock } from './blocks.ts'
|
import type { Block, DirectiveBlock } from './blocks.ts'
|
||||||
import type { BlockDirectiveNode } from './directive-nodes.ts'
|
import type { BlockDirectiveNode } from './directive-nodes.ts'
|
||||||
import type { ConvertFault } from '../../result.ts'
|
import type { ConvertFault } from '../../result.ts'
|
||||||
import type { Flavour } from '../plain-conventions.ts'
|
import type { Flavour } from '../plain/conventions.ts'
|
||||||
import type { LineContainer } from '../line-container.ts'
|
import type { LineContainer } from '../line-container.ts'
|
||||||
import type { LinkDefinitions } from './inline-content.ts'
|
import type { LinkDefinitions } from './inline-content.ts'
|
||||||
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
|
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
|
||||||
@@ -10,16 +10,16 @@ import { commonMarkSpelling, type SpellingMemo } from '../emit/adf-to-markdown.t
|
|||||||
import { documentAttribute, documentName, documentSpelling, listBreakName, listBreakSpelling } from '../block-directive.ts'
|
import { documentAttribute, documentName, documentSpelling, listBreakName, listBreakSpelling } from '../block-directive.ts'
|
||||||
import { emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
import { emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
|
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
|
||||||
import { inlineLeaves } from '../emit/plain-inline.ts'
|
import { inlineLeaves } from '../plain/inline-reduction.ts'
|
||||||
import { languageSlot } from '../code-language.ts'
|
import { languageSlot } from '../code-language.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { leadingMarker, readAlertMarker, readTaskMarker } from '../plain-conventions.ts'
|
import { leadingMarker, readAlertMarker, readTaskMarker } from '../plain/conventions.ts'
|
||||||
import { mintTaskIds } from './task-ids.ts'
|
import { mintTaskIds } from '../plain/task-ids.ts'
|
||||||
import { parseBlocks } from './blocks.ts'
|
import { parseBlocks } from './blocks.ts'
|
||||||
import { parseInlineContent } from './inline-content.ts'
|
import { parseInlineContent } from './inline-content.ts'
|
||||||
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
||||||
|
import { spellStringAttribute, unsupportedNodeShape } from '../directive-syntax.ts'
|
||||||
import { spellsEmpty } from '../empty-keys.ts'
|
import { spellsEmpty } from '../empty-keys.ts'
|
||||||
import { unsupportedNodeShape } from '../directive-syntax.ts'
|
|
||||||
|
|
||||||
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: Conver
|
|||||||
const fromFence = first.language !== ''
|
const fromFence = first.language !== ''
|
||||||
const slot = languageSlot(fromFence ? first.language : attribute)
|
const slot = languageSlot(fromFence ? first.language : attribute)
|
||||||
if (!fromFence && slot.kind === 'fence') {
|
if (!fromFence && slot.kind === 'fence') {
|
||||||
return failure('unsupported-node-shape', `move language=${slot.info} to the fence's info string: a fence carries the language wherever its info string can`, path)
|
return failure('unsupported-node-shape', `move language=${spellStringAttribute(slot.info)} to the fences' info strings: they can spell this language`, path)
|
||||||
}
|
}
|
||||||
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
||||||
return failure('unsupported-node-shape', `${node.type} spells its language in the fence info string, or in the attribute where no info string carries it back`, path)
|
return failure('unsupported-node-shape', `${node.type} spells its language in the fence info string, or in the attribute where no info string carries it back`, path)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import test from 'node:test'
|
|||||||
|
|
||||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import { adfToMarkdown } from '../emit/adf-to-markdown.ts'
|
import { adfToMarkdown } from '../emit/adf-to-markdown.ts'
|
||||||
import { adfToPlainMarkdown } from '../emit/plain-reduction.ts'
|
import { adfToPlainMarkdown } from '../plain/adf-to-plain-markdown.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { markdownToAdf, plainMarkdownToAdf } from './markdown-to-adf.ts'
|
import { markdownToAdf, plainMarkdownToAdf } from './markdown-to-adf.ts'
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import { adfToPlainMarkdown, reduceToPlain } from './plain-reduction.ts'
|
import { adfToPlainMarkdown, reduceToPlain } from './adf-to-plain-markdown.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
|
|
||||||
const code: AdfMark = { type: 'code' }
|
const code: AdfMark = { type: 'code' }
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||||
import { adfDocumentFault, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
import { adfDocumentFault, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
import { commonMarkSpelling, largestListMarker, writeMarkdown, type SpellingMemo } from './adf-to-markdown.ts'
|
import { commonMarkSpelling, largestListMarker, writeMarkdown, type SpellingMemo } from '../emit/adf-to-markdown.ts'
|
||||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { inlineLeaves, isBlockNodeType, oneLine, reduceInline, writableHref } from './plain-inline.ts'
|
import { inlineLeaves, isBlockNodeType, oneLine, reduceInline, writableHref } from './inline-reduction.ts'
|
||||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import { languageSlot } from '../code-language.ts'
|
import { languageSlot } from '../code-language.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { taskMarker } from '../plain-conventions.ts'
|
import { taskMarker } from './conventions.ts'
|
||||||
import { toEditorNormal } from '../../adf/editor-normal.ts'
|
import { toEditorNormal } from './editor-normal.ts'
|
||||||
|
|
||||||
// depth: the level the node reduced stands at, counted as the emitter counts it.
|
// depth: the level the node reduced stands at, counted as the emitter counts it.
|
||||||
type Reduction = { depth: number; memo: SpellingMemo; path: ConvertErrorPath }
|
type Reduction = { depth: number; memo: SpellingMemo; path: ConvertErrorPath }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isWordCharacter } from './commonmark/emphasis-matching.ts'
|
import { isWordCharacter } from '../commonmark/emphasis-matching.ts'
|
||||||
|
|
||||||
export type Flavour = 'lossless' | 'plain'
|
export type Flavour = 'lossless' | 'plain'
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
import type { AdfNode } from './document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { JsonValue } from '../json-value.ts'
|
import type { JsonValue } from '../../json-value.ts'
|
||||||
import { toEditorNormal } from './editor-normal.ts'
|
import { toEditorNormal } from './editor-normal.ts'
|
||||||
|
|
||||||
test('merges adjacent text nodes carrying identical marks, at every level', () => {
|
test('merges adjacent text nodes carrying identical marks, at every level', () => {
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||||
|
import { joinsWhenEditorNormal, normalAttributes, normalMark } from '../adjacent-text.ts'
|
||||||
|
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
|
|
||||||
|
type NodeHolder = { content?: AdfNode[] }
|
||||||
|
|
||||||
|
export function toEditorNormal(document: AdfDocument): AdfDocument {
|
||||||
|
const normal: AdfDocument = { type: document.type, version: Object.is(document.version, -0) ? 0 : document.version }
|
||||||
|
const pending: { holder: NodeHolder; source: NodeHolder }[] = [{ holder: normal, source: document }]
|
||||||
|
for (let entry = pending.pop(); entry !== undefined; entry = pending.pop()) {
|
||||||
|
const content = mergeAdjacentText(nodeContent(entry.source), joinsWhenEditorNormal)
|
||||||
|
if (content.length === 0) continue
|
||||||
|
entry.holder.content = content.map((source) => {
|
||||||
|
const holder = normalNode(source)
|
||||||
|
pending.push({ holder, source })
|
||||||
|
return holder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return normal
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalNode(node: AdfNode): AdfNode {
|
||||||
|
const normal: AdfNode = { type: node.type }
|
||||||
|
const attrs = normalAttributes(nodeAttrs(node))
|
||||||
|
if (attrs !== undefined) normal.attrs = attrs
|
||||||
|
const marks = nodeMarks(node).map(normalMark)
|
||||||
|
if (marks.length > 0) normal.marks = marks
|
||||||
|
if (node.text !== undefined) normal.text = node.text
|
||||||
|
return normal
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { LineContainer } from '../line-container.ts'
|
import type { LineContainer } from '../line-container.ts'
|
||||||
import type { MarkRun } from './line-escaping.ts'
|
import type { MarkRun } from '../emit/line-escaping.ts'
|
||||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { joinsNormally, sameMark } from '../../adf/editor-normal.ts'
|
import { joinsWhenEditorNormal, sameMarkWhenEditorNormal } from '../adjacent-text.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { plainLineFallback, type PlainLineFallback } from './inline-line.ts'
|
import { plainLineFallback, type PlainLineFallback } from '../emit/inline-line.ts'
|
||||||
import { spellDestination, spellLinkTarget } from '../commonmark/link-syntax.ts'
|
import { spellDestination, spellLinkTarget } from '../commonmark/link-syntax.ts'
|
||||||
|
|
||||||
const highlight = 'backgroundColor'
|
const highlight = 'backgroundColor'
|
||||||
@@ -175,11 +175,11 @@ function highlighted(leaves: readonly AdfNode[]): AdfNode[] {
|
|||||||
const marks = nodeMarks(leaf)
|
const marks = nodeMarks(leaf)
|
||||||
if (marks[0]?.type === highlight) {
|
if (marks[0]?.type === highlight) {
|
||||||
const held = marks.slice(1)
|
const held = marks.slice(1)
|
||||||
shared = run.length === 0 ? held : shared.filter((mark) => held.some((other) => sameMark(other, mark)))
|
shared = run.length === 0 ? held : shared.filter((mark) => held.some((other) => sameMarkWhenEditorNormal(other, mark)))
|
||||||
run.push(leaf)
|
run.push(leaf)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (const held of run) spelled.push(withMarks(held, [...shared, highlightMark, ...nodeMarks(held).slice(1).filter((mark) => !shared.some((other) => sameMark(other, mark)))]))
|
for (const held of run) spelled.push(withMarks(held, [...shared, highlightMark, ...nodeMarks(held).slice(1).filter((mark) => !shared.some((other) => sameMarkWhenEditorNormal(other, mark)))]))
|
||||||
run = []
|
run = []
|
||||||
spelled.push(leaf)
|
spelled.push(leaf)
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ function withMarks(leaf: AdfNode, marks: readonly AdfMark[]): AdfNode {
|
|||||||
|
|
||||||
function trimmedEdges(leaves: readonly AdfNode[]): AdfNode[] {
|
function trimmedEdges(leaves: readonly AdfNode[]): AdfNode[] {
|
||||||
for (let current = leaves; ; ) {
|
for (let current = leaves; ; ) {
|
||||||
const merged = withoutEdgeBreaks(mergeAdjacentText(current, joinsNormally))
|
const merged = withoutEdgeBreaks(mergeAdjacentText(current, joinsWhenEditorNormal))
|
||||||
let changed = false
|
let changed = false
|
||||||
const trimmed: AdfNode[] = []
|
const trimmed: AdfNode[] = []
|
||||||
for (const [index, leaf] of merged.entries()) {
|
for (const [index, leaf] of merged.entries()) {
|
||||||
@@ -251,7 +251,7 @@ function edgeDepth(marks: readonly AdfMark[], neighbour: AdfNode | undefined, wh
|
|||||||
function sameMarkAt(marks: readonly AdfMark[], others: readonly AdfMark[], index: number): boolean {
|
function sameMarkAt(marks: readonly AdfMark[], others: readonly AdfMark[], index: number): boolean {
|
||||||
const mark = marks[index]
|
const mark = marks[index]
|
||||||
const other = others[index]
|
const other = others[index]
|
||||||
return mark !== undefined && other !== undefined && sameMark(mark, other)
|
return mark !== undefined && other !== undefined && sameMarkWhenEditorNormal(mark, other)
|
||||||
}
|
}
|
||||||
|
|
||||||
function spellableLine(leaves: AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<AdfNode[]> {
|
function spellableLine(leaves: AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<AdfNode[]> {
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { AdfNode } from '../adf/document.ts'
|
|
||||||
import { identicalMarks, isPlainText, nodeMarks } from '../adf/document.ts'
|
|
||||||
import { spellInlineLeafDirective } from './directive-syntax.ts'
|
|
||||||
|
|
||||||
export const textBreakName = 'textBreak'
|
|
||||||
|
|
||||||
export const textBreakSpelling = spellInlineLeafDirective(textBreakName, '')
|
|
||||||
|
|
||||||
// Whether CommonMark reads the pair back as one text node, where neither rides the carry (spec/flavour.md, Inline nodes).
|
|
||||||
export function readsAsOne(previous: AdfNode, node: AdfNode): boolean {
|
|
||||||
return isPlainText(previous) && isPlainText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node))
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
`Bar = 9`
|
`Bar = 9`
|
||||||
|
|
||||||
`Next ID = 59`
|
`Next ID = 63`
|
||||||
|
|
||||||
| Goal | W |
|
| Goal | W |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -30,11 +30,14 @@
|
|||||||
| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 |
|
| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 |
|
||||||
| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 |
|
| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 |
|
||||||
| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists in the CommonMark reader.** | 4 | 5 | 5 | 8 | 3, 4 | 17.2 |
|
| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists in the CommonMark reader.** | 4 | 5 | 5 | 8 | 3, 4 | 17.2 |
|
||||||
|
| 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 |
|
||||||
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
|
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
|
||||||
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 |
|
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 |
|
||||||
| 54 | 0.2.0 | | **Make `docs/decisions.md` §The source parts by ADF and format name every import `parse/` takes from `emit/`.** | 2 | 2 | 2 | 5 | 1 | 11.5 |
|
|
||||||
| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 |
|
| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 |
|
||||||
|
| 60 | 0.2.0 | decision | **Give the parser's questions to the emitter one named module, so the parse→emit dependency reads as intended.** | 3 | 4 | 2 | 6 | 2 | 10.7 |
|
||||||
|
| 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, and move `Read<T>` to `result.ts` and `Flavour` out of the plain flavour.** | 3 | 5 | 2 | 6 | 2 | 10.4 |
|
||||||
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
|
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
|
||||||
|
| 62 | 0.2.0 | decision | **Move the plain reader out of `parse/markdown-to-adf.ts` into `markdown/plain/`.** | 3 | 4 | 2 | 5 | 2 | 8.9 |
|
||||||
| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 |
|
| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 |
|
||||||
| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 |
|
| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 |
|
||||||
| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 3, 4 | 12.6 |
|
| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 3, 4 | 12.6 |
|
||||||
@@ -53,9 +56,9 @@
|
|||||||
|
|
||||||
### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.
|
### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.
|
||||||
|
|
||||||
Lands after item 6. The CommonMark spec suite also runs against `markdownToHtml`. The README
|
Lands after items 6, 59, 60, 61 and 62. The CommonMark spec suite also runs against
|
||||||
documents HTML as it documents markdown, and its tagline and `package.json`'s `description` regain
|
`markdownToHtml`. The README documents HTML as it documents markdown, and its tagline and
|
||||||
HTML.
|
`package.json`'s `description` regain HTML.
|
||||||
|
|
||||||
### 55. Spell through the carry every document the emitter refuses today for a carriage return, a NUL, a code-span line start or a newline inside an emoji, mention or status.
|
### 55. Spell through the carry every document the emitter refuses today for a carriage return, a NUL, a code-span line start or a newline inside an emoji, mention or status.
|
||||||
|
|
||||||
@@ -99,6 +102,12 @@ Readings table gains its row, and its Spellings table one if `!adf:listBreak` re
|
|||||||
and 302 lose their `pending` exceptions, and the spelling leaves the README's "Four CommonMark
|
and 302 lose their `pending` exceptions, and the spelling leaves the README's "Four CommonMark
|
||||||
spellings" bullet, which counts one fewer.
|
spellings" bullet, which counts one fewer.
|
||||||
|
|
||||||
|
### 61. Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.
|
||||||
|
|
||||||
|
The comprehension panel's worst place: `escapeClaims` and `escapeClosedRuns` re-implement the
|
||||||
|
reader's view — flanking, code-span closers, link-definition openings, highlight flanking — and
|
||||||
|
only the property tests catch drift. It caps Locality.
|
||||||
|
|
||||||
### 52. Spell `colwidth` as a comma list, `colwidth="340,420"`.
|
### 52. Spell `colwidth` as a comma list, `colwidth="340,420"`.
|
||||||
|
|
||||||
A writer panel chose it on 2026-10-03, 5 of 7, over today's `colwidth="[340,420]"`. Breaking:
|
A writer panel chose it on 2026-10-03, 5 of 7, over today's `colwidth="[340,420]"`. Breaking:
|
||||||
@@ -111,18 +120,23 @@ example 540); lowercasing and then uppercasing folds it. Breaking, so it ships b
|
|||||||
`MIGRATION.md`'s Readings table gains its row. Its `pending` exceptions go, and its spelling leaves
|
`MIGRATION.md`'s Readings table gains its row. Its `pending` exceptions go, and its spelling leaves
|
||||||
the README's "Four CommonMark spellings" bullet, which counts one fewer.
|
the README's "Four CommonMark spellings" bullet, which counts one fewer.
|
||||||
|
|
||||||
### 54. Make `docs/decisions.md` §The source parts by ADF and format name every import `parse/` takes from `emit/`.
|
|
||||||
|
|
||||||
The entry says `parse/` imports only `commonMarkSpelling` and `openingLinkTakesDirective` from
|
|
||||||
`emit/`; `parse/markdown-to-adf.ts` also imports `inlineLeaves` from `emit/plain-inline.ts`. Either
|
|
||||||
the entry names it with its reason, or the reader stops importing it.
|
|
||||||
|
|
||||||
### 53. Put a block's `marks` spelling to the writer panel and adopt its pick.
|
### 53. Put a block's `marks` spelling to the writer panel and adopt its pick.
|
||||||
|
|
||||||
Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as
|
Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as
|
||||||
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
|
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
|
||||||
gains its row.
|
gains its row.
|
||||||
|
|
||||||
|
### 60. Give the parser's questions to the emitter one named module, so the parse→emit dependency reads as intended.
|
||||||
|
|
||||||
|
`parse/` asks `emit/` through `commonMarkSpelling` and `openingLinkTakesDirective`, each imported
|
||||||
|
from where it happens to live. One module naming the questions keeps `docs/decisions.md` §The source
|
||||||
|
parts by ADF and format true as they grow.
|
||||||
|
|
||||||
|
### 59. Group the directive grammar into `src/markdown/directive/`, and move `Read<T>` to `result.ts` and `Flavour` out of the plain flavour.
|
||||||
|
|
||||||
|
Eight directive files sit across three directories, and the `markdown/` root holds 14 entries. HTML
|
||||||
|
needs `Read<T>`, `Flavour`, the carry and the mark spellings out of `markdown/`.
|
||||||
|
|
||||||
### 50. Read `[](/url)` and `[]()` as CommonMark's empty link.
|
### 50. Read `[](/url)` and `[]()` as CommonMark's empty link.
|
||||||
|
|
||||||
Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a
|
Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a
|
||||||
@@ -132,6 +146,10 @@ input. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gai
|
|||||||
`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet,
|
`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet,
|
||||||
which counts one fewer.
|
which counts one fewer.
|
||||||
|
|
||||||
|
### 62. Move the plain reader out of `parse/markdown-to-adf.ts` into `markdown/plain/`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode.
|
### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode.
|
||||||
|
|
||||||
`adfToMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values
|
`adfToMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values
|
||||||
@@ -154,7 +172,7 @@ punctuation — and, where they do, read the code point, with a fixture per dire
|
|||||||
|
|
||||||
### 42. Trim a text leaf's trailing blanks in linear time.
|
### 42. Trim a text leaf's trailing blanks in linear time.
|
||||||
|
|
||||||
`plain-inline.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic in a run
|
`plain/inline-reduction.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic in a run
|
||||||
of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in
|
of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in
|
||||||
`adfToPlainMarkdown`. Scan backward, as the expand title's trim does.
|
`adfToPlainMarkdown`. Scan backward, as the expand title's trim does.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user