Answer the architecture review: emit-only spellings move down, and the tables drop markdown's words
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit is contained in:
+42
-38
@@ -1,9 +1,8 @@
|
||||
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
|
||||
export type BlockDirective = {
|
||||
argument?: string
|
||||
attributes: AttributeVocabulary
|
||||
body: 'block' | 'code' | 'inline' | 'none'
|
||||
contentModel: 'block' | 'code' | 'inline' | 'none'
|
||||
}
|
||||
|
||||
const cellAttributes: AttributeVocabulary = {
|
||||
@@ -42,48 +41,53 @@ const mediaAttributes: AttributeVocabulary = {
|
||||
|
||||
const syncBlockAttributes: AttributeVocabulary = { localId: 'string', resourceId: 'string' }
|
||||
|
||||
const blockDirectives: Readonly<Record<string, BlockDirective>> = {
|
||||
blockTaskItem: { argument: 'state', attributes: localIdAttributes, body: 'block' },
|
||||
blockquote: { attributes: localIdAttributes, body: 'block' },
|
||||
bodiedExtension: { attributes: extensionAttributes, body: 'block' },
|
||||
bodiedSyncBlock: { attributes: syncBlockAttributes, body: 'block' },
|
||||
bulletList: { attributes: localIdAttributes, body: 'block' },
|
||||
caption: { attributes: localIdAttributes, body: 'inline' },
|
||||
const blockDirectives = {
|
||||
blockTaskItem: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
blockquote: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
bodiedExtension: { attributes: extensionAttributes, contentModel: 'block' },
|
||||
bodiedSyncBlock: { attributes: syncBlockAttributes, contentModel: 'block' },
|
||||
bulletList: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
caption: { attributes: localIdAttributes, contentModel: 'inline' },
|
||||
codeBlock: {
|
||||
attributes: { hideLineNumbers: 'boolean', language: 'string', localId: 'string', uniqueId: 'string', wrap: 'boolean' },
|
||||
body: 'code',
|
||||
contentModel: 'code',
|
||||
},
|
||||
decisionItem: { attributes: { localId: 'string', state: 'string' }, body: 'inline' },
|
||||
decisionList: { attributes: localIdAttributes, body: 'block' },
|
||||
expand: { attributes: expandAttributes, body: 'block' },
|
||||
extension: { attributes: extensionAttributes, body: 'none' },
|
||||
extensionFrame: { attributes: {}, body: 'block' },
|
||||
heading: { attributes: { level: 'number', localId: 'string' }, body: 'inline' },
|
||||
layoutColumn: { attributes: { localId: 'string', valign: 'string', width: 'number' }, body: 'block' },
|
||||
layoutSection: { attributes: localIdAttributes, body: 'block' },
|
||||
listItem: { attributes: localIdAttributes, body: 'block' },
|
||||
media: { attributes: mediaAttributes, body: 'none' },
|
||||
mediaGroup: { attributes: {}, body: 'block' },
|
||||
mediaSingle: { attributes: { layout: 'string', localId: 'string', width: 'number', widthType: 'string' }, body: 'block' },
|
||||
multiBodiedExtension: { attributes: extensionAttributes, body: 'block' },
|
||||
nestedExpand: { attributes: expandAttributes, body: 'block' },
|
||||
orderedList: { attributes: { localId: 'string', order: 'number' }, body: 'block' },
|
||||
decisionItem: { attributes: { localId: 'string', state: 'string' }, contentModel: 'inline' },
|
||||
decisionList: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
expand: { attributes: expandAttributes, contentModel: 'block' },
|
||||
extension: { attributes: extensionAttributes, contentModel: 'none' },
|
||||
extensionFrame: { attributes: {}, contentModel: 'block' },
|
||||
heading: { attributes: { level: 'number', localId: 'string' }, contentModel: 'inline' },
|
||||
layoutColumn: { attributes: { localId: 'string', valign: 'string', width: 'number' }, contentModel: 'block' },
|
||||
layoutSection: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
listItem: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
media: { attributes: mediaAttributes, contentModel: 'none' },
|
||||
mediaGroup: { attributes: {}, contentModel: 'block' },
|
||||
mediaSingle: { attributes: { layout: 'string', localId: 'string', width: 'number', widthType: 'string' }, contentModel: 'block' },
|
||||
multiBodiedExtension: { attributes: extensionAttributes, contentModel: 'block' },
|
||||
nestedExpand: { attributes: expandAttributes, contentModel: 'block' },
|
||||
orderedList: { attributes: { localId: 'string', order: 'number' }, contentModel: 'block' },
|
||||
panel: {
|
||||
argument: 'panelType',
|
||||
attributes: { localId: 'string', panelColor: 'string', panelIcon: 'string', panelIconId: 'string', panelIconText: 'string' },
|
||||
body: 'block',
|
||||
contentModel: 'block',
|
||||
},
|
||||
paragraph: { attributes: localIdAttributes, body: 'inline' },
|
||||
rule: { attributes: localIdAttributes, body: 'none' },
|
||||
syncBlock: { attributes: syncBlockAttributes, body: 'none' },
|
||||
table: { attributes: { displayMode: 'string', isNumberColumnEnabled: 'boolean', layout: 'string', localId: 'string', width: 'number' }, body: 'block' },
|
||||
tableCell: { attributes: cellAttributes, body: 'block' },
|
||||
tableHeader: { attributes: cellAttributes, body: 'block' },
|
||||
tableRow: { attributes: localIdAttributes, body: 'block' },
|
||||
taskItem: { argument: 'state', attributes: localIdAttributes, body: 'inline' },
|
||||
taskList: { attributes: localIdAttributes, body: 'block' },
|
||||
}
|
||||
paragraph: { attributes: localIdAttributes, contentModel: 'inline' },
|
||||
rule: { attributes: localIdAttributes, contentModel: 'none' },
|
||||
syncBlock: { attributes: syncBlockAttributes, contentModel: 'none' },
|
||||
table: { attributes: { displayMode: 'string', isNumberColumnEnabled: 'boolean', layout: 'string', localId: 'string', width: 'number' }, contentModel: 'block' },
|
||||
tableCell: { attributes: cellAttributes, contentModel: 'block' },
|
||||
tableHeader: { attributes: cellAttributes, contentModel: 'block' },
|
||||
tableRow: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
taskItem: { attributes: localIdAttributes, contentModel: 'inline' },
|
||||
taskList: { attributes: localIdAttributes, contentModel: 'block' },
|
||||
} satisfies Readonly<Record<string, BlockDirective>>
|
||||
|
||||
export type BlockType = keyof typeof blockDirectives
|
||||
|
||||
export function blockDirective(type: string): BlockDirective | undefined {
|
||||
return Object.hasOwn(blockDirectives, type) ? blockDirectives[type] : undefined
|
||||
return isBlockType(type) ? blockDirectives[type] : undefined
|
||||
}
|
||||
|
||||
function isBlockType(type: string): type is BlockType {
|
||||
return Object.hasOwn(blockDirectives, type)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
|
||||
export type InlineDirective = {
|
||||
attributes: AttributeVocabulary
|
||||
slot?: string
|
||||
textAttribute?: string
|
||||
}
|
||||
|
||||
const inlineDirectives: Readonly<Record<string, InlineDirective>> = {
|
||||
date: { attributes: { localId: 'string', timestamp: 'string' } },
|
||||
emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string' }, slot: 'text' },
|
||||
emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string' }, textAttribute: 'text' },
|
||||
hardBreak: { attributes: { localId: 'string', text: 'string' } },
|
||||
inlineCard: { attributes: { data: 'json', localId: 'string', url: 'string' } },
|
||||
mediaInline: {
|
||||
@@ -23,8 +23,8 @@ const inlineDirectives: Readonly<Record<string, InlineDirective>> = {
|
||||
width: 'number',
|
||||
},
|
||||
},
|
||||
mention: { attributes: { accessLevel: 'string', id: 'string', localId: 'string', userType: 'string' }, slot: 'text' },
|
||||
status: { attributes: { color: 'string', localId: 'string', style: 'string' }, slot: 'text' },
|
||||
mention: { attributes: { accessLevel: 'string', id: 'string', localId: 'string', userType: 'string' }, textAttribute: 'text' },
|
||||
status: { attributes: { color: 'string', localId: 'string', style: 'string' }, textAttribute: 'text' },
|
||||
}
|
||||
|
||||
export function inlineDirective(type: string): InlineDirective | undefined {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { BlockType } from '../adf/block-directives.ts'
|
||||
|
||||
const blockArguments: Readonly<Record<string, string>> = {
|
||||
blockTaskItem: 'state',
|
||||
panel: 'panelType',
|
||||
taskItem: 'state',
|
||||
} satisfies Partial<Record<BlockType, string>>
|
||||
|
||||
export function blockArgument(type: string): string | undefined {
|
||||
return blockArguments[type]
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
export type LinePosition = 'first' | 'later'
|
||||
|
||||
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
||||
const htmlConstructs = [/^<[!?]/, /^<\/?[A-Za-z][A-Za-z0-9-]*(?:[\s/>]|$)/, /^<[^\s<>@]+@[^\s<>@]+>/]
|
||||
|
||||
const controlCharacterRange = '\\u0000-\\u001f\\u007f'
|
||||
const autolinkSource = `[A-Za-z][A-Za-z0-9+.-]{1,31}:[^\\s<>${controlCharacterRange}]*`
|
||||
const nullCharacterSource = '\\u0000'
|
||||
@@ -42,6 +45,10 @@ export function holdsNullCharacter(text: string): boolean {
|
||||
return nullCharacter.test(text)
|
||||
}
|
||||
|
||||
export function isAsciiPunctuation(character: string): boolean {
|
||||
return asciiPunctuation.test(character)
|
||||
}
|
||||
|
||||
export function isAutolink(text: string): boolean {
|
||||
return autolink.test(text)
|
||||
}
|
||||
@@ -58,6 +65,10 @@ export function opensBracketedAutolink(text: string): boolean {
|
||||
return bracketedAutolink.test(text)
|
||||
}
|
||||
|
||||
export function opensHtmlConstruct(text: string): boolean {
|
||||
return htmlConstructs.some((construct) => construct.test(text))
|
||||
}
|
||||
|
||||
export function startsEntityReference(text: string): boolean {
|
||||
return anchoredEntityReference.test(text)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { VocabularyPair } from '../adf/attribute-vocabulary.ts'
|
||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||
|
||||
const bareToken = /^[A-Za-z0-9_-]+$/
|
||||
const inlineDirectiveOpener = /^:[a-z][A-Za-z0-9]*[[{]/
|
||||
|
||||
// spec/flavour.md, Attributes.
|
||||
const quotedEscapes = /[&<`|]/g
|
||||
@@ -11,6 +12,10 @@ export function isBareToken(text: string): boolean {
|
||||
return bareToken.test(text)
|
||||
}
|
||||
|
||||
export function opensInlineDirective(text: string): boolean {
|
||||
return inlineDirectiveOpener.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}`)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { failure, success, type ConvertErrorPath, type Result } from '../../resu
|
||||
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 { spellDirectiveHeader } from './block-directive-spelling.ts'
|
||||
import { tryImage } from './image.ts'
|
||||
import { tryPipeTable } from './pipe-table.ts'
|
||||
|
||||
@@ -117,14 +117,14 @@ function commonMarkText(text: string): EmittedBlock {
|
||||
function emitDirectiveBlock(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text`, path)
|
||||
const content = node.content ?? []
|
||||
if (directive.body === 'none' && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content`, path)
|
||||
if (directive.body === 'code') return emitCodeDirective(node, directive, path)
|
||||
if (directive.contentModel === 'none' && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content`, path)
|
||||
if (directive.contentModel === 'code') return emitCodeDirective(node, directive, path)
|
||||
const header = spellDirectiveHeader(node, directive)
|
||||
if (header === undefined) return commonMarkLine(carriedBlock(node, path))
|
||||
if (directive.body === 'none' || (directive.body === 'inline' && content.length === 0)) {
|
||||
if (directive.contentModel === 'none' || (directive.contentModel === 'inline' && content.length === 0)) {
|
||||
return success({ fenceColons: 2, spelling: 'directive', text: `::${header}` })
|
||||
}
|
||||
const body = directive.body === 'inline' ? emitInlineBody(content, path) : emitBlocks(content, 'directive', path, depth + 1)
|
||||
const body = directive.contentModel === 'inline' ? emitInlineBody(content, path) : emitBlocks(content, 'directive', path, depth + 1)
|
||||
if (!body.ok) return body
|
||||
const fenceColons = Math.max(3, body.value.fenceColons + 1)
|
||||
const fence = ':'.repeat(fenceColons)
|
||||
|
||||
+11
-9
@@ -1,13 +1,15 @@
|
||||
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'
|
||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import type { JsonValue } from '../../json-value.ts'
|
||||
import { blockArgument } from '../block-directive-arguments.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)
|
||||
const argumentAttribute = blockArgument(node.type)
|
||||
const argument = spellArgument(node, argumentAttribute)
|
||||
if (argument === undefined) return undefined
|
||||
const spelled = directive.argument === undefined ? spelledByBody : [directive.argument, ...spelledByBody]
|
||||
const spelled = argumentAttribute === undefined ? spelledByBody : [argumentAttribute, ...spelledByBody]
|
||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, spelled)
|
||||
if (pairs === undefined) return undefined
|
||||
const spelledPairs = spellVocabulary(pairs)
|
||||
@@ -17,8 +19,8 @@ export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, s
|
||||
return `${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
||||
}
|
||||
|
||||
function spellArgument(node: AdfNode, directive: BlockDirective): string | undefined {
|
||||
const value = directive.argument === undefined ? undefined : node.attrs?.[directive.argument]
|
||||
function spellArgument(node: AdfNode, argumentAttribute: string | undefined): string | undefined {
|
||||
const value = argumentAttribute === undefined ? undefined : node.attrs?.[argumentAttribute]
|
||||
if (value === undefined) return ''
|
||||
if (typeof value !== 'string' || !isBareToken(value)) return undefined
|
||||
return ` ${value}`
|
||||
@@ -1,5 +1,5 @@
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../result.ts'
|
||||
import { holdsControlCharacter, holdsEntityReference } from './commonmark-grammar.ts'
|
||||
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)
|
||||
@@ -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.textAttribute === undefined ? [] : [directive.textAttribute])
|
||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||
}
|
||||
@@ -10,8 +10,8 @@ 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'
|
||||
import { spellDestination, spellTitle } from './destination-spelling.ts'
|
||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||
|
||||
type EmittedLine = { line: string; segments: InlineSegment[] }
|
||||
|
||||
@@ -209,7 +209,7 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: n
|
||||
if (!empty.ok) return empty
|
||||
const attributes = spellInlineNodeAttributes(node, directive)
|
||||
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
||||
const slot = directive.slot === undefined ? undefined : node.attrs?.[directive.slot]
|
||||
const slot = directive.textAttribute === undefined ? undefined : node.attrs?.[directive.textAttribute]
|
||||
if (slot === undefined) return success({ segments: [syntax(spellLeafDirective(node.type, attributes))] })
|
||||
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
||||
if (/[\n\r]/.test(slot)) return failure('unspellable-whitespace', `a ${node.type} content slot holds a newline no inline directive spans`, path)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { escapesLineClaim, opensBracketedAutolink, startsEntityReference, type LinePosition } from '../commonmark-grammar.ts'
|
||||
import { delimiterFlags, isWordCharacter, matchEmphasis } from '../emphasis-matching.ts'
|
||||
import {
|
||||
escapesLineClaim,
|
||||
isAsciiPunctuation,
|
||||
opensBracketedAutolink,
|
||||
opensHtmlConstruct,
|
||||
startsEntityReference,
|
||||
type LinePosition,
|
||||
} from '../commonmark-grammar.ts'
|
||||
import { opensInlineDirective } from '../directive-attributes.ts'
|
||||
|
||||
export type EmphasisRole = 'close' | 'open'
|
||||
|
||||
@@ -21,9 +29,6 @@ type EmittedRun = { canClose: boolean; canOpen: boolean; character: string; deli
|
||||
|
||||
const delimiters = ['*', '_', '`', '~']
|
||||
|
||||
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
||||
const htmlConstructs = [/^<[!?]/, /^<\/?[A-Za-z][A-Za-z0-9-]*(?:[\s/>]|$)/, /^<[^\s<>@]+@[^\s<>@]+>/]
|
||||
const inlineDirectiveOpener = /^:[a-z][A-Za-z0-9]*[[{]/
|
||||
const followsLinkText = /[([:]/
|
||||
|
||||
export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer): AssembledLine {
|
||||
@@ -211,10 +216,10 @@ function claimsCharacter(
|
||||
const rest = scan.slice(index)
|
||||
if (inBrackets && (character === '[' || character === ']')) return true
|
||||
if (character === '|') return container === 'table-cell'
|
||||
if (character === '\\') return asciiPunctuation.test(scan.charAt(index + 1))
|
||||
if (character === '\\') return isAsciiPunctuation(scan.charAt(index + 1))
|
||||
if (character === '&') return startsEntityReference(rest)
|
||||
if (character === '<') return opensBracketedAutolink(rest) || htmlConstructs.some((construct) => construct.test(rest))
|
||||
if (character === ':') return inlineDirectiveOpener.test(rest)
|
||||
if (character === '<') return opensBracketedAutolink(rest) || opensHtmlConstruct(rest)
|
||||
if (character === ':') return opensInlineDirective(rest)
|
||||
if (character === '[') return opensLink(scan, escapings, index)
|
||||
if (character === '`') return opensCodeSpan(scan, index, escaped)
|
||||
if (character === '*' || character === '_' || character === '~') return claimsEmphasis(scan, index, escaped)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user