3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 19:58:28 +01:00

Refuse a markdown input that is not a string with not-a-string

This commit is contained in:
2026-10-05 09:19:19 +02:00
committed by Lillem4n
parent 5834ac0c7d
commit 2c4fadbd8b
9 changed files with 29 additions and 17 deletions
+4
View File
@@ -2,6 +2,10 @@
## Unreleased
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` refuse a value that is not a string with
the new code `not-a-string`, at position `{ line: 1, offset: 0 }`. Before, `null` and `undefined`
threw a `TypeError` and a number read as an empty document. A `switch` over `ConvertErrorCode`
naming every code needs the new case.
- **Breaking:** an attribute value that is not a plain object, array or JSON primitive, such as a
`Date`, `Map`, typed array or class instance, makes `isAdfDocument` return `false` and the
converters refuse it as `not-an-adf-document`. Before, the converters wrote it as `{}` or its own
+2
View File
@@ -65,6 +65,8 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc
`unspellable-character`, `unspellable-line-start` and `unspellable-link` leave `ConvertErrorCode`:
a `switch` naming one stops compiling. A document `adfToMarkdown` refused with one of them now
converts; markdown the parser refused with one of them now refuses with the code the table gives.
`not-a-string` joins it: a `switch` naming every code needs its case. The markdown readers return
it for a value that is not a string, which threw or read as an empty document before.
| Input | `0.1.0` | `0.2.0` |
| --- | --- | --- |
+3 -1
View File
@@ -178,7 +178,8 @@ the identical error: fix the input, or set the document aside.
`position` is `{ line, offset }` into the string passed in: `line` counted from 1, `offset` a
UTF-16 code unit, a JavaScript string index rather than a codepoint or a byte offset. It points at
or before the refusal — currently the start of the line the enclosing block begins on; a later
minor may narrow that, never widen it.
minor may narrow that, never widen it. `not-a-string`, having no string to point into, names
`{ line: 1, offset: 0 }`.
Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`:
@@ -186,6 +187,7 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`
| --- | --- | --- |
| `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or, for a code fence whose info string opens `adf:`, drop the info string and wrap the fence in `!adf:codeBlock {language="adf:…"}` |
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
| `not-a-string` | the value handed in is not a string: `null`, `undefined`, a number, an object | pass the markdown as a string; the message names what was passed |
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
| `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour |
| `unmappable-image` | an image sits inside other content that is not another image's description, or carries a title | give the image a paragraph of its own and drop the title |
+3
View File
@@ -383,6 +383,9 @@ handles one cause alike whichever node, attribute or direction raised it.
knows which direction it called and parting them after `0.1.0` is MAJOR (2026-09-23).
- A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document`
emitting — no document holds one, so no round-trip crosses them (2026-09-23).
- A value handed to a markdown reader that is not a string is `not-a-string`, its position
`{ line: 1, offset: 0 }` and its path empty: no code read true of it, and `not-an-adf-document`
names the other direction's input (2026-10-05).
## `message` and `path`
+1 -1
View File
@@ -208,7 +208,7 @@ function notADocument(message: string): ConvertFault {
return { code: 'not-an-adf-document', message }
}
function describe(value: unknown): string {
export function describe(value: unknown): string {
if (typeof value === 'string') return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value)
if (typeof value === 'function') return 'a function'
if (typeof value === 'object' && value !== null) return Array.isArray(value) ? 'an array' : 'an object'
+13 -1
View File
@@ -4,7 +4,7 @@ import test from 'node:test'
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
import type { ParseError, Result, SourcePosition } from '../../result.ts'
import { largestNesting } from '../../nesting.ts'
import { markdownToAdf } from './markdown-to-adf.ts'
import { markdownToAdf, plainMarkdownToAdf } from './markdown-to-adf.ts'
const em: AdfMark = { type: 'em' }
const strike: AdfMark = { type: 'strike' }
@@ -84,6 +84,18 @@ function table(...rows: AdfNode[]): AdfNode {
return { content: rows, type: 'table' }
}
test('refuses a value that is not a string, in both readers, at the start of the input', () => {
const found: [unknown, string][] = [[null, 'null'], [undefined, 'undefined'], [42, '42'], [{}, 'an object'], [['x'], 'an array']]
for (const read of [markdownToAdf, plainMarkdownToAdf]) {
for (const [value, described] of found) {
assert.deepEqual(Reflect.apply(read, undefined, [value]), {
error: { code: 'not-a-string', message: `markdown is a string: found ${described}`, path: [], position: { line: 1, offset: 0 } },
ok: false,
})
}
}
})
test('builds an empty document from input holding no block, and one holding no content key from the doc directive alone', () => {
assert.deepEqual(markdownToAdf(''), { ok: true, value: { content: [], type: 'doc', version: 1 } })
assert.deepEqual(markdownToAdf('\n \n\t\n'), { ok: true, value: { content: [], type: 'doc', version: 1 } })
+2 -1
View File
@@ -8,7 +8,7 @@ import type { LinkDefinitions } from './inline-content.ts'
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
import { commonMarkSpelling, type SpellingMemo } from '../emit/adf-to-markdown.ts'
import { documentAttribute, documentName, documentSpelling, listBreakName, listBreakSpelling } from '../block-directive.ts'
import { emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
import { describe, emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
import { inlineLeaves } from '../plain/inline-reduction.ts'
import { languageSlot } from '../code-language.ts'
@@ -39,6 +39,7 @@ export function plainMarkdownToAdf(markdown: string): Result<AdfDocument, ParseE
}
function readDocument(markdown: string, flavour: Flavour): Result<AdfDocument, ParseError> {
if (typeof markdown !== 'string') return positioned(failure('not-a-string', `markdown is a string: found ${describe(markdown)}`, []), documentStart)
const parsed = parseBlocks(markdown)
const [only] = parsed.blocks
if (parsed.blocks.length === 1 && only?.kind === 'directive' && only.name === documentName) {
+1
View File
@@ -1,6 +1,7 @@
export type ConvertErrorCode =
| 'malformed-directive'
| 'malformed-pipe-table'
| 'not-a-string'
| 'not-an-adf-document'
| 'unknown-directive-name'
| 'unmappable-html'
-13
View File
@@ -25,7 +25,6 @@
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|---|---|---|---|---|---|---|---|---|---|
| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 |
| 78 | 0.2.0 | defect | **Return a result when `markdownToAdf` or `plainMarkdownToAdf` is handed a value that is not a string.** | 2 | 2 | 5 | 9 | 1 | 25.5 |
| 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 3 | 6 | 8 | 1 | 25.2 |
| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 |
| 64 | 0.2.0 | defect | **Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 8 | 23.0 |
@@ -68,18 +67,6 @@
Lands after items 6, 59, 60, 61 and 62. The CommonMark spec suite also runs against
`markdownToHtml`. The README documents HTML as it documents markdown.
### 78. Return a result when `markdownToAdf` or `plainMarkdownToAdf` is handed a value that is not a string.
Today `null` and `undefined` throw a `TypeError` reading `length`, and `42` reads as an empty
document. A JavaScript caller, or a TypeScript one past an untyped boundary, can pass any of these.
No existing code reads true of a non-string input (`docs/decisions.md` §The code list). The
maintainer chose a new one on 2026-10-05: `not-a-string`, with position `{ line: 1, offset: 0 }`
and an empty path. The chunk adds it to `ConvertErrorCode` and the README's parsing table, rewords
the README's `position` paragraph, which assumes a string was passed in, and records under
`docs/decisions.md` §Which code a cause takes that a non-string input takes `not-a-string`.
Breaking (§The code list): the changelog entry says so. Found while checking the other ADF
converters for item 71.
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.
Lands with item 77: the reader returns item 77's copy. Goal 1 has every call return a result; the