4c - the label trim, the segment tail, the break run and the guard's spread stop re-walking
This commit is contained in:
@@ -77,3 +77,9 @@ test('accepts the JSON values an attribute may hold', () => {
|
|||||||
assert.equal(isAdfDocument({ content: [{ attrs: { a: [1, 'x', null, true, { b: 2 }] }, type: 'paragraph' }], type: 'doc', version: 1 }), true)
|
assert.equal(isAdfDocument({ content: [{ attrs: { a: [1, 'x', null, true, { b: 2 }] }, type: 'paragraph' }], type: 'doc', version: 1 }), true)
|
||||||
assert.equal(isAdfDocument({ content: [{ attrs: { a: [() => 1] }, type: 'paragraph' }], type: 'doc', version: 1 }), false)
|
assert.equal(isAdfDocument({ content: [{ attrs: { a: [() => 1] }, type: 'paragraph' }], type: 'doc', version: 1 }), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("reads a node's siblings as a walk rather than as one call's arguments", () => {
|
||||||
|
const wide = { content: [{ content: Array.from({ length: 200000 }, () => ({ type: 'rule' })), type: 'blockquote' }], type: 'doc', version: 1 }
|
||||||
|
assert.equal(isAdfDocument(wide), true)
|
||||||
|
assert.equal(fault(wide), 'accepted')
|
||||||
|
})
|
||||||
|
|||||||
+2
-2
@@ -96,7 +96,7 @@ function isNodeArray(value: readonly unknown[]): value is readonly AdfNode[] {
|
|||||||
if ('content' in node) {
|
if ('content' in node) {
|
||||||
const content = node['content']
|
const content = node['content']
|
||||||
if (!Array.isArray(content)) return false
|
if (!Array.isArray(content)) return false
|
||||||
pending.push(...content)
|
for (const child of content) pending.push(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -109,7 +109,7 @@ function nestingFault(nodes: readonly AdfNode[]): ConvertFault | undefined {
|
|||||||
if (node === undefined) continue
|
if (node === undefined) continue
|
||||||
const fault = attributesFault(nodeAttrs(node), node.type) ?? marksFault(nodeMarks(node))
|
const fault = attributesFault(nodeAttrs(node), node.type) ?? marksFault(nodeMarks(node))
|
||||||
if (fault !== undefined) return fault
|
if (fault !== undefined) return fault
|
||||||
pending.push(...nodeContent(node))
|
for (const child of nodeContent(node)) pending.push(child)
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { readEntityReference, replacementCharacter } from './entity-references.t
|
|||||||
|
|
||||||
export type LinePosition = 'first' | 'later'
|
export type LinePosition = 'first' | 'later'
|
||||||
|
|
||||||
|
// The suffix lengths of a text that spell a thematic break, and the marker each of them opens with.
|
||||||
|
export type ThematicBreakTail = { longest: number; marker: string; shortest: number }
|
||||||
|
|
||||||
type OpenHtmlBlock = { closer: RegExp | undefined; construct: string }
|
type OpenHtmlBlock = { closer: RegExp | undefined; construct: string }
|
||||||
|
|
||||||
type HtmlBlockCondition = { closer: RegExp | undefined; construct: string | undefined; interrupts: boolean; start: RegExp }
|
type HtmlBlockCondition = { closer: RegExp | undefined; construct: string | undefined; interrupts: boolean; start: RegExp }
|
||||||
@@ -52,6 +55,7 @@ const htmlBlockConditions: HtmlBlockCondition[] = [
|
|||||||
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
||||||
const atxHeadingOpener = /^(#{1,6})(?:[ \t]|$)/
|
const atxHeadingOpener = /^(#{1,6})(?:[ \t]|$)/
|
||||||
const blankLine = /^[ \t]*$/
|
const blankLine = /^[ \t]*$/
|
||||||
|
const breakMarkers = '*-_'
|
||||||
const codeFenceOpener = /^(`{3,}|~{3,})/
|
const codeFenceOpener = /^(`{3,}|~{3,})/
|
||||||
const pipeClaim = /^\|/
|
const pipeClaim = /^\|/
|
||||||
const bulletListOpener = /^[*+-](?:[ \t]|$)/
|
const bulletListOpener = /^[*+-](?:[ \t]|$)/
|
||||||
@@ -63,7 +67,6 @@ const emailLabelSource = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
|
|||||||
const emailAutolink = new RegExp(`<${emailNameSource}@${emailLabelSource}(?:\\.${emailLabelSource})*>`, 'y')
|
const emailAutolink = new RegExp(`<${emailNameSource}@${emailLabelSource}(?:\\.${emailLabelSource})*>`, 'y')
|
||||||
const orderedListOpener = /^(\d{1,9})(?:[.)])(?:[ \t]|$)/
|
const orderedListOpener = /^(\d{1,9})(?:[.)])(?:[ \t]|$)/
|
||||||
const setextUnderline = /^(=+|-+)[ \t]*$/
|
const setextUnderline = /^(=+|-+)[ \t]*$/
|
||||||
const thematicBreak = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/
|
|
||||||
const unicodeWhitespace = /[\t\n\f\r \p{Zs}]/u
|
const unicodeWhitespace = /[\t\n\f\r \p{Zs}]/u
|
||||||
|
|
||||||
export function atxHeading(line: string): { level: number; text: string } | undefined {
|
export function atxHeading(line: string): { level: number; text: string } | undefined {
|
||||||
@@ -118,7 +121,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)) || isThematicBreak(line)) return true
|
||||||
if (openingHtmlBlock(line, position === 'later') !== undefined) return true
|
if (openingHtmlBlock(line, position === 'later') !== undefined) return true
|
||||||
return position === 'later' && setextUnderline.test(line)
|
return position === 'later' && setextUnderline.test(line)
|
||||||
}
|
}
|
||||||
@@ -168,7 +171,33 @@ export function isBlankLine(line: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isThematicBreak(line: string): boolean {
|
export function isThematicBreak(line: string): boolean {
|
||||||
return thematicBreak.test(line)
|
return holdsThematicBreak(thematicBreakTail(line), line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A break runs to the line's end, so one scan of that run answers every level the list walk opens.
|
||||||
|
export function thematicBreakTail(text: string): ThematicBreakTail | undefined {
|
||||||
|
let marker: string | undefined
|
||||||
|
let markers = 0
|
||||||
|
let first = text.length
|
||||||
|
let third = text.length
|
||||||
|
let cursor = text.length
|
||||||
|
while (cursor > 0) {
|
||||||
|
const character = text.charAt(cursor - 1)
|
||||||
|
if (marker === undefined ? breakMarkers.includes(character) : character === marker) {
|
||||||
|
marker = character
|
||||||
|
markers += 1
|
||||||
|
first = cursor - 1
|
||||||
|
if (markers === 3) third = cursor - 1
|
||||||
|
} else if (!spaceOrTab(character)) break
|
||||||
|
cursor -= 1
|
||||||
|
}
|
||||||
|
if (marker === undefined || markers < 3) return undefined
|
||||||
|
return { longest: text.length - first, marker, shortest: text.length - third }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function holdsThematicBreak(tail: ThematicBreakTail | undefined, text: string): boolean {
|
||||||
|
if (tail === undefined) return false
|
||||||
|
return text.length >= tail.shortest && text.length <= tail.longest && text.charAt(0) === tail.marker
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUnicodeWhitespace(character: string): boolean {
|
export function isUnicodeWhitespace(character: string): boolean {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
|||||||
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
||||||
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
||||||
import { carriedInline } from '../opaque-carry.ts'
|
import { carriedInline } from '../opaque-carry.ts'
|
||||||
import { claimsLine, holdsNullCharacter } from '../commonmark-grammar.ts'
|
import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark-grammar.ts'
|
||||||
import { commonMarkLink, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
import { commonMarkLink, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||||
import { escapeUnbalanced, spellDestination } from '../link-syntax.ts'
|
import { escapeUnbalanced, spellDestination } from '../link-syntax.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
@@ -129,8 +129,8 @@ function carryEdges(segment: InlineSegment, leading: boolean, trailing: boolean)
|
|||||||
if (segment.escaping !== 'backslash' && segment.escaping !== 'bracketed') return [segment]
|
if (segment.escaping !== 'backslash' && segment.escaping !== 'bracketed') return [segment]
|
||||||
const head = leading ? (/^[ \t]+/.exec(segment.text)?.[0] ?? '') : ''
|
const head = leading ? (/^[ \t]+/.exec(segment.text)?.[0] ?? '') : ''
|
||||||
const body = segment.text.slice(head.length)
|
const body = segment.text.slice(head.length)
|
||||||
const tail = trailing ? (/[ \t]+$/.exec(body)?.[0] ?? '') : ''
|
const middle = trailing ? trimTrailingSpace(body) : body
|
||||||
const middle = body.slice(0, body.length - tail.length)
|
const tail = body.slice(middle.length)
|
||||||
const edges: InlineSegment[] = []
|
const edges: InlineSegment[] = []
|
||||||
if (head !== '') edges.push(carriedText(head))
|
if (head !== '') edges.push(carriedText(head))
|
||||||
if (middle !== '') edges.push({ escaping: segment.escaping, text: middle })
|
if (middle !== '') edges.push({ escaping: segment.escaping, text: middle })
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ export function readLabel(text: string, offset: number): LinkPart | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeLabel(raw: string): string {
|
export function normalizeLabel(raw: string): string {
|
||||||
return raw
|
const collapsed = raw.replace(/[ \t\n]+/g, ' ')
|
||||||
.replace(/^[ \t\n]+|[ \t\n]+$/g, '')
|
const start = collapsed.startsWith(' ') ? 1 : 0
|
||||||
.replace(/[ \t\n]+/g, ' ')
|
const end = collapsed.endsWith(' ') ? collapsed.length - 1 : collapsed.length
|
||||||
.toLowerCase()
|
return collapsed.slice(start, end).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDestination(text: string, offset: number): LinkPart | undefined {
|
export function readDestination(text: string, offset: number): LinkPart | undefined {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
claimsPipeLine,
|
claimsPipeLine,
|
||||||
closingCodeFence,
|
closingCodeFence,
|
||||||
decodeTextEscapes,
|
decodeTextEscapes,
|
||||||
|
holdsThematicBreak,
|
||||||
isBlankLine,
|
isBlankLine,
|
||||||
isThematicBreak,
|
isThematicBreak,
|
||||||
listMarker,
|
listMarker,
|
||||||
@@ -14,6 +15,8 @@ import {
|
|||||||
openingHtmlBlock,
|
openingHtmlBlock,
|
||||||
replaceNullCharacters,
|
replaceNullCharacters,
|
||||||
setextHeadingLevel,
|
setextHeadingLevel,
|
||||||
|
thematicBreakTail,
|
||||||
|
type ThematicBreakTail,
|
||||||
} from '../commonmark-grammar.ts'
|
} from '../commonmark-grammar.ts'
|
||||||
import { blockDirectiveForm } from '../block-directive-forms.ts'
|
import { blockDirectiveForm } from '../block-directive-forms.ts'
|
||||||
import { directiveEscape, malformedDirective, readDirectiveLine, spellDirectiveCloser } from '../directive-syntax.ts'
|
import { directiveEscape, malformedDirective, readDirectiveLine, spellDirectiveCloser } from '../directive-syntax.ts'
|
||||||
@@ -143,10 +146,11 @@ function blockquoteRest(opener: Line): Line | undefined {
|
|||||||
|
|
||||||
function openContainers(walk: Walk, line: Line, paragraphOpen: boolean, depth: number): { opened: boolean; rest: Line } {
|
function openContainers(walk: Walk, line: Line, paragraphOpen: boolean, depth: number): { opened: boolean; rest: Line } {
|
||||||
const unmatched = walk.stack[depth]
|
const unmatched = walk.stack[depth]
|
||||||
|
const tail = thematicBreakTail(line.text)
|
||||||
let opened = false
|
let opened = false
|
||||||
let rest = line
|
let rest = line
|
||||||
while (leadingColumns(rest) < indentedCodeColumns) {
|
while (leadingColumns(rest) < indentedCodeColumns) {
|
||||||
const start = containerStart(rest, opened ? false : paragraphOpen, opened ? undefined : unmatched, walk.position)
|
const start = containerStart(rest, tail, opened ? false : paragraphOpen, opened ? undefined : unmatched, walk.position)
|
||||||
if (start === undefined) break
|
if (start === undefined) break
|
||||||
if (!opened) closeContainers(walk, depth)
|
if (!opened) closeContainers(walk, depth)
|
||||||
opened = true
|
opened = true
|
||||||
@@ -156,11 +160,17 @@ function openContainers(walk: Walk, line: Line, paragraphOpen: boolean, depth: n
|
|||||||
return { opened, rest }
|
return { opened, rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
function containerStart(line: Line, paragraphOpen: boolean, enclosing: OpenContainer | undefined, position: SourcePosition): ContainerStart | undefined {
|
function containerStart(
|
||||||
|
line: Line,
|
||||||
|
tail: ThematicBreakTail | undefined,
|
||||||
|
paragraphOpen: boolean,
|
||||||
|
enclosing: OpenContainer | undefined,
|
||||||
|
position: SourcePosition,
|
||||||
|
): ContainerStart | undefined {
|
||||||
const opener = removeColumns(line, largestOpenerIndentation)
|
const opener = removeColumns(line, largestOpenerIndentation)
|
||||||
const blockquote = blockquoteRest(opener)
|
const blockquote = blockquoteRest(opener)
|
||||||
if (blockquote !== undefined) return { kind: 'blockquote', rest: blockquote }
|
if (blockquote !== undefined) return { kind: 'blockquote', rest: blockquote }
|
||||||
if (isThematicBreak(opener.text)) return undefined
|
if (holdsThematicBreak(tail, opener.text)) return undefined
|
||||||
return itemStart(line, opener, paragraphOpen, enclosing, position)
|
return itemStart(line, opener, paragraphOpen, enclosing, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user