Read the emphasis, the links and the lone image CommonMark spells (#34)
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit was merged in pull request #34.
This commit is contained in:
@@ -76,7 +76,6 @@ test('spells a code block language no info string holds as an attribute', () =>
|
||||
|
||||
test('refuses a link destination CommonMark cannot spell', () => {
|
||||
const link = (href: string): AdfDocument => document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))
|
||||
assert.equal(code(adfToMarkdown(link('https://example.com/a)b'))), 'unspellable-link-destination')
|
||||
assert.equal(code(adfToMarkdown(link('https://example.com/a b>c'))), 'unspellable-link-destination')
|
||||
assert.equal(code(adfToMarkdown(link('<https://example.com/'))), 'unspellable-link-destination')
|
||||
assert.equal(code(adfToMarkdown(link('https://example.com/a\\b'))), 'unspellable-link-destination')
|
||||
@@ -84,14 +83,23 @@ test('refuses a link destination CommonMark cannot spell', () => {
|
||||
assert.equal(code(adfToMarkdown(link('https://example.com/a\nb'))), 'unspellable-link-destination')
|
||||
const entity = 'https://example.com/?a=1&b=2'
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' })))), 'unspellable-link-destination')
|
||||
assert.equal(markdown(adfToMarkdown(link('https://en.example.com/a_(b)'))), '[t](https://en.example.com/a_(b))\n')
|
||||
})
|
||||
|
||||
test('refuses a link title CommonMark cannot spell', () => {
|
||||
test('escapes the parenthesis a link destination leaves unbalanced, and no other', () => {
|
||||
const link = (href: string): string => markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(link('https://en.example.com/a_(b)'), '[t](https://en.example.com/a_(b))\n')
|
||||
assert.equal(link('https://example.com/a)b'), '[t](https://example.com/a\\)b)\n')
|
||||
assert.equal(link('https://example.com/a(b'), '[t](https://example.com/a\\(b)\n')
|
||||
assert.equal(link('https://example.com/)(') , '[t](https://example.com/\\)\\()\n')
|
||||
assert.equal(link('https://example.com/a (b'), '[t](<https://example.com/a (b>)\n')
|
||||
})
|
||||
|
||||
test('escapes the quote a link title holds, and refuses the rest', () => {
|
||||
const titled = (title: string): AdfDocument =>
|
||||
document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' }))
|
||||
assert.equal(code(adfToMarkdown(titled('He said "hi"'))), 'unspellable-link-title')
|
||||
assert.equal(markdown(adfToMarkdown(titled('He said "hi"'))), '[t](https://example.com/ "He said \\"hi\\"")\n')
|
||||
assert.equal(code(adfToMarkdown(titled('a\nb'))), 'unspellable-link-title')
|
||||
assert.equal(code(adfToMarkdown(titled('a\\b'))), 'unspellable-link-title')
|
||||
})
|
||||
|
||||
test('carries a link mark the link spelling cannot write', () => {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { holdsControlCharacter } from '../commonmark-grammar.ts'
|
||||
import { holdsEntityReference } from '../entity-references.ts'
|
||||
|
||||
export function spellDestination(href: string, path: ConvertErrorPath): Result<string> {
|
||||
if (holdsControlCharacter(href)) return failure('unspellable-link-destination', 'a link destination holds a control character', path)
|
||||
if (href.includes('\\')) return failure('unspellable-link-destination', 'no canonical escape spells a backslash in a link destination', path)
|
||||
if (holdsEntityReference(href)) {
|
||||
return failure('unspellable-link-destination', 'a link destination holds an entity reference that decodes on the way back', path)
|
||||
}
|
||||
if (href.includes(' ')) {
|
||||
if (/[<>]/.test(href)) {
|
||||
return failure('unspellable-link-destination', 'no canonical escape spells an angle bracket beside a space in a link destination', path)
|
||||
}
|
||||
return success(`<${href}>`)
|
||||
}
|
||||
if (href.startsWith('<')) return failure('unspellable-link-destination', 'a bare link destination cannot begin with an angle bracket', path)
|
||||
if (!balanced(href)) return failure('unspellable-link-destination', 'no canonical escape spells an unbalanced parenthesis in a link destination', path)
|
||||
return success(href)
|
||||
}
|
||||
|
||||
export function spellTitle(title: string, path: ConvertErrorPath): Result<string> {
|
||||
if (/["\n\r\\]/.test(title)) {
|
||||
return failure('unspellable-link-title', 'no canonical escape spells a quote, backslash or newline in a link title', path)
|
||||
}
|
||||
if (holdsEntityReference(title)) return failure('unspellable-link-title', 'a link title holds an entity reference that decodes on the way back', path)
|
||||
return success(` "${title}"`)
|
||||
}
|
||||
|
||||
function balanced(href: string): boolean {
|
||||
let depth = 0
|
||||
for (const character of href) {
|
||||
if (character === '(') depth += 1
|
||||
if (character === ')') depth -= 1
|
||||
if (depth < 0) return false
|
||||
}
|
||||
return depth === 0
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { longestBacktickRun } from '../backtick-runs.ts'
|
||||
import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
||||
import { sameMark } from '../../adf/editor-normal.ts'
|
||||
import { spellAttributes, spellStringAttribute } from '../directive-attributes.ts'
|
||||
import { spellDestination, spellTitle } from './destination-spelling.ts'
|
||||
import { spellDestination, spellTitle } from '../link-syntax.ts'
|
||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||
|
||||
type EmittedLine = { line: string; segments: InlineSegment[] }
|
||||
@@ -297,7 +297,3 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range
|
||||
return success({ segments: [syntax('['), ...inner.value.segments, syntax(`](${destination.value}${spelledTitle.value})`)] })
|
||||
}
|
||||
|
||||
function sameMark(candidate: AdfMark, mark: AdfMark): boolean {
|
||||
if (candidate.type !== mark.type) return false
|
||||
return serializeCanonicalJson(candidate.attrs ?? {}, 'compact') === serializeCanonicalJson(mark.attrs ?? {}, 'compact')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
|
||||
import { delimiterFlags, isWordCharacter, matchEmphasis } from '../emphasis-matching.ts'
|
||||
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../emphasis-matching.ts'
|
||||
import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts'
|
||||
import { opensInlineDirective } from '../directive-attributes.ts'
|
||||
import { readEntityReference } from '../entity-references.ts'
|
||||
@@ -260,13 +260,6 @@ function startsRun(scan: string, index: number, escaped: ReadonlySet<number>): b
|
||||
return scan.charAt(index - 1) !== scan.charAt(index)
|
||||
}
|
||||
|
||||
function runLength(scan: string, index: number): number {
|
||||
const character = scan.charAt(index)
|
||||
let length = 0
|
||||
while (scan.charAt(index + length) === character) length += 1
|
||||
return length
|
||||
}
|
||||
|
||||
function charAt(text: string, index: number): string {
|
||||
return index < 0 ? '' : text.charAt(index)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user