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:
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
export type ConvertErrorCode =
|
||||
| 'malformed-directive'
|
||||
| 'malformed-pipe-table'
|
||||
| 'not-a-string'
|
||||
| 'not-an-adf-document'
|
||||
| 'unknown-directive-name'
|
||||
| 'unmappable-html'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user