Answer the architecture review: one table for the HTML block start, and one code-span reader
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit is contained in:
@@ -54,9 +54,9 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
|
|||||||
`dependencies` is empty. A runtime dependency enters only through a decision entry here stating
|
`dependencies` is empty. A runtime dependency enters only through a decision entry here stating
|
||||||
why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the
|
why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the
|
||||||
CommonMark and HTML parsers are written in this repo. A table a standard fixes is data rather than
|
CommonMark and HTML parsers are written in this repo. A table a standard fixes is data rather than
|
||||||
a dependency: HTML5's 2125 semicolon-terminated character references ship packed in
|
a dependency: HTML5's 2125 semicolon-terminated character references ship packed in their own
|
||||||
`markdown/entity-references.ts`, so entity decoding is complete without one. `devDependencies`:
|
module, so entity decoding is complete without one. `devDependencies`: few, each earning its keep;
|
||||||
few, each earning its keep; they never reach a consumer.
|
they never reach a consumer.
|
||||||
|
|
||||||
## 6. The package contract
|
## 6. The package contract
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,24 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "paragraph"
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "<div opens no HTML block",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "<?php opens none either",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
|
|||||||
@@ -7,3 +7,7 @@
|
|||||||
snake_case_name
|
snake_case_name
|
||||||
|
|
||||||
\*not emphasis\*
|
\*not emphasis\*
|
||||||
|
|
||||||
|
\<div opens no HTML block
|
||||||
|
|
||||||
|
\<?php opens none either
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
export function backtickRun(text: string, index: number): number {
|
||||||
|
let length = 0
|
||||||
|
while (text.charAt(index + length) === '`') length += 1
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where the run of exactly `opener` backticks closing a code span begins, `undefined` where none does.
|
||||||
|
export function closingBacktickRun(text: string, from: number, opener: number): number | undefined {
|
||||||
|
let cursor = from
|
||||||
|
while (cursor < text.length) {
|
||||||
|
const run = backtickRun(text, cursor)
|
||||||
|
if (run === opener) return cursor
|
||||||
|
cursor += run === 0 ? 1 : run
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function fencedCodeBlock(info: string, body: string): string {
|
export function fencedCodeBlock(info: string, body: string): string {
|
||||||
const fence = '`'.repeat(Math.max(3, longestBacktickRun(body) + 1))
|
const fence = '`'.repeat(Math.max(3, longestBacktickRun(body) + 1))
|
||||||
return body === '' ? `${fence}${info}\n${fence}` : `${fence}${info}\n${body}\n${fence}`
|
return body === '' ? `${fence}${info}\n${fence}` : `${fence}${info}\n${body}\n${fence}`
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ export type HtmlConstruct = { length: number; name: string }
|
|||||||
|
|
||||||
export type LinePosition = 'first' | 'later'
|
export type LinePosition = 'first' | 'later'
|
||||||
|
|
||||||
|
export type OpenHtmlBlock = { closer: RegExp | undefined; construct: string }
|
||||||
|
|
||||||
|
type HtmlBlockCondition = { closer: RegExp | undefined; construct: string | undefined; interrupts: boolean; start: RegExp }
|
||||||
|
|
||||||
export const htmlConstructNames = {
|
export const htmlConstructNames = {
|
||||||
cdata: 'a CDATA section',
|
cdata: 'a CDATA section',
|
||||||
comment: 'an HTML comment',
|
comment: 'an HTML comment',
|
||||||
@@ -32,6 +36,19 @@ const inlineHtmlConstructs = [
|
|||||||
{ name: htmlConstructNames.declaration, pattern: /^<![A-Za-z][^>]*>/ },
|
{ name: htmlConstructNames.declaration, pattern: /^<![A-Za-z][^>]*>/ },
|
||||||
{ name: htmlConstructNames.processingInstruction, pattern: /^<\?[\s\S]*?\?>/ },
|
{ name: htmlConstructNames.processingInstruction, pattern: /^<\?[\s\S]*?\?>/ },
|
||||||
]
|
]
|
||||||
|
// CommonMark 0.31.2, HTML blocks: the tag names start condition 6 lists.
|
||||||
|
const blockTagNames =
|
||||||
|
'address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul'
|
||||||
|
const completeTag = new RegExp(`^${htmlTagSource}[ \\t]*$`)
|
||||||
|
const htmlBlockConditions: HtmlBlockCondition[] = [
|
||||||
|
{ closer: /<\/(?:pre|script|style|textarea)>/i, construct: undefined, interrupts: true, start: /^<(?:pre|script|style|textarea)(?:[ \t>]|$)/i },
|
||||||
|
{ closer: /-->/, construct: htmlConstructNames.comment, interrupts: true, start: /^<!--/ },
|
||||||
|
{ closer: /\?>/, construct: htmlConstructNames.processingInstruction, interrupts: true, start: /^<\?/ },
|
||||||
|
{ closer: />/, construct: htmlConstructNames.declaration, interrupts: true, start: /^<![A-Za-z]/ },
|
||||||
|
{ closer: /\]\]>/, construct: htmlConstructNames.cdata, interrupts: true, start: /^<!\[CDATA\[/ },
|
||||||
|
{ closer: undefined, construct: undefined, interrupts: true, start: new RegExp(`^</?(?:${blockTagNames})(?:[ \\t>]|/>|$)`, 'i') },
|
||||||
|
{ closer: undefined, construct: undefined, interrupts: false, start: completeTag },
|
||||||
|
]
|
||||||
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
||||||
const atxHeadingOpener = /^(#{1,6})(?:[ \t]|$)/
|
const atxHeadingOpener = /^(#{1,6})(?:[ \t]|$)/
|
||||||
const codeFenceOpener = /^(`{3,}|~{3,})/
|
const codeFenceOpener = /^(`{3,}|~{3,})/
|
||||||
@@ -53,6 +70,13 @@ export function atxHeading(line: string): { level: number; text: string } | unde
|
|||||||
return { level: hashes.length, text: trimSpace(text.replace(/(?:^|(?<=[ \t]))#+$/, '')) }
|
return { level: hashes.length, text: trimSpace(text.replace(/(?:^|(?<=[ \t]))#+$/, '')) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The character a backslash escapes at `index`, `undefined` where the backslash is literal text.
|
||||||
|
export function backslashEscape(text: string, index: number): string | undefined {
|
||||||
|
if (text.charAt(index) !== '\\') return undefined
|
||||||
|
const escaped = text.charAt(index + 1)
|
||||||
|
return isAsciiPunctuation(escaped) ? escaped : undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function claimsDirectiveLine(line: string): boolean {
|
export function claimsDirectiveLine(line: string): boolean {
|
||||||
return directiveClaim.test(line)
|
return directiveClaim.test(line)
|
||||||
}
|
}
|
||||||
@@ -71,7 +95,6 @@ export function closingCodeFence(line: string, marker: string): boolean {
|
|||||||
return /^[ \t]*$/.test(line.slice(closing.length))
|
return /^[ \t]*$/.test(line.slice(closing.length))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text where only backslash escapes and entity references are processed: an info string, a link title.
|
|
||||||
export function decodeTextEscapes(text: string): string {
|
export function decodeTextEscapes(text: string): string {
|
||||||
let decoded = ''
|
let decoded = ''
|
||||||
let index = 0
|
let index = 0
|
||||||
@@ -97,6 +120,7 @@ export function decodeTextEscapes(text: string): string {
|
|||||||
export function escapesLineClaim(line: string, offset: number, position: LinePosition): boolean {
|
export function escapesLineClaim(line: string, offset: number, position: LinePosition): boolean {
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
if (firstCharacterOpeners.some((opener) => opener.test(line)) || thematicBreak.test(line)) return true
|
if (firstCharacterOpeners.some((opener) => opener.test(line)) || thematicBreak.test(line)) return true
|
||||||
|
if (openingHtmlBlock(line, position === 'later') !== undefined) return true
|
||||||
return position === 'later' && setextUnderline.test(line)
|
return position === 'later' && setextUnderline.test(line)
|
||||||
}
|
}
|
||||||
const digits = orderedListOpener.exec(line)?.[1]
|
const digits = orderedListOpener.exec(line)?.[1]
|
||||||
@@ -161,6 +185,14 @@ export function openingCodeFence(line: string): { info: string; marker: string }
|
|||||||
return marker.startsWith('`') && info.includes('`') ? undefined : { info, marker }
|
return marker.startsWith('`') && info.includes('`') ? undefined : { info, marker }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openingHtmlBlock(line: string, interrupting: boolean): OpenHtmlBlock | undefined {
|
||||||
|
for (const condition of htmlBlockConditions) {
|
||||||
|
if ((interrupting && !condition.interrupts) || !condition.start.test(line)) continue
|
||||||
|
return { closer: condition.closer, construct: condition.construct ?? htmlTagName(line) }
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function opensBracketedAutolink(text: string): boolean {
|
export function opensBracketedAutolink(text: string): boolean {
|
||||||
return bracketedAutolink.test(text)
|
return bracketedAutolink.test(text)
|
||||||
}
|
}
|
||||||
@@ -178,9 +210,3 @@ export function setextHeadingLevel(line: string): number | undefined {
|
|||||||
export function trimSpace(text: string): string {
|
export function trimSpace(text: string): string {
|
||||||
return text.replace(/^[ \t]+|[ \t]+$/g, '')
|
return text.replace(/^[ \t]+|[ \t]+$/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function backslashEscape(text: string, index: number): string | undefined {
|
|
||||||
if (text.charAt(index) !== '\\') return undefined
|
|
||||||
const escaped = text.charAt(index + 1)
|
|
||||||
return isAsciiPunctuation(escaped) ? escaped : undefined
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -202,11 +202,23 @@ test('spells a heading level no ATX heading fits as a directive', () => {
|
|||||||
test('escapes only text that would otherwise open a construct', () => {
|
test('escapes only text that would otherwise open a construct', () => {
|
||||||
const emitted = (text: string): string => markdown(adfToMarkdown(document(paragraph({ text, type: 'text' }))))
|
const emitted = (text: string): string => markdown(adfToMarkdown(document(paragraph({ text, type: 'text' }))))
|
||||||
assert.equal(emitted('<div>'), '\\<div>\n')
|
assert.equal(emitted('<div>'), '\\<div>\n')
|
||||||
|
assert.equal(emitted('<div'), '\\<div\n')
|
||||||
|
assert.equal(emitted('<div and more'), '\\<div and more\n')
|
||||||
|
assert.equal(emitted('<pre'), '\\<pre\n')
|
||||||
|
assert.equal(emitted('<!x'), '\\<!x\n')
|
||||||
|
assert.equal(emitted('<!-- x'), '\\<!-- x\n')
|
||||||
|
assert.equal(emitted('<?php'), '\\<?php\n')
|
||||||
|
assert.equal(emitted('<![CDATA[x'), '\\<![CDATA[x\n')
|
||||||
|
assert.equal(emitted('<span'), '<span\n')
|
||||||
assert.equal(emitted('a < b'), 'a < b\n')
|
assert.equal(emitted('a < b'), 'a < b\n')
|
||||||
assert.equal(emitted('& & x'), '\\& & x\n')
|
assert.equal(emitted('& & x'), '\\& & x\n')
|
||||||
assert.equal(emitted('¬areference; x'), '¬areference; x\n')
|
assert.equal(emitted('¬areference; x'), '¬areference; x\n')
|
||||||
assert.equal(emitted('a <b@c.d> e'), 'a \\<b@c.d> e\n')
|
assert.equal(emitted('a <b@c.d> e'), 'a \\<b@c.d> e\n')
|
||||||
assert.equal(emitted('a <b 2'), 'a <b 2\n')
|
assert.equal(emitted('a <b 2'), 'a <b 2\n')
|
||||||
|
assert.equal(emitted('a <div b'), 'a <div b\n')
|
||||||
|
const later = paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, { text: '<div', type: 'text' })
|
||||||
|
assert.equal(markdown(adfToMarkdown(document(later))), 'a\\\n\\<div\n')
|
||||||
|
assert.equal(markdown(adfToMarkdown(document({ content: [paragraph({ text: '<!-- x', type: 'text' })], type: 'blockquote' }))), '> \\<!-- x\n')
|
||||||
assert.equal(emitted('| a | b |'), '\\| a | b |\n')
|
assert.equal(emitted('| a | b |'), '\\| a | b |\n')
|
||||||
assert.equal(emitted(':mention[@x]{id=1}'), '\\:mention[@x]{id=1}\n')
|
assert.equal(emitted(':mention[@x]{id=1}'), '\\:mention[@x]{id=1}\n')
|
||||||
assert.equal(emitted(':::panel info'), '\\:::panel info\n')
|
assert.equal(emitted(':::panel info'), '\\:::panel info\n')
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
|
||||||
import { delimiterFlags, isWordCharacter, matchEmphasis } from '../emphasis-matching.ts'
|
import { delimiterFlags, isWordCharacter, matchEmphasis } from '../emphasis-matching.ts'
|
||||||
import { escapesLineClaim, inlineHtmlConstruct, isAsciiPunctuation, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts'
|
import { escapesLineClaim, inlineHtmlConstruct, isAsciiPunctuation, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts'
|
||||||
import { opensInlineDirective } from '../directive-attributes.ts'
|
import { opensInlineDirective } from '../directive-attributes.ts'
|
||||||
@@ -232,8 +233,8 @@ function opensLink(scan: string, escapings: readonly (InlineEscaping | undefined
|
|||||||
|
|
||||||
function opensCodeSpan(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
function opensCodeSpan(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
||||||
if (!startsRun(scan, index, escaped)) return false
|
if (!startsRun(scan, index, escaped)) return false
|
||||||
const length = runLength(scan, index)
|
const opener = backtickRun(scan, index)
|
||||||
return new RegExp('(?<!`)`{' + length + '}(?!`)').test(scan.slice(index + length))
|
return closingBacktickRun(scan, index + opener, opener) !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimsEmphasis(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
function claimsEmphasis(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
listMarker,
|
listMarker,
|
||||||
markerInterruptsParagraph,
|
markerInterruptsParagraph,
|
||||||
openingCodeFence,
|
openingCodeFence,
|
||||||
|
openingHtmlBlock,
|
||||||
setextHeadingLevel,
|
setextHeadingLevel,
|
||||||
} from '../commonmark-grammar.ts'
|
} from '../commonmark-grammar.ts'
|
||||||
import { openingHtmlBlock } from './html-blocks.ts'
|
|
||||||
import { readLinkDefinitions } from './link-reference-definitions.ts'
|
import { readLinkDefinitions } from './link-reference-definitions.ts'
|
||||||
|
|
||||||
export type ClaimedConstruct = 'directive' | 'pipe-table'
|
export type ClaimedConstruct = 'directive' | 'pipe-table'
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { htmlConstructNames, htmlTagName, htmlTagSource } from '../commonmark-grammar.ts'
|
|
||||||
|
|
||||||
export type OpenHtmlBlock = { closer: RegExp | undefined; construct: string }
|
|
||||||
|
|
||||||
type HtmlBlockCondition = { closer: RegExp | undefined; construct: string | undefined; interrupts: boolean; start: RegExp }
|
|
||||||
|
|
||||||
// CommonMark 0.31.2, HTML blocks: the tag names start condition 6 lists.
|
|
||||||
const blockTagNames =
|
|
||||||
'address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul'
|
|
||||||
const completeTag = new RegExp(`^${htmlTagSource}[ \\t]*$`)
|
|
||||||
|
|
||||||
const conditions: HtmlBlockCondition[] = [
|
|
||||||
{ closer: /<\/(?:pre|script|style|textarea)>/i, construct: undefined, interrupts: true, start: /^<(?:pre|script|style|textarea)(?:[ \t>]|$)/i },
|
|
||||||
{ closer: /-->/, construct: htmlConstructNames.comment, interrupts: true, start: /^<!--/ },
|
|
||||||
{ closer: /\?>/, construct: htmlConstructNames.processingInstruction, interrupts: true, start: /^<\?/ },
|
|
||||||
{ closer: />/, construct: htmlConstructNames.declaration, interrupts: true, start: /^<![A-Za-z]/ },
|
|
||||||
{ closer: /\]\]>/, construct: htmlConstructNames.cdata, interrupts: true, start: /^<!\[CDATA\[/ },
|
|
||||||
{ closer: undefined, construct: undefined, interrupts: true, start: new RegExp(`^</?(?:${blockTagNames})(?:[ \\t>]|/>|$)`, 'i') },
|
|
||||||
{ closer: undefined, construct: undefined, interrupts: false, start: completeTag },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function openingHtmlBlock(line: string, interrupting: boolean): OpenHtmlBlock | undefined {
|
|
||||||
for (const condition of conditions) {
|
|
||||||
if ((interrupting && !condition.interrupts) || !condition.start.test(line)) continue
|
|
||||||
return { closer: condition.closer, construct: condition.construct ?? htmlTagName(line) }
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
@@ -1,49 +1,44 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import { decodeTextEscapes, inlineHtmlConstruct } from '../commonmark-grammar.ts'
|
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct } from '../commonmark-grammar.ts'
|
||||||
|
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { readEntityReference } from '../entity-references.ts'
|
import { readEntityReference } from '../entity-references.ts'
|
||||||
|
|
||||||
type CodeSpan = { end: number; text: string }
|
// `text` holds the decoded content of the text node being built; `start` where its undecoded tail begins.
|
||||||
|
type Run = { nodes: AdfNode[]; start: number; text: string }
|
||||||
|
|
||||||
const hardBreakSpaces = / {2,}$/
|
const hardBreakSpaces = / {2,}$/
|
||||||
const trailingSpace = /[ \t]+$/
|
const trailingSpace = /[ \t]+$/
|
||||||
|
|
||||||
export function parseInlineContent(source: string, path: ConvertErrorPath): Result<AdfNode[]> {
|
export function parseInlineContent(source: string, path: ConvertErrorPath): Result<AdfNode[]> {
|
||||||
const nodes: AdfNode[] = []
|
const run: Run = { nodes: [], start: 0, text: '' }
|
||||||
let text = ''
|
|
||||||
let runStart = 0
|
|
||||||
let index = 0
|
let index = 0
|
||||||
while (index < source.length) {
|
while (index < source.length) {
|
||||||
const character = source.charAt(index)
|
const character = source.charAt(index)
|
||||||
if (character === '\\' && source.charAt(index + 1) === '\n') {
|
if (character === '\\' && source.charAt(index + 1) === '\n') {
|
||||||
// CommonMark strips the spaces the two-space break is spelled with, and keeps those before a backslash.
|
// CommonMark strips the spaces the two-space break is spelled with, and keeps those before a backslash.
|
||||||
text = pushText(nodes, text + decodeTextEscapes(source.slice(runStart, index)))
|
takeRun(run, source, index, index + 2, false)
|
||||||
nodes.push({ type: 'hardBreak' })
|
pushNode(run, { type: 'hardBreak' })
|
||||||
index += 2
|
index += 2
|
||||||
runStart = index
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (character === '\n') {
|
if (character === '\n') {
|
||||||
const run = source.slice(runStart, index)
|
const hard = hardBreakSpaces.test(source.slice(run.start, index))
|
||||||
text += decodeTextEscapes(run.replace(trailingSpace, ''))
|
takeRun(run, source, index, index + 1, true)
|
||||||
if (hardBreakSpaces.test(run)) {
|
if (hard) pushNode(run, { type: 'hardBreak' })
|
||||||
text = pushText(nodes, text)
|
else run.text += ' '
|
||||||
nodes.push({ type: 'hardBreak' })
|
|
||||||
} else text += ' '
|
|
||||||
index += 1
|
index += 1
|
||||||
runStart = index
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (character === '`') {
|
if (character === '`') {
|
||||||
const span = readCodeSpan(source, index)
|
const span = readCodeSpan(source, index)
|
||||||
if (span !== undefined) {
|
if (span === undefined) {
|
||||||
text = pushText(nodes, text + decodeTextEscapes(source.slice(runStart, index)))
|
index += backtickRun(source, index)
|
||||||
nodes.push({ marks: [{ type: 'code' }], text: span.text, type: 'text' })
|
|
||||||
index = span.end
|
|
||||||
runStart = index
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
index += backtickRun(source, index)
|
takeRun(run, source, index, span.end, false)
|
||||||
|
pushNode(run, { marks: [{ type: 'code' }], text: span.text, type: 'text' })
|
||||||
|
index = span.end
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (character === '<') {
|
if (character === '<') {
|
||||||
@@ -54,36 +49,34 @@ export function parseInlineContent(source: string, path: ConvertErrorPath): Resu
|
|||||||
index += readEntityReference(source.slice(index))?.length ?? 1
|
index += readEntityReference(source.slice(index))?.length ?? 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
index += character === '\\' ? 2 : 1
|
index += backslashEscape(source, index) === undefined ? 1 : 2
|
||||||
}
|
}
|
||||||
pushText(nodes, text + decodeTextEscapes(source.slice(runStart).replace(trailingSpace, '')))
|
takeRun(run, source, source.length, source.length, true)
|
||||||
return success(nodes)
|
pushText(run)
|
||||||
|
return success(run.nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushText(nodes: AdfNode[], text: string): string {
|
function takeRun(run: Run, source: string, end: number, resume: number, strip: boolean): void {
|
||||||
if (text !== '') nodes.push({ text, type: 'text' })
|
const raw = source.slice(run.start, end)
|
||||||
return ''
|
run.text += decodeTextEscapes(strip ? raw.replace(trailingSpace, '') : raw)
|
||||||
|
run.start = resume
|
||||||
}
|
}
|
||||||
|
|
||||||
function backtickRun(source: string, index: number): number {
|
function pushText(run: Run): void {
|
||||||
let length = 0
|
if (run.text !== '') run.nodes.push({ text: run.text, type: 'text' })
|
||||||
while (source.charAt(index + length) === '`') length += 1
|
run.text = ''
|
||||||
return length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCodeSpan(source: string, index: number): CodeSpan | undefined {
|
function pushNode(run: Run, node: AdfNode): void {
|
||||||
|
pushText(run)
|
||||||
|
run.nodes.push(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCodeSpan(source: string, index: number): { end: number; text: string } | undefined {
|
||||||
const opener = backtickRun(source, index)
|
const opener = backtickRun(source, index)
|
||||||
let cursor = index + opener
|
const closer = closingBacktickRun(source, index + opener, opener)
|
||||||
while (cursor < source.length) {
|
if (closer === undefined) return undefined
|
||||||
if (source.charAt(cursor) !== '`') {
|
return { end: closer + opener, text: codeSpanText(source.slice(index + opener, closer)) }
|
||||||
cursor += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const closer = backtickRun(source, cursor)
|
|
||||||
if (closer === opener) return { end: cursor + closer, text: codeSpanText(source.slice(index + opener, cursor)) }
|
|
||||||
cursor += closer
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function codeSpanText(content: string): string {
|
function codeSpanText(content: string): string {
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ test('refuses the raw HTML no element mapping carries', () => {
|
|||||||
assert.equal(code(markdownToAdf('<![CDATA[x]]>\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf('<![CDATA[x]]>\n')), 'unmappable-html')
|
||||||
assert.equal(code(markdownToAdf('<pre>\nx\n</pre>\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf('<pre>\nx\n</pre>\n')), 'unmappable-html')
|
||||||
assert.equal(code(markdownToAdf('<span foo="bar">\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf('<span foo="bar">\n')), 'unmappable-html')
|
||||||
|
assert.equal(code(markdownToAdf('<div\n')), 'unmappable-html')
|
||||||
|
assert.equal(code(markdownToAdf('<?php\n')), 'unmappable-html')
|
||||||
assert.deepEqual(path(markdownToAdf('Part.\n\n<div>\n')), ['content', 1])
|
assert.deepEqual(path(markdownToAdf('Part.\n\n<div>\n')), ['content', 1])
|
||||||
assert.equal(code(markdownToAdf('<div>\nx\n\n:::\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf('<div>\nx\n\n:::\n')), 'unmappable-html')
|
||||||
assert.equal(code(markdownToAdf('<div>\n- x\n</div>\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf('<div>\n- x\n</div>\n')), 'unmappable-html')
|
||||||
@@ -257,6 +259,7 @@ test('decodes the entity references HTML5 names, and the numeric ones', () => {
|
|||||||
assert.deepEqual(content(markdownToAdf('� � �\n')), [paragraph('\ufffd \ufffd \ufffd')])
|
assert.deepEqual(content(markdownToAdf('� � �\n')), [paragraph('\ufffd \ufffd \ufffd')])
|
||||||
assert.deepEqual(content(markdownToAdf('&zzz; & &#; &\n')), [paragraph('&zzz; & &#; &')])
|
assert.deepEqual(content(markdownToAdf('&zzz; & &#; &\n')), [paragraph('&zzz; & &#; &')])
|
||||||
assert.deepEqual(content(markdownToAdf('`not code`\n')), [paragraph('`not code`')])
|
assert.deepEqual(content(markdownToAdf('`not code`\n')), [paragraph('`not code`')])
|
||||||
|
assert.deepEqual(content(markdownToAdf('a	b
c d𝔸e|f\n')), [paragraph('a\tb\nc d\u{1d538}e|f')])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('reads a code span, its content literal', () => {
|
test('reads a code span, its content literal', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user