Refuse the pipe form where a link hides a pipe, and fall back to the directive form for every image CommonMark cannot spell
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit is contained in:
@@ -334,10 +334,10 @@ test('spells the image form for exactly the centered external media shape', () =
|
||||
assert.equal(markdown(adfToMarkdown(single({ alt: 'The moon', type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToMarkdown(single({ type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToMarkdown(single({ alt: 'a [b] c', type: 'external', url }))), `![a \\[b\\] c](${url})\n`)
|
||||
assert.equal(code(adfToMarkdown(single({ alt: '', type: 'external', url }))), 'ambiguous-attribute-spelling')
|
||||
assert.equal(code(adfToMarkdown(single({ alt: 'a\nb', type: 'external', url }))), 'unspellable-whitespace')
|
||||
assert.equal(code(adfToMarkdown(single({ alt: 'a\u0000b', type: 'external', url }))), 'unspellable-character')
|
||||
assert.equal(code(adfToMarkdown(single({ type: 'external', url: 'https://example.com/a b>c' }))), 'unspellable-link-destination')
|
||||
const fallback = (attrs: AdfAttributes): boolean => markdown(adfToMarkdown(single(attrs))).startsWith(':::mediaSingle {layout=center}')
|
||||
assert.ok(fallback({ alt: '', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 'a\nb', type: 'external', url }) && fallback({ alt: 'a\u0000b', type: 'external', url }))
|
||||
assert.ok(fallback({ type: 'external', url: 'https://example.com/a b>c' }) && fallback({ alt: ' moon ', type: 'external', url }))
|
||||
assert.equal(code(adfToMarkdown(single({ alt: 4, type: 'external', url }))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToMarkdown(single({ type: 'external', url: 4 }))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToMarkdown(single({ type: 'external', url }, paragraph()))), 'unsupported-node-shape')
|
||||
@@ -372,4 +372,8 @@ test('spells a table as a pipe table only where every row and cell is plain', ()
|
||||
assert.equal(code(adfToMarkdown(table(row(cell('tableHeader', { attrs: { localId: 'a' }, type: 'paragraph' }))))), 'unspelled-node-attribute')
|
||||
assert.ok(directive(adfToMarkdown(table(row(cell('tableHeader', { attrs: { level: 1 }, type: 'heading' }))))))
|
||||
assert.equal(code(adfToMarkdown(table(row(cell('tableHeader', { content: [{ text: ' a', type: 'text' }], type: 'paragraph' }))))), 'unspellable-whitespace')
|
||||
const marked = (mark: AdfMark): AdfDocument => table(row(cell('tableHeader', { content: [{ marks: [mark], text: 'l', type: 'text' }], type: 'paragraph' })))
|
||||
assert.ok(directive(adfToMarkdown(marked({ attrs: { href: 'https://example.com/?x|y' }, type: 'link' }))))
|
||||
assert.ok(directive(adfToMarkdown(marked({ attrs: { href: 'https://example.com/', title: 'a|b' }, type: 'link' }))))
|
||||
assert.equal(markdown(adfToMarkdown(marked({ attrs: { href: 'https://example.com/x' }, type: 'link' }))), '| [l](https://example.com/x) |\n| --- |\n')
|
||||
})
|
||||
|
||||
@@ -67,7 +67,7 @@ function separationBetween(previous: PlacedBlock, next: PlacedBlock, container:
|
||||
if (previous.spelling === 'directive' && next.spelling === 'directive') return success('\n')
|
||||
return failure(
|
||||
'unspelled-block-separation',
|
||||
`the canonical form leaves the separation between a ${previous.node.type} and a ${next.node.type} in a container body unspelled`,
|
||||
`the canonical form leaves the separation between a ${previous.spelling} and a ${next.spelling} block in a container body unspelled`,
|
||||
next.path,
|
||||
)
|
||||
}
|
||||
|
||||
+4
-2
@@ -77,7 +77,7 @@ function fenceNestingFault(markdown: string): string | undefined {
|
||||
const open: number[] = []
|
||||
let codeFence: string | undefined
|
||||
for (const line of markdown.split('\n')) {
|
||||
const content = line.replace(/^ {0,3}(?:(?:> ?|[-*+] |\d{1,9}[.)] ) {0,3})*/, '')
|
||||
const content = line.replace(/^[ \t]*(?:(?:> ?|[-*+] |\d{1,9}[.)] )[ \t]*)*/, '')
|
||||
const backticks = /^(`{3,}|~{3,})/.exec(content)?.[1]
|
||||
if (codeFence !== undefined) {
|
||||
if (backticks !== undefined && backticks[0] === codeFence[0] && backticks.length >= codeFence.length) codeFence = undefined
|
||||
@@ -103,7 +103,9 @@ function fenceNestingFault(markdown: string): string | undefined {
|
||||
|
||||
test('the fence nesting check catches a fence a container cannot hold', () => {
|
||||
assert.equal(fenceNestingFault(':::panel info\n- :::panel warning\n B\n :::\n:::'), '"- :::panel warning" sits in a container fenced with 3 colons')
|
||||
assert.equal(fenceNestingFault('::::panel info\n- :::panel warning\n B\n :::\n::::'), undefined)
|
||||
assert.equal(fenceNestingFault(':::panel info\n- - :::panel warning\n B\n :::\n:::'), '"- - :::panel warning" sits in a container fenced with 3 colons')
|
||||
assert.equal(fenceNestingFault(':::panel info\n10. :::panel warning\n B\n :::\n:::'), '"10. :::panel warning" sits in a container fenced with 3 colons')
|
||||
assert.equal(fenceNestingFault('::::panel info\n- - :::panel warning\n B\n :::\n::::'), undefined)
|
||||
assert.equal(fenceNestingFault(':::tableCell\n```text\n:::::::panel warning\n:::\n```\n:::'), undefined)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdfNode } from './adf-document.ts'
|
||||
import type { ConvertErrorPath, Result } from './result.ts'
|
||||
import { emitImageLine } from './markdown-inline.ts'
|
||||
import { failure, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
|
||||
const centeredMediaSingle = '{"layout":"center"}'
|
||||
@@ -9,9 +9,8 @@ const imageAttributes = ['alt', 'type', 'url']
|
||||
export function emitImage(node: AdfNode, path: ConvertErrorPath): Result<string> | undefined {
|
||||
const image = imageShape(node)
|
||||
if (image === undefined) return undefined
|
||||
const mediaPath = [...path, 'content', 0]
|
||||
if (image.alt === '') return failure('ambiguous-attribute-spelling', 'an empty media alt and an absent one share one image spelling', mediaPath)
|
||||
return emitImageLine(image.alt, image.url, mediaPath)
|
||||
const line = emitImageLine(image.alt, image.url, [...path, 'content', 0])
|
||||
return line.ok ? line : undefined
|
||||
}
|
||||
|
||||
function imageShape(node: AdfNode): { alt: string | undefined; url: string } | undefined {
|
||||
@@ -22,7 +21,7 @@ function imageShape(node: AdfNode): { alt: string | undefined; url: string } | u
|
||||
const attrs = media.attrs ?? {}
|
||||
const alt = attrs['alt']
|
||||
const url = attrs['url']
|
||||
if (Object.keys(attrs).some((key) => !imageAttributes.includes(key)) || attrs['type'] !== 'external') return undefined
|
||||
if (typeof url !== 'string' || (alt !== undefined && typeof alt !== 'string')) return undefined
|
||||
if (Object.keys(attrs).some((key) => !imageAttributes.includes(key)) || attrs['type'] !== 'external' || typeof url !== 'string') return undefined
|
||||
if (alt !== undefined && (typeof alt !== 'string' || alt === '')) return undefined
|
||||
return { alt, url }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ export function emitInlineLine(nodes: readonly AdfNode[], container: LineContain
|
||||
}
|
||||
|
||||
export function emitImageLine(alt: string | undefined, href: string, path: ConvertErrorPath): Result<string> {
|
||||
if (alt !== undefined && /[\n\r]/.test(alt)) return failure('unspellable-whitespace', 'a media alt holds a newline no image description spells', path)
|
||||
if (alt !== undefined && /^[ \t]|[ \t]$|[\n\r]/.test(alt)) {
|
||||
return failure('unspellable-whitespace', 'a media alt holds whitespace no image description spells', path)
|
||||
}
|
||||
if (alt !== undefined && holdsNullCharacter(alt)) return failure('unspellable-character', 'a media alt holds a null character CommonMark replaces', path)
|
||||
const destination = spellDestination(href, path)
|
||||
if (!destination.ok) return destination
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AdfNode } from './adf-document.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { emitInlineLine } from './markdown-inline.ts'
|
||||
import { success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
|
||||
@@ -48,6 +49,18 @@ function plainParagraph(cell: AdfNode): AdfNode | undefined {
|
||||
const content = cell.content ?? []
|
||||
const paragraph = content[0]
|
||||
if (paragraph === undefined || content.length !== 1 || paragraph.type !== 'paragraph' || !isPlain(paragraph)) return undefined
|
||||
const pipedCode = (paragraph.content ?? []).some((child) => (child.marks ?? []).some((mark) => mark.type === 'code') && (child.text ?? '').includes('|'))
|
||||
return pipedCode ? undefined : paragraph
|
||||
return (paragraph.content ?? []).some(spellsPipeAsSyntax) ? undefined : paragraph
|
||||
}
|
||||
|
||||
// A pipe the inline layer emits as syntax takes no backslash, so the cell has no pipe spelling.
|
||||
function spellsPipeAsSyntax(child: AdfNode): boolean {
|
||||
return (child.marks ?? []).some((mark) => {
|
||||
if (mark.type === 'code') return (child.text ?? '').includes('|')
|
||||
if (mark.type !== 'link') return false
|
||||
return holdsPipe(mark.attrs?.['href']) || holdsPipe(mark.attrs?.['title'])
|
||||
})
|
||||
}
|
||||
|
||||
function holdsPipe(value: JsonValue | undefined): boolean {
|
||||
return typeof value === 'string' && value.includes('|')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user