Lead the code block, empty-key, carry fence, text break and carry-in-mark refusals with their fix
This commit is contained in:
@@ -23,10 +23,10 @@ export function readEmptyKeys(type: string, attributes: DirectiveAttributes, key
|
||||
for (const key of keys) {
|
||||
const spelled = rest.get(key)
|
||||
if (spelled === undefined) continue
|
||||
if (!spellsEmpty(spelled)) return { fault: unsupportedNodeShape(`the reserved key ${key} reads ${key}=${emptyValue} alone: this one spells ${key}=${spelled.spelling}`) }
|
||||
if (!spellsEmpty(spelled)) return { fault: unsupportedNodeShape(`the reserved key ${key} takes only the value ${emptyValue}: this one spells ${key}=${spelled.spelling}`) }
|
||||
empty.add(key)
|
||||
rest.delete(key)
|
||||
}
|
||||
if (empty.has('attrs') && (held || [...rest.keys()].some((key) => key !== 'marks'))) return { fault: unsupportedNodeShape(`${type} spells attrs=empty beside an attribute it holds`) }
|
||||
if (empty.has('attrs') && (held || [...rest.keys()].some((key) => key !== 'marks'))) return { fault: unsupportedNodeShape(`${type} spells attrs=empty beside another attribute, an argument or content: drop attrs=empty or the rest`) }
|
||||
return { value: { empty, rest } }
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function readCarriedBlock(type: string, body: string, depth: number): Rea
|
||||
}
|
||||
const read = readCarriedJson(body, 'two-space', largestNesting - depth, type)
|
||||
if (read.fault !== undefined || type !== '' || !infoStringCarries(read.value.type)) return read
|
||||
return { fault: unsupportedNodeShape(`the carry fence names a type its info string carries: spell it ${carryFencePrefix}${read.value.type}`) }
|
||||
return { fault: unsupportedNodeShape(`the ${carryFencePrefix} fence holds a type its info string can carry: spell the fence ${carryFencePrefix}${read.value.type} and drop type from the JSON`) }
|
||||
}
|
||||
|
||||
export function readCarriedInline(span: DirectiveSpan): Read<AdfNode> | undefined {
|
||||
|
||||
@@ -72,7 +72,7 @@ type SharedScan = Pick<Scan, 'definitions' | 'path'>
|
||||
|
||||
type SlotContent = { carry: boolean; nodes: Inline[] }
|
||||
|
||||
const carriedInMark = 'no mark spelling wraps an opaque carry or an inline node spelling marks=empty: its marks are its own'
|
||||
const carriedInMark = 'move the opaque carry, or the node spelling marks=empty, out of the mark spelling: it holds its own marks'
|
||||
const editorHighlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
||||
const hreflessLink = 'the link mark spells its href: this one spells none'
|
||||
const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content'
|
||||
@@ -247,7 +247,7 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result<
|
||||
const mark = readDirectiveMark(span.name, span.attributes, scan.path)
|
||||
if (mark !== undefined) return mark.ok ? directiveMarkPiece(scan, span.name, mark.value, slot.value, index) : mark
|
||||
const content = slot.value?.nodes
|
||||
if (content?.some(isTextBreak) === true) return failure('unsupported-node-shape', `${textBreakName} parts two text nodes: a content slot holds plain text`, scan.path)
|
||||
if (content?.some(isTextBreak) === true) return failure('unsupported-node-shape', `delete ${textBreakSpelling} from this content slot: a slot holds one text node`, scan.path)
|
||||
const node = readInlineDirectiveNode(span.name, span.attributes, content === undefined ? undefined : adfNodes(content), scan.path)
|
||||
if (!node.ok) return node
|
||||
return success(node.value.marks === undefined ? { kind: 'nodes', nodes: [node.value] } : { kind: 'carry', node: node.value })
|
||||
@@ -488,7 +488,7 @@ function closeImage(scan: Scan, at: number, inner: readonly Piece[], definition:
|
||||
function imageAlt(inner: readonly Piece[], scan: Scan): Result<string> {
|
||||
const nodes = resolveNodes(inner, scan, false)
|
||||
if (!nodes.ok) return nodes
|
||||
if (nodes.value.some(isTextBreak)) return failure('unsupported-node-shape', `${textBreakName} parts two text nodes: an image description holds plain text`, scan.path)
|
||||
if (nodes.value.some(isTextBreak)) return failure('unsupported-node-shape', `delete ${textBreakSpelling} from this image description: the description reads as plain alt text`, scan.path)
|
||||
return success(adfNodes(nodes.value).map(altText).join(''))
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ test('reads the codeBlock directive body as the node content, the info string it
|
||||
|
||||
test('names the slot a codeBlock spells its language outside of', () => {
|
||||
const slot = 'unsupported-node-shape: codeBlock spells its language in the fence info string, or in the attribute where no info string carries it back'
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n')), "unsupported-node-shape: move language=rust to the fence's info string: a fence carries the language wherever its info string can")
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust}\n```sql\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```adf:x\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```a\\b\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||
@@ -376,7 +376,7 @@ test('reads the carry fence back to the node its info string names and its JSON
|
||||
content(markdownToAdf('```adf:blockCard\n{\n "type": "blockCard"\n}\n```\n')),
|
||||
'unsupported-node-shape: the adf:blockCard fence names its node\'s type: this JSON holds a type as well; !adf:codeBlock {language="adf:blockCard"} around a bare fence keeps it a code block',
|
||||
)
|
||||
assert.equal(content(markdownToAdf('```adf:\n{\n "type": "blockCard"\n}\n```\n')), 'unsupported-node-shape: the carry fence names a type its info string carries: spell it adf:blockCard')
|
||||
assert.equal(content(markdownToAdf('```adf:\n{\n "type": "blockCard"\n}\n```\n')), 'unsupported-node-shape: the adf: fence holds a type its info string can carry: spell the fence adf:blockCard and drop type from the JSON')
|
||||
assert.equal(content(markdownToAdf('```adf:\n{}\n```\n')), "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node; !adf:codeBlock {language=\"adf:\"} around a bare fence keeps it a code block")
|
||||
const unnamed = 'unsupported-node-shape: the carry fence names a type no info string carries back: spell it adf: with the type in the JSON; !adf:codeBlock {language="adf:\\\\"} around a bare fence keeps it a code block'
|
||||
assert.equal(content(markdownToAdf('```adf:\\\\\n{}\n```\n')), unnamed)
|
||||
@@ -441,7 +441,7 @@ test('names the number no JSON spelling carries in an opaque carry', () => {
|
||||
})
|
||||
|
||||
test('names the mark spelling no opaque carry sits inside', () => {
|
||||
const named = 'unsupported-node-shape: no mark spelling wraps an opaque carry or an inline node spelling marks=empty: its marks are its own'
|
||||
const named = 'unsupported-node-shape: move the opaque carry, or the node spelling marks=empty, out of the mark spelling: it holds its own marks'
|
||||
assert.equal(content(markdownToAdf(`_a ${carried} b_\n`)), named)
|
||||
assert.equal(content(markdownToAdf(`**${carried}**\n`)), named)
|
||||
assert.equal(content(markdownToAdf(`~~a ${carried}~~\n`)), named)
|
||||
@@ -1095,8 +1095,8 @@ test('reads the text break only between two text nodes CommonMark joins, buildin
|
||||
for (const markdown of ['!adf:textBreak{}a\n', 'a!adf:textBreak{}\n', 'a!adf:textBreak{}!adf:textBreak{}b\n', '**a**!adf:textBreak{}b\n', '[!adf:textBreak{}](/u)\n', `a!adf:textBreak{}${carried}\n`]) {
|
||||
assert.equal(content(markdownToAdf(markdown)), parts, markdown)
|
||||
}
|
||||
assert.equal(content(markdownToAdf('!adf:status[a!adf:textBreak{}b]\n')), 'unsupported-node-shape: textBreak parts two text nodes: a content slot holds plain text')
|
||||
assert.equal(content(markdownToAdf('\n')), 'unsupported-node-shape: textBreak parts two text nodes: an image description holds plain text')
|
||||
assert.equal(content(markdownToAdf('!adf:status[a!adf:textBreak{}b]\n')), 'unsupported-node-shape: delete !adf:textBreak{} from this content slot: a slot holds one text node')
|
||||
assert.equal(content(markdownToAdf('\n')), 'unsupported-node-shape: delete !adf:textBreak{} from this image description: the description reads as plain alt text')
|
||||
assert.equal(content(markdownToAdf('a!adf:textBreak{x=y}b\n')), 'unsupported-node-shape: textBreak spells the bare leaf form, !adf:textBreak{}: this one spells more')
|
||||
assert.deepEqual(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"textBreak\\"}"}\n')), [{ content: [{ type: 'textBreak' }], type: 'paragraph' }])
|
||||
})
|
||||
@@ -1110,4 +1110,12 @@ test('leads a refusal ordinary editing meets with the edit that fixes it', () =>
|
||||
content(markdownToAdf('!adf:codeBlock\n```\na\n```\n```\n```\n!adf:/codeBlock\n')),
|
||||
'unsupported-node-shape: delete the empty fence: a fence beside another holds code, and this one holds none',
|
||||
)
|
||||
assert.equal(
|
||||
content(markdownToAdf('!adf:codeBlock {attrs=empty}\n```js\na\n```\n```js\nb\n```\n!adf:/codeBlock\n')),
|
||||
"unsupported-node-shape: remove attrs=empty to give the codeBlock the fence's language: attrs=empty holds no language, and this fence names one",
|
||||
)
|
||||
assert.equal(
|
||||
content(markdownToAdf('!adf:panel info {attrs=empty}\nText.\n!adf:/panel\n')),
|
||||
'unsupported-node-shape: panel spells attrs=empty beside another attribute, an argument or content: drop attrs=empty or the rest',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -256,10 +256,15 @@ function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: Conver
|
||||
const attribute = nodeAttrs(node)['language']
|
||||
const fromFence = first.language !== ''
|
||||
const slot = languageSlot(fromFence ? first.language : attribute)
|
||||
if (!fromFence && slot.kind === 'fence') {
|
||||
return failure('unsupported-node-shape', `move language=${slot.info} to the fence's info string: a fence carries the language wherever its info string can`, path)
|
||||
}
|
||||
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
||||
return failure('unsupported-node-shape', `${node.type} spells its language in the fence info string, or in the attribute where no info string carries it back`, path)
|
||||
}
|
||||
if (fromFence && emptyKeys(node).includes('attrs')) return failure('unsupported-node-shape', `${node.type} spells attrs=empty beside an attribute it holds`, path)
|
||||
if (fromFence && emptyKeys(node).includes('attrs')) {
|
||||
return failure('unsupported-node-shape', `remove attrs=empty to give the ${node.type} the fence's language: attrs=empty holds no language, and this fence names one`, path)
|
||||
}
|
||||
const spelled = fromFence ? { ...node, attrs: { ...node.attrs, language: first.language } } : node
|
||||
return success(withContent(spelled, first.text === '' ? [] : fences.map((fence): AdfNode => ({ text: fence.text, type: 'text' }))))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user