Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ the inline content.</p>
<code><strong><a href="#user-content-markdownconfig.parseinline">parseInline</a></strong>&#8288;?: readonly <a href="#user-content-inlineparser">InlineParser</a>[]</code></dt>

<dd><p>Define new <a href="#user-content-inlineparser">inline parsing</a> logic.</p>
</dd><dt id="user-content-markdownconfig.preresolvedelimiters">
<code><strong><a href="#user-content-markdownconfig.preresolvedelimiters">preResolveDelimiters</a></strong>&#8288;?: readonly (fn(<a id="user-content-markdownconfig.preresolvedelimiters^ctx" href="#user-content-markdownconfig.preresolvedelimiters^ctx">ctx</a>: <a href="#user-content-preresolvecontext">PreResolveContext</a>))[]</code></dt>

<dd><p>Hooks called before standard delimiter resolution. Each hook
receives a <a href="#user-content-preresolvecontext">PreResolveContext</a> and can
inspect delimiters and add elements or mark delimiters as resolved.</p>
</dd><dt id="user-content-markdownconfig.remove">
<code><strong><a href="#user-content-markdownconfig.remove">remove</a></strong>&#8288;?: readonly <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>[]</code></dt>

Expand All @@ -96,12 +102,57 @@ the inline content.</p>
parser</a>) to this parser.</p>
</dd></dl>

</dd>
</dl>
<dl>
<dt id="user-content-preresolvecontext">
<h4>
<code>interface</code>
<a href="#user-content-preresolvecontext">PreResolveContext</a></h4>
</dt>

<dd><p>Context object passed to pre-resolve delimiter hooks. Provides
an immutable view of pending delimiters and methods to mark them
as resolved or add elements.</p>
<dl><dt id="user-content-preresolvecontext.delimiters">
<code><strong><a href="#user-content-preresolvecontext.delimiters">delimiters</a></strong>: readonly {type: <a href="#user-content-delimitertype">DelimiterType</a>, from: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>, to: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>, side: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>}[]</code></dt>

<dd><p>Immutable list of pending delimiters. Each delimiter has a type,
position range (from/to), and side flags (1=open, 2=close, 3=both).</p>
</dd><dt id="user-content-preresolvecontext.blockend">
<code><strong><a href="#user-content-preresolvecontext.blockend">blockEnd</a></strong>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a></code></dt>

<dd><p>End position of the current inline section.</p>
</dd><dt id="user-content-preresolvecontext.parser">
<code><strong><a href="#user-content-preresolvecontext.parser">parser</a></strong>: <a href="#user-content-markdownparser">MarkdownParser</a></code></dt>

<dd><p>The parser being used.</p>
</dd><dt id="user-content-preresolvecontext.markresolved">
<code><strong><a href="#user-content-preresolvecontext.markresolved">markResolved</a></strong>(<a id="user-content-preresolvecontext.markresolved^index" href="#user-content-preresolvecontext.markresolved^index">index</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>)</code></dt>

<dd><p>Mark the delimiter at the given index as resolved (it will be
skipped by standard resolution).</p>
</dd><dt id="user-content-preresolvecontext.addelement">
<code><strong><a href="#user-content-preresolvecontext.addelement">addElement</a></strong>(<a id="user-content-preresolvecontext.addelement^element" href="#user-content-preresolvecontext.addelement^element">element</a>: <a href="#user-content-element">Element</a>)</code></dt>

<dd><p>Add an element to the output.</p>
</dd><dt id="user-content-preresolvecontext.elt">
<code><strong><a href="#user-content-preresolvecontext.elt">elt</a></strong>(<a id="user-content-preresolvecontext.elt^type" href="#user-content-preresolvecontext.elt^type">type</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>, <a id="user-content-preresolvecontext.elt^from" href="#user-content-preresolvecontext.elt^from">from</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>, <a id="user-content-preresolvecontext.elt^to" href="#user-content-preresolvecontext.elt^to">to</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>, <a id="user-content-preresolvecontext.elt^children" href="#user-content-preresolvecontext.elt^children">children</a>&#8288;?: readonly <a href="#user-content-element">Element</a>[]) → <a href="#user-content-element">Element</a></code></dt>

<dd><p>Create an element.</p>
</dd><dt id="user-content-preresolvecontext.slice">
<code><strong><a href="#user-content-preresolvecontext.slice">slice</a></strong>(<a id="user-content-preresolvecontext.slice^from" href="#user-content-preresolvecontext.slice^from">from</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>, <a id="user-content-preresolvecontext.slice^to" href="#user-content-preresolvecontext.slice^to">to</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>) → <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String">string</a></code></dt>

<dd><p>Get a substring of the inline section using document-relative positions.</p>
</dd></dl>

</dd>
</dl>
<dl>
<dt id="user-content-markdownextension">
<code>type</code>
<code><strong><a href="#user-content-markdownextension">MarkdownExtension</a></strong> = <a href="#user-content-markdownconfig">MarkdownConfig</a> | readonly <a href="#user-content-markdownextension">MarkdownExtension</a>[]</code>
<code>
type
<strong><a href="#user-content-markdownextension">MarkdownExtension</a></strong> = <a href="#user-content-markdownconfig">MarkdownConfig</a> | readonly <a href="#user-content-markdownextension">MarkdownExtension</a>[]</code>
</dt>

<dd><p>To make it possible to group extensions together into bigger
Expand Down Expand Up @@ -339,7 +390,9 @@ general types of block parsers:</p>
<p>Composite block parsers, which handle things like lists and
blockquotes. These define a <a href="#user-content-blockparser.parse"><code>parse</code></a> method
that <a href="#user-content-blockcontext.startcomposite">starts</a> a composite block
and returns null when it recognizes its syntax.</p>
and returns null when it recognizes its syntax. The node type
used by such a block must define a
<a href="#user-content-nodespec.composite"><code>composite</code></a> function as well.</p>
</li>
<li>
<p>Eager leaf block parsers, used for things like code or HTML
Expand Down Expand Up @@ -372,7 +425,7 @@ observe that block.</p>
<dd><p>The eager parse function, which can look at the block's first
line and return <code>false</code> to do nothing, <code>true</code> if it has parsed
(and <a href="#user-content-blockcontext.nextline">moved past</a> a block), or <code>null</code> if
it has started a composite block.</p>
it has <a href="#user-content-blockcontext.startcomposite">started</a> a composite block.</p>
</dd><dt id="user-content-blockparser.leaf">
<code><strong><a href="#user-content-blockparser.leaf">leaf</a></strong>&#8288;?: fn(<a id="user-content-blockparser.leaf^cx" href="#user-content-blockparser.leaf^cx">cx</a>: <a href="#user-content-blockcontext">BlockContext</a>, <a id="user-content-blockparser.leaf^leaf" href="#user-content-blockparser.leaf^leaf">leaf</a>: <a href="#user-content-leafblock">LeafBlock</a>) → <a href="#user-content-leafblockparser">LeafBlockParser</a> | <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null">null</a></code></dt>

Expand Down
2 changes: 2 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ The code is licensed under an MIT license.

@MarkdownConfig

@PreResolveContext

@MarkdownExtension

@parseCode
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export {parser, MarkdownParser, MarkdownConfig, MarkdownExtension,
export {parser, MarkdownParser, MarkdownConfig, MarkdownExtension, PreResolveContext,
NodeSpec, InlineParser, BlockParser, LeafBlockParser,
Line, Element, LeafBlock, DelimiterType, BlockContext, InlineContext} from "./markdown"
export {parseCode} from "./nest"
Expand Down
127 changes: 123 additions & 4 deletions src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,33 @@ export interface LeafBlockParser {
finish(cx: BlockContext, leaf: LeafBlock): boolean
}

/// Context object passed to pre-resolve delimiter hooks. Provides
/// an immutable view of pending delimiters and methods to mark them
/// as resolved or add elements.
export interface PreResolveContext {
/// Immutable list of pending delimiters. Each delimiter has a type,
/// position range (from/to), and side flags (1=open, 2=close, 3=both).
readonly delimiters: ReadonlyArray<{
readonly type: DelimiterType
readonly from: number
readonly to: number
readonly side: number
}>
/// End position of the current inline section.
readonly blockEnd: number
/// The parser being used.
readonly parser: MarkdownParser
/// Mark the delimiter at the given index as resolved (it will be
/// skipped by standard resolution).
markResolved(index: number): void
/// Add an element to the output.
addElement(element: Element): void
/// Create an element.
elt(type: string, from: number, to: number, children?: readonly Element[]): Element
/// Get a substring of the inline section using document-relative positions.
slice(from: number, to: number): string
}

/// Objects of this type are used to
/// [configure](#MarkdownParser.configure) the Markdown parser.
export interface MarkdownConfig {
Expand All @@ -1104,6 +1131,10 @@ export interface MarkdownConfig {
parseBlock?: readonly BlockParser[]
/// Define new [inline parsing](#InlineParser) logic.
parseInline?: readonly InlineParser[]
/// Hooks called before standard delimiter resolution. Each hook
/// receives a [PreResolveContext](#PreResolveContext) and can
/// inspect delimiters and add elements or mark delimiters as resolved.
preResolveDelimiters?: readonly ((ctx: PreResolveContext) => void)[]
/// Remove the named parsers from the configuration.
remove?: readonly string[]
/// Add a parse wrapper (such as a [mixed-language
Expand Down Expand Up @@ -1140,6 +1171,8 @@ export class MarkdownParser extends Parser {
/// @internal
readonly inlineParsers: readonly (((cx: InlineContext, next: number, pos: number) => number) | undefined)[],
/// @internal
readonly preResolveDelimiters: readonly ((ctx: PreResolveContext) => void)[],
/// @internal
readonly inlineNames: readonly string[],
/// @internal
readonly wrappers: readonly ParseWrapper[]
Expand All @@ -1160,9 +1193,10 @@ export class MarkdownParser extends Parser {
if (!config) return this
let {nodeSet, skipContextMarkup} = this
let blockParsers = this.blockParsers.slice(), leafBlockParsers = this.leafBlockParsers.slice(),
blockNames = this.blockNames.slice(), inlineParsers = this.inlineParsers.slice(),
inlineNames = this.inlineNames.slice(), endLeafBlock = this.endLeafBlock.slice(),
wrappers = this.wrappers
blockNames = this.blockNames.slice(), endLeafBlock = this.endLeafBlock.slice()
let inlineParsers = this.inlineParsers.slice(), inlineNames = this.inlineNames.slice()
let preResolveDelimiters = this.preResolveDelimiters.slice()
let wrappers = this.wrappers

if (nonEmpty(config.defineNodes)) {
skipContextMarkup = Object.assign({}, skipContextMarkup)
Expand Down Expand Up @@ -1232,11 +1266,12 @@ export class MarkdownParser extends Parser {
}

if (config.wrap) wrappers = wrappers.concat(config.wrap)
if (config.preResolveDelimiters) preResolveDelimiters = preResolveDelimiters.concat(config.preResolveDelimiters)

return new MarkdownParser(nodeSet,
blockParsers, leafBlockParsers, blockNames,
endLeafBlock, skipContextMarkup,
inlineParsers, inlineNames, wrappers)
inlineParsers, preResolveDelimiters, inlineNames, wrappers)
}

/// @internal
Expand Down Expand Up @@ -1282,6 +1317,7 @@ function resolveConfig(spec: MarkdownExtension): MarkdownConfig | null {
defineNodes: conc(conf.defineNodes, rest.defineNodes),
parseBlock: conc(conf.parseBlock, rest.parseBlock),
parseInline: conc(conf.parseInline, rest.parseInline),
preResolveDelimiters: conc(conf.preResolveDelimiters, rest.preResolveDelimiters),
remove: conc(conf.remove, rest.remove),
wrap: !wrapA ? wrapB : !wrapB ? wrapA :
(inner, input, fragments, ranges) => wrapA!(wrapB!(inner, input, fragments, ranges), input, fragments, ranges)
Expand Down Expand Up @@ -1633,6 +1669,82 @@ function parseLinkLabel(text: string, start: number, offset: number, requireNonW
return null
}

// Internal implementation of PreResolveContext that wraps InlineContext
class PreResolveContextImpl implements PreResolveContext {
readonly delimiters: ReadonlyArray<{
readonly type: DelimiterType
readonly from: number
readonly to: number
readonly side: number
}>
readonly blockEnd: number
readonly parser: MarkdownParser
private resolvedIndices: Set<number> = new Set()
private addedElements: Element[] = []
private cx: InlineContext
private delimToPartIndex: number[] = []

constructor(cx: InlineContext, from: number) {
this.cx = cx
this.parser = cx.parser
this.blockEnd = cx.end
// Build immutable list of delimiters with mapping to parts indices
const delims: Array<{
readonly type: DelimiterType
readonly from: number
readonly to: number
readonly side: number
}> = []
for (let i = from; i < cx.parts.length; i++) {
const part = cx.parts[i]
if (part && !(part instanceof Element)) {
const delim = part as InlineDelimiter
this.delimToPartIndex.push(i)
delims.push({
type: delim.type,
from: delim.from,
to: delim.to,
side: delim.side
})
}
}
this.delimiters = delims
}

markResolved(index: number): void {
if (index >= 0 && index < this.delimiters.length) {
this.resolvedIndices.add(index)
}
}

addElement(element: Element): void {
this.addedElements.push(element)
}

elt(type: string, from: number, to: number, children?: readonly Element[]): Element {
return this.cx.elt(type, from, to, children as Element[])
}

slice(from: number, to: number): string {
return this.cx.slice(from, to)
}

// Apply the changes back to the InlineContext
apply(): void {
// Null out resolved delimiters using the mapping
for (const delimIndex of this.resolvedIndices) {
const partIndex = this.delimToPartIndex[delimIndex]
if (partIndex !== undefined) {
this.cx.parts[partIndex] = null
}
}
// Add new elements to parts
for (const elt of this.addedElements) {
this.cx.parts.push(elt)
}
}
}

/// Inline parsing functions get access to this context, and use it to
/// read the content and emit syntax nodes.
export class InlineContext {
Expand Down Expand Up @@ -1692,6 +1804,12 @@ export class InlineContext {
/// Resolve markers between this.parts.length and from, wrapping matched markers in the
/// appropriate node and updating the content of this.parts. @internal
resolveMarkers(from: number) {
// Run pre-resolve hooks with a safe context wrapper
if (this.parser.preResolveDelimiters.length) {
const ctx = new PreResolveContextImpl(this, from)
for (const hook of this.parser.preResolveDelimiters) hook(ctx)
ctx.apply()
}
// Scan forward, looking for closing tokens
for (let i = from; i < this.parts.length; i++) {
let close = this.parts[i]
Expand Down Expand Up @@ -1956,6 +2074,7 @@ export const parser = new MarkdownParser(
DefaultEndLeaf,
DefaultSkipMarkup,
Object.keys(DefaultInline).map(n => DefaultInline[n]),
[],
Object.keys(DefaultInline),
[]
)
52 changes: 52 additions & 0 deletions test/test-delimiter-resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {parser as cmParser, PreResolveContext} from "../dist/index.js"
import {compareTree} from "./compare-tree.js"
import {SpecParser} from "./spec.js"
import {it, describe} from "mocha"

// A resolver that clears all asterisk emphasis delimiters (identified by checking
// the character at their position). This demonstrates access to delimiters added
// by the built-in Emphasis parser.
function clearAsteriskEmphasis(ctx: PreResolveContext) {
for (let i = 0; i < ctx.delimiters.length; i++) {
const delim = ctx.delimiters[i]
// Check if this is an emphasis delimiter
if (delim.type && (delim.type as any).resolve === "Emphasis") {
// Check if this is an asterisk delimiter by looking at the character
let char = ctx.slice(delim.from, delim.from + 1)
if (char === "*") {
ctx.markResolved(i)
}
}
}
}

const NoAsteriskEmphasis = {
preResolveDelimiters: [clearAsteriskEmphasis]
} as any

const parser = cmParser.configure([NoAsteriskEmphasis])
const specParser = new SpecParser(parser)

function test(name: string, spec: string, p = parser) {
it(name, () => {
let {tree, doc} = specParser.parse(spec, name)
compareTree(p.parse(doc), tree)
})
}

describe("preResolveDelimiters", () => {
test("clears asterisk emphasis", `
{P:*hello*}`)

test("clears strong asterisk emphasis", `
{P:**hello**}`)

test("preserves underscore emphasis", `
{P:{Em:{e:_}hello{e:_}}}`)

test("clears asterisk but preserves underscore", `
{P:*foo* {Em:{e:_}bar{e:_}}}`)

test("clears nested asterisk in underscore", `
{P:{Em:{e:_}hello *world*{e:_}}}`)
})