Corpus 2e2: the mark runs, the run a carry breaks, and one mark vocabulary
CI / gate (push) Successful in 4s
CI / gate (push) Successful in 4s
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "em"
|
||||
},
|
||||
{
|
||||
"type": "strong"
|
||||
}
|
||||
],
|
||||
"text": "Ready",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"extensionKey": "status-lozenge",
|
||||
"extensionType": "com.atlassian.confluence.macro.core"
|
||||
},
|
||||
"marks": [
|
||||
{
|
||||
"type": "em"
|
||||
},
|
||||
{
|
||||
"type": "strong"
|
||||
}
|
||||
],
|
||||
"type": "inlineExtension"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "em"
|
||||
},
|
||||
{
|
||||
"type": "strong"
|
||||
}
|
||||
],
|
||||
"text": "to ship",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
_**Ready**_:adf{json="{\"attrs\":{\"extensionKey\":\"status-lozenge\",\"extensionType\":\"com.atlassian.confluence.macro.core\"},\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"}_**to ship**_
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"color": "#ae2e24"
|
||||
},
|
||||
"type": "textColor"
|
||||
}
|
||||
],
|
||||
"text": "Overdue since ",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"color": "red",
|
||||
"text": "Blocked"
|
||||
},
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"color": "#ae2e24"
|
||||
},
|
||||
"type": "textColor"
|
||||
}
|
||||
],
|
||||
"type": "status"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"color": "#ae2e24"
|
||||
},
|
||||
"type": "textColor"
|
||||
}
|
||||
],
|
||||
"text": " on Tuesday",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "em"
|
||||
}
|
||||
],
|
||||
"text": "Torque the ",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "em"
|
||||
},
|
||||
{
|
||||
"type": "strong"
|
||||
}
|
||||
],
|
||||
"text": "M8 bolt",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "em"
|
||||
}
|
||||
],
|
||||
"text": " to ",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "em"
|
||||
},
|
||||
{
|
||||
"type": "strong"
|
||||
}
|
||||
],
|
||||
"text": "25 Nm",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "https://example.com/release"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"text": "Release notes ",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"shortName": ":rocket:",
|
||||
"text": "🚀"
|
||||
},
|
||||
"marks": [
|
||||
{
|
||||
"attrs": {
|
||||
"href": "https://example.com/release"
|
||||
},
|
||||
"type": "link"
|
||||
}
|
||||
],
|
||||
"type": "emoji"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
:textColor[Overdue since :status[Blocked]{color=red} on Tuesday]{color="#ae2e24"}
|
||||
|
||||
_Torque the **M8 bolt** to **25 Nm**_
|
||||
|
||||
[Release notes :emoji[🚀]{shortName=":rocket:"}](https://example.com/release)
|
||||
@@ -0,0 +1 @@
|
||||
unspellable-mark
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "un",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"marks": [
|
||||
{
|
||||
"type": "strong"
|
||||
}
|
||||
],
|
||||
"text": "-real",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"text": "istic",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"type": "paragraph"
|
||||
}
|
||||
],
|
||||
"type": "doc",
|
||||
"version": 1
|
||||
}
|
||||
+4
-3
@@ -386,9 +386,10 @@ inline nodes carrying an identical mark, attributes included, at that depth. A r
|
||||
node the emitter carries, so no emitted carry sits inside a mark spelling.
|
||||
|
||||
An inline node whose marks no nesting spells — a mark type not listed here, an attribute no
|
||||
spelling holds, an order putting a code span outside another mark, or `code` on text holding a
|
||||
newline — rides the inline carry whole. An opaque carry inside a mark spelling is a named error
|
||||
in input: the carry restores its node exactly, marks included (AGENTS.md §3).
|
||||
spelling holds, an order putting a code span outside another mark, `code` on text holding a
|
||||
newline, or a spelling CommonMark's flanking rules cannot open or close where the run sits
|
||||
(`un**-real**istic`) — rides the inline carry whole. An opaque carry inside a mark spelling is a
|
||||
named error in input: the carry restores its node exactly, marks included (AGENTS.md §3).
|
||||
|
||||
```
|
||||
:textColor[**Overdue**]{color="#ae2e24"}, H:subsup[2]{type=sub}O, :underline[signed].
|
||||
|
||||
@@ -96,6 +96,7 @@ test('refuses a mark the canonical spellings cannot nest', () => {
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ type: 'annotation' }], text: 'x', type: 'text' })))), 'unspellable-mark')
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }, { type: 'strong' }], text: 'x', type: 'text' })))), 'unspellable-mark')
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs: { colour: 'red' }, type: 'em' }], text: 'x', type: 'text' })))), 'unspellable-mark')
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs: { localId: 'x' }, type: 'code' }], text: 'x', type: 'text' })))), 'unspellable-mark')
|
||||
})
|
||||
|
||||
test('refuses whitespace CommonMark cannot hold', () => {
|
||||
|
||||
@@ -8,6 +8,10 @@ export type InlineDirective = {
|
||||
slot?: string
|
||||
}
|
||||
|
||||
export type MarkSpelling =
|
||||
| { attributes: AttributeVocabulary; kind: 'code' | 'directive' | 'link'; spelling?: undefined }
|
||||
| { attributes: AttributeVocabulary; kind: 'emphasis'; spelling: string }
|
||||
|
||||
const inlineDirectives: Readonly<Record<string, InlineDirective>> = {
|
||||
date: { attributes: { localId: 'string', timestamp: 'string' } },
|
||||
emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string' }, slot: 'text' },
|
||||
@@ -29,19 +33,24 @@ const inlineDirectives: Readonly<Record<string, InlineDirective>> = {
|
||||
status: { attributes: { color: 'string', localId: 'string', style: 'string' }, slot: 'text' },
|
||||
}
|
||||
|
||||
const markDirectives: Readonly<Record<string, AttributeVocabulary>> = {
|
||||
border: { color: 'string', size: 'number' },
|
||||
subsup: { type: 'string' },
|
||||
textColor: { color: 'string' },
|
||||
underline: {},
|
||||
const markSpellings: Readonly<Record<string, MarkSpelling>> = {
|
||||
border: { attributes: { color: 'string', size: 'number' }, kind: 'directive' },
|
||||
code: { attributes: {}, kind: 'code' },
|
||||
em: { attributes: {}, kind: 'emphasis', spelling: '_' },
|
||||
link: { attributes: { href: 'string', title: 'string' }, kind: 'link' },
|
||||
strike: { attributes: {}, kind: 'emphasis', spelling: '~~' },
|
||||
strong: { attributes: {}, kind: 'emphasis', spelling: '**' },
|
||||
subsup: { attributes: { type: 'string' }, kind: 'directive' },
|
||||
textColor: { attributes: { color: 'string' }, kind: 'directive' },
|
||||
underline: { attributes: {}, kind: 'directive' },
|
||||
}
|
||||
|
||||
export function inlineDirective(type: string): InlineDirective | undefined {
|
||||
return Object.hasOwn(inlineDirectives, type) ? inlineDirectives[type] : undefined
|
||||
}
|
||||
|
||||
export function markDirective(type: string): AttributeVocabulary | undefined {
|
||||
return Object.hasOwn(markDirectives, type) ? markDirectives[type] : undefined
|
||||
export function markSpelling(type: string): MarkSpelling | undefined {
|
||||
return Object.hasOwn(markSpellings, type) ? markSpellings[type] : undefined
|
||||
}
|
||||
|
||||
export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective, path: ConvertErrorPath): Result<string> {
|
||||
|
||||
+9
-18
@@ -1,7 +1,7 @@
|
||||
import type { AdfMark, AdfNode } from './adf-document.ts'
|
||||
import type { InlineDirective } from './inline-directives.ts'
|
||||
import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer } from './markdown-escaping.ts'
|
||||
import { inlineDirective, markDirective, spellInlineNodeAttributes, spellMarkAttributes } from './inline-directives.ts'
|
||||
import { inlineDirective, markSpelling, spellInlineNodeAttributes, spellMarkAttributes } from './inline-directives.ts'
|
||||
import { largestNesting } from './nesting.ts'
|
||||
import { claimsLine, holdsControlCharacter, holdsEntityReference, holdsNullCharacter, isAutolink, isUnicodeWhitespace } from './commonmark-grammar.ts'
|
||||
import { carriedInline } from './opaque-carry.ts'
|
||||
@@ -19,9 +19,6 @@ type InlineContext = {
|
||||
|
||||
type InlineRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: AdfNode[] } | { index: number; kind: 'plain'; node: AdfNode }
|
||||
|
||||
const emphasisSpellings: Readonly<Record<string, string>> = { em: '_', strike: '~~', strong: '**' }
|
||||
const linkAttributes = ['href', 'title']
|
||||
|
||||
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> {
|
||||
const segments = lineSegments(nodes, container, path)
|
||||
if (!segments.ok) return segments
|
||||
@@ -204,15 +201,14 @@ function emitText(node: AdfNode, context: InlineContext, path: ConvertErrorPath)
|
||||
}
|
||||
|
||||
function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number, index: number, context: InlineContext): Result<InlineSegment[]> {
|
||||
if (mark.type === 'code') return emitCodeSpan(nodes, depth, nodePath(context, index))
|
||||
if (mark.type === 'link') return emitLink(nodes, mark, depth, index, context)
|
||||
const path = nodePath(context, index)
|
||||
const spelling = Object.hasOwn(emphasisSpellings, mark.type) ? emphasisSpellings[mark.type] : undefined
|
||||
if (spelling !== undefined) return emitEmphasis(nodes, mark, spelling, depth, index, context, path)
|
||||
const vocabulary = markDirective(mark.type)
|
||||
if (vocabulary === undefined) return failure('unspellable-mark', `no markdown spelling holds the ${mark.type} mark`, path)
|
||||
const attributes = spellMarkAttributes(mark, vocabulary, path)
|
||||
const spelling = markSpelling(mark.type)
|
||||
if (spelling === undefined) return failure('unspellable-mark', `no markdown spelling holds the ${mark.type} mark`, path)
|
||||
const attributes = spellMarkAttributes(mark, spelling.attributes, path)
|
||||
if (!attributes.ok) return attributes
|
||||
if (spelling.kind === 'code') return emitCodeSpan(nodes, depth, path)
|
||||
if (spelling.kind === 'emphasis') return emitEmphasis(nodes, mark, spelling.spelling, depth, index, context, path)
|
||||
if (spelling.kind === 'link') return emitLink(nodes, mark, depth, index, context, path)
|
||||
const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true, spansLines: false })
|
||||
if (!inner.ok) return inner
|
||||
return success([syntax(`:${mark.type}[`), ...inner.value, syntax(`]${attributes.value}`)])
|
||||
@@ -227,7 +223,6 @@ function emitEmphasis(
|
||||
context: InlineContext,
|
||||
path: ConvertErrorPath,
|
||||
): Result<InlineSegment[]> {
|
||||
if (Object.keys(mark.attrs ?? {}).length > 0) return failure('unspellable-mark', `the ${mark.type} spelling holds no attributes`, path)
|
||||
const inner = emitRun(nodes, depth + 1, index, context)
|
||||
if (!inner.ok) return inner
|
||||
const carried = carryStrippedWhitespace(inner.value)
|
||||
@@ -263,20 +258,16 @@ function needsPadding(text: string): boolean {
|
||||
return text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
|
||||
}
|
||||
|
||||
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, index: number, context: InlineContext): Result<InlineSegment[]> {
|
||||
const path = nodePath(context, index)
|
||||
const unspelled = Object.keys(mark.attrs ?? {}).find((key) => !linkAttributes.includes(key))
|
||||
if (unspelled !== undefined) return failure('unspellable-mark', `the link spelling holds no ${unspelled} attribute`, path)
|
||||
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, index: number, context: InlineContext, path: ConvertErrorPath): Result<InlineSegment[]> {
|
||||
const href = mark.attrs?.['href']
|
||||
const title = mark.attrs?.['title']
|
||||
if (typeof href !== 'string') return failure('unsupported-node-shape', 'a link mark carries no href', path)
|
||||
if (title !== undefined && typeof title !== 'string') return failure('unsupported-node-shape', 'a link title is no string', path)
|
||||
const node = nodes[0]
|
||||
const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && (node.marks ?? []).length === depth + 1
|
||||
if (bare && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success([syntax(`<${href}>`)])
|
||||
const destination = spellDestination(href, path)
|
||||
if (!destination.ok) return destination
|
||||
const spelledTitle = title === undefined ? success('') : spellTitle(title, path)
|
||||
const spelledTitle = typeof title === 'string' ? spellTitle(title, path) : success('')
|
||||
if (!spelledTitle.ok) return spelledTitle
|
||||
const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true })
|
||||
if (!inner.ok) return inner
|
||||
|
||||
Reference in New Issue
Block a user