This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import type { AdfAttributes } from './document.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
|
||||
export type AttributeKind = 'boolean' | 'json' | 'number' | 'string'
|
||||
|
||||
export type AttributeVocabulary = Readonly<Record<string, AttributeKind>>
|
||||
|
||||
export type VocabularyPair =
|
||||
| { key: string; kind: 'boolean'; value: boolean }
|
||||
| { key: string; kind: 'json'; value: JsonValue }
|
||||
| { key: string; kind: 'number'; value: number }
|
||||
| { key: string; kind: 'string'; value: string }
|
||||
|
||||
export function vocabularyPairs(attrs: AdfAttributes, vocabulary: AttributeVocabulary, spelledElsewhere: readonly string[]): VocabularyPair[] | undefined {
|
||||
const pairs: VocabularyPair[] = []
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (spelledElsewhere.includes(key)) continue
|
||||
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
||||
if (kind === undefined) return undefined
|
||||
const pair = vocabularyPair(key, value, kind)
|
||||
if (pair === undefined) return undefined
|
||||
pairs.push(pair)
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
function vocabularyPair(key: string, value: JsonValue, kind: AttributeKind): VocabularyPair | undefined {
|
||||
if (kind === 'boolean') return typeof value === 'boolean' ? { key, kind, value } : undefined
|
||||
if (kind === 'number') return typeof value === 'number' ? { key, kind, value } : undefined
|
||||
if (kind === 'string') return typeof value === 'string' ? { key, kind, value } : undefined
|
||||
return { key, kind, value }
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { AdfMark, AdfNode, AttributeVocabulary } from './adf-document.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { isBareToken, spellAttributes, spellJsonAttribute, vocabularyPairs } from './directive-attributes.ts'
|
||||
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
|
||||
export type BlockDirective = {
|
||||
argument?: string
|
||||
@@ -89,29 +87,3 @@ const blockDirectives: Readonly<Record<string, BlockDirective>> = {
|
||||
export function blockDirective(type: string): BlockDirective | undefined {
|
||||
return Object.hasOwn(blockDirectives, type) ? blockDirectives[type] : undefined
|
||||
}
|
||||
|
||||
export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
||||
const argument = spellArgument(node, directive)
|
||||
if (argument === undefined) return undefined
|
||||
const spelled = directive.argument === undefined ? spelledByBody : [directive.argument, ...spelledByBody]
|
||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, spelled)
|
||||
if (pairs === undefined) return undefined
|
||||
const marks = node.marks ?? []
|
||||
if (marks.length > 0) pairs.push(['marks', spellJsonAttribute(markValues(marks))])
|
||||
const attributes = spellAttributes(pairs)
|
||||
return `${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
||||
}
|
||||
|
||||
function spellArgument(node: AdfNode, directive: BlockDirective): string | undefined {
|
||||
const value = directive.argument === undefined ? undefined : node.attrs?.[directive.argument]
|
||||
if (value === undefined) return ''
|
||||
if (typeof value !== 'string' || !isBareToken(value)) return undefined
|
||||
return ` ${value}`
|
||||
}
|
||||
|
||||
function markValues(marks: readonly AdfMark[]): JsonValue {
|
||||
return marks.map((mark) => {
|
||||
const attrs = mark.attrs ?? {}
|
||||
return Object.keys(attrs).length === 0 ? { type: mark.type } : { attrs, type: mark.type }
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { isAdfDocument } from './adf-document.ts'
|
||||
import { isAdfDocument } from './document.ts'
|
||||
|
||||
test('accepts an editor-normal document', () => {
|
||||
assert.equal(isAdfDocument({ content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 }), true)
|
||||
@@ -1,11 +1,7 @@
|
||||
import { isJsonValue, type JsonValue } from './json-value.ts'
|
||||
import { isJsonValue, type JsonValue } from '../json-value.ts'
|
||||
|
||||
export type AdfAttributes = { [key: string]: JsonValue }
|
||||
|
||||
export type AttributeKind = 'boolean' | 'json' | 'number' | 'string'
|
||||
|
||||
export type AttributeVocabulary = Readonly<Record<string, AttributeKind>>
|
||||
|
||||
export type AdfMark = {
|
||||
attrs?: AdfAttributes
|
||||
type: string
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
|
||||
export type InlineDirective = {
|
||||
attributes: AttributeVocabulary
|
||||
slot?: string
|
||||
}
|
||||
|
||||
const inlineDirectives: Readonly<Record<string, InlineDirective>> = {
|
||||
date: { attributes: { localId: 'string', timestamp: 'string' } },
|
||||
emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string' }, slot: 'text' },
|
||||
hardBreak: { attributes: { localId: 'string', text: 'string' } },
|
||||
inlineCard: { attributes: { data: 'json', localId: 'string', url: 'string' } },
|
||||
mediaInline: {
|
||||
attributes: {
|
||||
alt: 'string',
|
||||
collection: 'string',
|
||||
data: 'json',
|
||||
height: 'number',
|
||||
id: 'string',
|
||||
localId: 'string',
|
||||
occurrenceKey: 'string',
|
||||
type: 'string',
|
||||
width: 'number',
|
||||
},
|
||||
},
|
||||
mention: { attributes: { accessLevel: 'string', id: 'string', localId: 'string', userType: 'string' }, slot: 'text' },
|
||||
status: { attributes: { color: 'string', localId: 'string', style: 'string' }, slot: 'text' },
|
||||
}
|
||||
|
||||
export function inlineDirective(type: string): InlineDirective | undefined {
|
||||
return Object.hasOwn(inlineDirectives, type) ? inlineDirectives[type] : undefined
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
|
||||
export const markAttributes = {
|
||||
border: { color: 'string', size: 'number' },
|
||||
code: {},
|
||||
em: {},
|
||||
link: { href: 'string', title: 'string' },
|
||||
strike: {},
|
||||
strong: {},
|
||||
subsup: { type: 'string' },
|
||||
textColor: { color: 'string' },
|
||||
underline: {},
|
||||
} satisfies Readonly<Record<string, AttributeVocabulary>>
|
||||
|
||||
export type MarkType = keyof typeof markAttributes
|
||||
|
||||
export function isMarkType(type: string): type is MarkType {
|
||||
return Object.hasOwn(markAttributes, type)
|
||||
}
|
||||
+2
-2
@@ -4,8 +4,8 @@ import { dirname, join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { adfToMarkdown } from './adf-to-markdown.ts'
|
||||
import { isAdfDocument } from './adf-document.ts'
|
||||
import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||
import { isAdfDocument } from './adf/document.ts'
|
||||
import { isJsonValue } from './json-value.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { AdfAttributes, AttributeKind, AttributeVocabulary } from './adf-document.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
|
||||
const bareToken = /^[A-Za-z0-9_-]+$/
|
||||
|
||||
// spec/flavour.md, Attributes.
|
||||
const quotedEscapes = /[&<`|]/g
|
||||
|
||||
export function isBareToken(text: string): boolean {
|
||||
return bareToken.test(text)
|
||||
}
|
||||
|
||||
export function vocabularyPairs(attrs: AdfAttributes, vocabulary: AttributeVocabulary, spelledElsewhere: readonly string[]): [string, string][] | undefined {
|
||||
const pairs: [string, string][] = []
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (spelledElsewhere.includes(key)) continue
|
||||
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
||||
if (kind === undefined) return undefined
|
||||
const spelled = spellAttributeValue(value, kind)
|
||||
if (spelled === undefined) return undefined
|
||||
pairs.push([key, spelled])
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
export function spellAttributes(pairs: readonly (readonly [string, string])[]): string {
|
||||
if (pairs.length === 0) return ''
|
||||
const spelled = [...pairs].sort(([left], [right]) => (left < right ? -1 : 1)).map(([key, value]) => `${key}=${value}`)
|
||||
return `{${spelled.join(' ')}}`
|
||||
}
|
||||
|
||||
export function spellAttributeValue(value: JsonValue, kind: AttributeKind): string | undefined {
|
||||
if (kind === 'json') return spellJsonAttribute(value)
|
||||
if (kind === 'boolean') return typeof value === 'boolean' ? `${value}` : undefined
|
||||
if (kind === 'number') return typeof value === 'number' ? spellStringAttribute(JSON.stringify(value)) : undefined
|
||||
return typeof value === 'string' ? spellStringAttribute(value) : undefined
|
||||
}
|
||||
|
||||
export function spellJsonAttribute(value: JsonValue): string {
|
||||
return quote(serializeCanonicalJson(value, 'compact'))
|
||||
}
|
||||
|
||||
export function spellStringAttribute(text: string): string {
|
||||
return isBareToken(text) ? text : quote(text)
|
||||
}
|
||||
|
||||
function quote(text: string): string {
|
||||
return JSON.stringify(text).replace(quotedEscapes, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`)
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf-document.ts'
|
||||
export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/document.ts'
|
||||
export type { ConvertError, ConvertErrorCode, Result } from './result.ts'
|
||||
export type { JsonValue } from './json-value.ts'
|
||||
export { adfToMarkdown } from './adf-to-markdown.ts'
|
||||
export { isAdfDocument } from './adf-document.ts'
|
||||
export { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||
export { isAdfDocument } from './adf/document.ts'
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { AdfMark, AdfNode, AttributeVocabulary } from './adf-document.ts'
|
||||
import { spellAttributes, vocabularyPairs } from './directive-attributes.ts'
|
||||
|
||||
export type InlineDirective = {
|
||||
attributes: AttributeVocabulary
|
||||
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' },
|
||||
hardBreak: { attributes: { localId: 'string', text: 'string' } },
|
||||
inlineCard: { attributes: { data: 'json', localId: 'string', url: 'string' } },
|
||||
mediaInline: {
|
||||
attributes: {
|
||||
alt: 'string',
|
||||
collection: 'string',
|
||||
data: 'json',
|
||||
height: 'number',
|
||||
id: 'string',
|
||||
localId: 'string',
|
||||
occurrenceKey: 'string',
|
||||
type: 'string',
|
||||
width: 'number',
|
||||
},
|
||||
},
|
||||
mention: { attributes: { accessLevel: 'string', id: 'string', localId: 'string', userType: 'string' }, slot: 'text' },
|
||||
status: { attributes: { color: 'string', localId: 'string', style: 'string' }, slot: 'text' },
|
||||
}
|
||||
|
||||
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 markSpelling(type: string): MarkSpelling | undefined {
|
||||
return Object.hasOwn(markSpellings, type) ? markSpellings[type] : undefined
|
||||
}
|
||||
|
||||
export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective): string | undefined {
|
||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, directive.slot === undefined ? [] : [directive.slot])
|
||||
return pairs === undefined ? undefined : spellAttributes(pairs)
|
||||
}
|
||||
|
||||
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
||||
const pairs = vocabularyPairs(mark.attrs ?? {}, vocabulary, [])
|
||||
return pairs === undefined ? undefined : spellAttributes(pairs)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { AdfMark, AdfNode } from '../adf/document.ts'
|
||||
import type { BlockDirective } from '../adf/block-directives.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import { isBareToken, spellAttributes, spellJsonAttribute, spellVocabulary } from './directive-attributes.ts'
|
||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||
|
||||
export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
||||
const argument = spellArgument(node, directive)
|
||||
if (argument === undefined) return undefined
|
||||
const spelled = directive.argument === undefined ? spelledByBody : [directive.argument, ...spelledByBody]
|
||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, spelled)
|
||||
if (pairs === undefined) return undefined
|
||||
const spelledPairs = spellVocabulary(pairs)
|
||||
const marks = node.marks ?? []
|
||||
if (marks.length > 0) spelledPairs.push(['marks', spellJsonAttribute(markValues(marks))])
|
||||
const attributes = spellAttributes(spelledPairs)
|
||||
return `${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
||||
}
|
||||
|
||||
function spellArgument(node: AdfNode, directive: BlockDirective): string | undefined {
|
||||
const value = directive.argument === undefined ? undefined : node.attrs?.[directive.argument]
|
||||
if (value === undefined) return ''
|
||||
if (typeof value !== 'string' || !isBareToken(value)) return undefined
|
||||
return ` ${value}`
|
||||
}
|
||||
|
||||
function markValues(marks: readonly AdfMark[]): JsonValue {
|
||||
return marks.map((mark) => {
|
||||
const attrs = mark.attrs ?? {}
|
||||
return Object.keys(attrs).length === 0 ? { type: mark.type } : { attrs, type: mark.type }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../result.ts'
|
||||
import { holdsControlCharacter, holdsEntityReference } from './commonmark-grammar.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 shaped like an entity reference 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 shaped like an entity reference 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
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import type { VocabularyPair } from '../adf/attribute-vocabulary.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
|
||||
const bareToken = /^[A-Za-z0-9_-]+$/
|
||||
|
||||
// spec/flavour.md, Attributes.
|
||||
const quotedEscapes = /[&<`|]/g
|
||||
|
||||
export function isBareToken(text: string): boolean {
|
||||
return bareToken.test(text)
|
||||
}
|
||||
|
||||
export function spellAttributes(pairs: readonly (readonly [string, string])[]): string {
|
||||
if (pairs.length === 0) return ''
|
||||
const spelled = [...pairs].sort(([left], [right]) => (left < right ? -1 : 1)).map(([key, value]) => `${key}=${value}`)
|
||||
return `{${spelled.join(' ')}}`
|
||||
}
|
||||
|
||||
export function spellVocabulary(pairs: readonly VocabularyPair[]): [string, string][] {
|
||||
return pairs.map((pair): [string, string] => [pair.key, spellAttributeValue(pair)])
|
||||
}
|
||||
|
||||
export function spellJsonAttribute(value: JsonValue): string {
|
||||
return quote(serializeCanonicalJson(value, 'compact'))
|
||||
}
|
||||
|
||||
export function spellStringAttribute(text: string): string {
|
||||
return isBareToken(text) ? text : quote(text)
|
||||
}
|
||||
|
||||
function spellAttributeValue(pair: VocabularyPair): string {
|
||||
if (pair.kind === 'boolean') return `${pair.value}`
|
||||
if (pair.kind === 'json') return spellJsonAttribute(pair.value)
|
||||
if (pair.kind === 'number') return spellStringAttribute(JSON.stringify(pair.value))
|
||||
return spellStringAttribute(pair.value)
|
||||
}
|
||||
|
||||
function quote(text: string): string {
|
||||
return JSON.stringify(text).replace(quotedEscapes, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf-document.ts'
|
||||
import type { Result } from './result.ts'
|
||||
import { adfToMarkdown } from './index.ts'
|
||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToMarkdown } from '../../index.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
@@ -1,16 +1,17 @@
|
||||
import type { AdfDocument, AdfNode } from './adf-document.ts'
|
||||
import type { BlockDirective } from './block-directives.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { blockDirective, spellDirectiveHeader } from './block-directives.ts'
|
||||
import { tryImage } from './markdown-image.ts'
|
||||
import { emitInlineLine } from './markdown-inline.ts'
|
||||
import { tryPipeTable } from './markdown-pipe-table.ts'
|
||||
import { carriedBlock, carryName } from './opaque-carry.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { holdsControlCharacter, holdsEntityReference, holdsNullCharacter, isThematicBreak } from './commonmark-grammar.ts'
|
||||
import { carriesOnly, isAdfDocument } from './adf-document.ts'
|
||||
import { largestNesting } from './nesting.ts'
|
||||
import { fencedCodeBlock } from './backtick-runs.ts'
|
||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import type { JsonValue } from '../../json-value.ts'
|
||||
import { blockDirective } from '../../adf/block-directives.ts'
|
||||
import { carriedBlock, carryName } from '../opaque-carry.ts'
|
||||
import { carriesOnly, isAdfDocument } from '../../adf/document.ts'
|
||||
import { emitInlineLine } from './inline-line.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { fencedCodeBlock } from '../backtick-runs.ts'
|
||||
import { holdsControlCharacter, holdsEntityReference, holdsNullCharacter, isThematicBreak } from '../commonmark-grammar.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { spellDirectiveHeader } from '../block-directive-spelling.ts'
|
||||
import { tryImage } from './image.ts'
|
||||
import { tryPipeTable } from './pipe-table.ts'
|
||||
|
||||
type BlockContainer = 'directive' | 'document' | 'list-item'
|
||||
type BlockSpelling = 'commonmark' | 'directive'
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AdfNode } from './adf-document.ts'
|
||||
import { carriesOnly } from './adf-document.ts'
|
||||
import type { ConvertErrorPath } from './result.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
import { tryImageLine } from './markdown-inline.ts'
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import { carriesOnly } from '../../adf/document.ts'
|
||||
import type { ConvertErrorPath } from '../../result.ts'
|
||||
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
||||
import { tryImageLine } from './inline-line.ts'
|
||||
|
||||
const centeredMediaSingle = '{"layout":"center"}'
|
||||
const imageAttributes = ['alt', 'type', 'url']
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { AdfMark, AdfNode } from './adf-document.ts'
|
||||
import type { InlineDirective } from './inline-directives.ts'
|
||||
import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './markdown-escaping.ts'
|
||||
import { inlineDirective, markSpelling, spellInlineNodeAttributes, spellMarkAttributes } from './inline-directives.ts'
|
||||
import { largestNesting } from './nesting.ts'
|
||||
import { claimsLine, holdsControlCharacter, holdsEntityReference, holdsNullCharacter, isAutolink } from './commonmark-grammar.ts'
|
||||
import { carriedInline } from './opaque-carry.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { longestBacktickRun } from './backtick-runs.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
import { spellAttributes, spellStringAttribute } from './directive-attributes.ts'
|
||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
||||
import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
||||
import { carriedInline } from '../opaque-carry.ts'
|
||||
import { claimsLine, holdsEntityReference, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
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 { spellAttributes, spellStringAttribute } from '../directive-attributes.ts'
|
||||
import { spellDestination, spellTitle } from '../destination-spelling.ts'
|
||||
import { spellInlineNodeAttributes } from '../inline-directive-spelling.ts'
|
||||
|
||||
type EmittedLine = { line: string; segments: InlineSegment[] }
|
||||
|
||||
@@ -293,41 +296,6 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range
|
||||
return success({ segments: [syntax('['), ...inner.value.segments, syntax(`](${destination.value}${spelledTitle.value})`)] })
|
||||
}
|
||||
|
||||
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 shaped like an entity reference 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)
|
||||
}
|
||||
|
||||
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 shaped like an entity reference 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
|
||||
}
|
||||
|
||||
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 { escapesLineClaim, opensBracketedAutolink, startsEntityReference, type LinePosition } from './commonmark-grammar.ts'
|
||||
import { delimiterFlags, isWordCharacter, matchEmphasis } from './emphasis-matching.ts'
|
||||
import { escapesLineClaim, opensBracketedAutolink, startsEntityReference, type LinePosition } from '../commonmark-grammar.ts'
|
||||
import { delimiterFlags, isWordCharacter, matchEmphasis } from '../emphasis-matching.ts'
|
||||
|
||||
export type EmphasisRole = 'close' | 'open'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdfNode } from './adf-document.ts'
|
||||
import { carriesOnly } from './adf-document.ts'
|
||||
import { tryPipeCell } from './markdown-inline.ts'
|
||||
import type { ConvertErrorPath } from './result.ts'
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import { carriesOnly } from '../../adf/document.ts'
|
||||
import { tryPipeCell } from './inline-line.ts'
|
||||
import type { ConvertErrorPath } from '../../result.ts'
|
||||
|
||||
export function tryPipeTable(node: AdfNode, path: ConvertErrorPath): string | undefined {
|
||||
const rows = pipeRows(node)
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { AdfNode } from '../adf/document.ts'
|
||||
import type { InlineDirective } from '../adf/inline-directives.ts'
|
||||
import { spellAttributes, spellVocabulary } from './directive-attributes.ts'
|
||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||
|
||||
export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective): string | undefined {
|
||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, directive.slot === undefined ? [] : [directive.slot])
|
||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { AdfMark } from '../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import type { MarkType } from '../adf/mark-attributes.ts'
|
||||
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { spellAttributes, spellVocabulary } from './directive-attributes.ts'
|
||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||
|
||||
type Spelling = { kind: 'code' | 'directive' | 'link'; spelling?: undefined } | { kind: 'emphasis'; spelling: string }
|
||||
|
||||
export type MarkSpelling = Spelling & { attributes: AttributeVocabulary }
|
||||
|
||||
const markSpellings: Readonly<Record<MarkType, Spelling>> = {
|
||||
border: { kind: 'directive' },
|
||||
code: { kind: 'code' },
|
||||
em: { kind: 'emphasis', spelling: '_' },
|
||||
link: { kind: 'link' },
|
||||
strike: { kind: 'emphasis', spelling: '~~' },
|
||||
strong: { kind: 'emphasis', spelling: '**' },
|
||||
subsup: { kind: 'directive' },
|
||||
textColor: { kind: 'directive' },
|
||||
underline: { kind: 'directive' },
|
||||
}
|
||||
|
||||
export function markSpelling(type: string): MarkSpelling | undefined {
|
||||
if (!isMarkType(type)) return undefined
|
||||
const spelling = markSpellings[type]
|
||||
const attributes = markAttributes[type]
|
||||
if (spelling.kind === 'emphasis') return { attributes, kind: spelling.kind, spelling: spelling.spelling }
|
||||
return { attributes, kind: spelling.kind }
|
||||
}
|
||||
|
||||
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
||||
const pairs = vocabularyPairs(mark.attrs ?? {}, vocabulary, [])
|
||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AdfNode } from './adf-document.ts'
|
||||
import type { JsonSpelling } from './canonical-json.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { isJsonValue } from './json-value.ts'
|
||||
import type { AdfNode } from '../adf/document.ts'
|
||||
import type { JsonSpelling } from '../canonical-json.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../result.ts'
|
||||
import { isJsonValue } from '../json-value.ts'
|
||||
import { fencedCodeBlock } from './backtick-runs.ts'
|
||||
import { largestNesting } from './nesting.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
import { largestNesting } from '../nesting.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
import { spellAttributes, spellStringAttribute } from './directive-attributes.ts'
|
||||
|
||||
export const carryName = 'adf'
|
||||
Reference in New Issue
Block a user