4.2: a link's brackets balance inside a directive's content
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "subsup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "["
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "a",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "underline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "]"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "b",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "underline"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": " ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "underline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "x",
|
||||||
|
"title": "["
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "c",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "underline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "e[f]"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "d",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "underline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "http://a[b"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "http://a[b",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
:subsup[[a](\[)]
|
||||||
|
|
||||||
|
:underline[[b](\]) [c](x "\[")]
|
||||||
|
|
||||||
|
:underline[[d](e[f])]
|
||||||
|
|
||||||
|
:underline[[http://a\[b](http://a\[b)]
|
||||||
+5
-3
@@ -129,9 +129,11 @@ quoted where bare carries it, an escape longer than it need be, an empty `{attrs
|
|||||||
each a named error naming the spelling to write instead.
|
each a named error naming the spelling to write instead.
|
||||||
|
|
||||||
**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as
|
**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as
|
||||||
directive syntax — the leading `:` of a would-be directive, `]` inside content, a `{` right
|
directive syntax — the leading `:` of a would-be directive, `]` inside content, a bracket a link's
|
||||||
after a directive's closing `]`, which would otherwise be read as the attributes it has none
|
destination and title inside content leave unbalanced (an autolink holding one takes the
|
||||||
of; outside code spans and code blocks, a backslash before `:` in input yields a literal colon.
|
`[text](url)` form), a `{` right after a directive's closing `]`, which would otherwise be read as
|
||||||
|
the attributes it has none of; outside code spans and code blocks, a backslash before `:` in input
|
||||||
|
yields a literal colon.
|
||||||
|
|
||||||
**Malformed directives are error results**, named: an unclosed container at end of input, a body
|
**Malformed directives are error results**, named: an unclosed container at end of input, a body
|
||||||
fence line of the container's length or longer, a bare colon-run line outside any container or
|
fence line of the container's length or longer, a bare colon-run line outside any container or
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { InlineDirective } from '../../adf/inline-directives.ts'
|
|||||||
import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
||||||
import { carriedInline } from '../opaque-carry.ts'
|
import { carriedInline } from '../opaque-carry.ts'
|
||||||
import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts'
|
import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts'
|
||||||
|
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../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 { holdsEntityReference } from '../entity-references.ts'
|
import { holdsEntityReference } from '../entity-references.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
@@ -12,7 +13,6 @@ import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
|||||||
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { sameMark } from '../../adf/editor-normal.ts'
|
import { sameMark } from '../../adf/editor-normal.ts'
|
||||||
import { slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts'
|
import { slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts'
|
||||||
import { spellDestination, spellLinkTarget } from '../link-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'
|
||||||
|
|
||||||
@@ -284,12 +284,14 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range
|
|||||||
if (typeof href !== 'string') return success({ carry: range })
|
if (typeof href !== 'string') return success({ carry: range })
|
||||||
const node = nodes[0]
|
const node = nodes[0]
|
||||||
const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1
|
const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1
|
||||||
if (bare && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success({ segments: [syntax(`<${href}>`)] })
|
const balanced = !context.bracketed || escapeUnbalanced(href, '[', ']') === href
|
||||||
|
if (bare && balanced && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success({ segments: [syntax(`<${href}>`)] })
|
||||||
const target = spellLinkTarget(href, typeof title === 'string' ? title : undefined, path)
|
const target = spellLinkTarget(href, typeof title === 'string' ? title : undefined, path)
|
||||||
if (!target.ok) return target
|
if (!target.ok) return target
|
||||||
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
||||||
if (!inner.ok) return inner
|
if (!inner.ok) return inner
|
||||||
if (inner.value.carry !== undefined) return inner
|
if (inner.value.carry !== undefined) return inner
|
||||||
return success({ segments: [syntax('['), ...inner.value.segments, syntax(`](${target.value})`)] })
|
const spelledTarget = context.bracketed ? escapeUnbalanced(target.value, '[', ']') : target.value
|
||||||
|
return success({ segments: [syntax('['), ...inner.value.segments, syntax(`](${spelledTarget})`)] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -103,7 +103,7 @@ export function spellDestination(href: string, path: ConvertErrorPath): Result<s
|
|||||||
return success(`<${href}>`)
|
return success(`<${href}>`)
|
||||||
}
|
}
|
||||||
if (href.startsWith('<')) return failure('unspellable-link', 'a bare link destination cannot begin with an angle bracket', path)
|
if (href.startsWith('<')) return failure('unspellable-link', 'a bare link destination cannot begin with an angle bracket', path)
|
||||||
return success(escapeUnbalanced(href))
|
return success(escapeUnbalanced(href, '(', ')'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spellLinkTarget(href: string, title: string | undefined, path: ConvertErrorPath): Result<string> {
|
export function spellLinkTarget(href: string, title: string | undefined, path: ConvertErrorPath): Result<string> {
|
||||||
@@ -114,6 +114,20 @@ export function spellLinkTarget(href: string, title: string | undefined, path: C
|
|||||||
return success(`${destination.value === '' ? '<>' : destination.value}${spelledTitle.value}`)
|
return success(`${destination.value === '' ? '<>' : destination.value}${spelledTitle.value}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function escapeUnbalanced(spelling: string, opener: string, closer: string): string {
|
||||||
|
const open: number[] = []
|
||||||
|
const unbalanced = new Set<number>()
|
||||||
|
for (let index = 0; index < spelling.length; index += backslashEscape(spelling, index) === undefined ? 1 : 2) {
|
||||||
|
const character = spelling.charAt(index)
|
||||||
|
if (character === opener) open.push(index)
|
||||||
|
if (character === closer && open.pop() === undefined) unbalanced.add(index)
|
||||||
|
}
|
||||||
|
for (const index of open) unbalanced.add(index)
|
||||||
|
let spelled = ''
|
||||||
|
for (let index = 0; index < spelling.length; index += 1) spelled += (unbalanced.has(index) ? '\\' : '') + spelling.charAt(index)
|
||||||
|
return spelled
|
||||||
|
}
|
||||||
|
|
||||||
function spellTitle(title: string, path: ConvertErrorPath): Result<string> {
|
function spellTitle(title: string, path: ConvertErrorPath): Result<string> {
|
||||||
if (/[\n\r\\]/.test(title)) {
|
if (/[\n\r\\]/.test(title)) {
|
||||||
return failure('unspellable-link', 'no canonical escape spells a backslash or newline in a link title', path)
|
return failure('unspellable-link', 'no canonical escape spells a backslash or newline in a link title', path)
|
||||||
@@ -121,17 +135,3 @@ function spellTitle(title: string, path: ConvertErrorPath): Result<string> {
|
|||||||
if (holdsEntityReference(title)) return failure('unspellable-link', 'a link title holds an entity reference that decodes on the way back', path)
|
if (holdsEntityReference(title)) return failure('unspellable-link', 'a link title holds an entity reference that decodes on the way back', path)
|
||||||
return success(` "${title.replaceAll('"', '\\"')}"`)
|
return success(` "${title.replaceAll('"', '\\"')}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeUnbalanced(href: string): string {
|
|
||||||
const open: number[] = []
|
|
||||||
const unbalanced = new Set<number>()
|
|
||||||
for (let index = 0; index < href.length; index += 1) {
|
|
||||||
const character = href.charAt(index)
|
|
||||||
if (character === '(') open.push(index)
|
|
||||||
if (character === ')' && open.pop() === undefined) unbalanced.add(index)
|
|
||||||
}
|
|
||||||
for (const index of open) unbalanced.add(index)
|
|
||||||
let spelled = ''
|
|
||||||
for (let index = 0; index < href.length; index += 1) spelled += (unbalanced.has(index) ? '\\' : '') + href.charAt(index)
|
|
||||||
return spelled
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user