forked from FINAKON/HelpProject
1. Initial Commit - a boiler plate code and POC to realize the concept of context sensitive help 2. Frontend code written in ReactJS 3. Backend code written in Java, Spring Boot Framework 4. Frontend Start: pre-requisites : node, npm npm run dev ==> to start the frontend vite server 5. Backend Start: pre-requisites : java, mvn mvn spring-boot:run ==> to start the backend server 6. Visit http://localhost:5173/ for basic demo of help, press F1 in textboxes 7. Visit http://localhost:5173/editor and enter "admin123" to add/modify texts. Happy Coding !!! Thank you, Bhargava.
1349 lines
33 KiB
JavaScript
1349 lines
33 KiB
JavaScript
/**
|
||
* @import {
|
||
* Break,
|
||
* Blockquote,
|
||
* Code,
|
||
* Definition,
|
||
* Emphasis,
|
||
* Heading,
|
||
* Html,
|
||
* Image,
|
||
* InlineCode,
|
||
* Link,
|
||
* ListItem,
|
||
* List,
|
||
* Nodes,
|
||
* Paragraph,
|
||
* PhrasingContent,
|
||
* ReferenceType,
|
||
* Root,
|
||
* Strong,
|
||
* Text,
|
||
* ThematicBreak
|
||
* } from 'mdast'
|
||
* @import {
|
||
* Encoding,
|
||
* Event,
|
||
* Token,
|
||
* Value
|
||
* } from 'micromark-util-types'
|
||
* @import {Point} from 'unist'
|
||
* @import {
|
||
* CompileContext,
|
||
* CompileData,
|
||
* Config,
|
||
* Extension,
|
||
* Handle,
|
||
* OnEnterError,
|
||
* Options
|
||
* } from './types.js'
|
||
*/
|
||
|
||
import {ok as assert} from 'devlop'
|
||
import {toString} from 'mdast-util-to-string'
|
||
import {parse, postprocess, preprocess} from 'micromark'
|
||
import {decodeNumericCharacterReference} from 'micromark-util-decode-numeric-character-reference'
|
||
import {decodeString} from 'micromark-util-decode-string'
|
||
import {normalizeIdentifier} from 'micromark-util-normalize-identifier'
|
||
import {codes, constants, types} from 'micromark-util-symbol'
|
||
import {decodeNamedCharacterReference} from 'decode-named-character-reference'
|
||
import {stringifyPosition} from 'unist-util-stringify-position'
|
||
|
||
const own = {}.hasOwnProperty
|
||
|
||
/**
|
||
* Turn markdown into a syntax tree.
|
||
*
|
||
* @overload
|
||
* @param {Value} value
|
||
* @param {Encoding | null | undefined} [encoding]
|
||
* @param {Options | null | undefined} [options]
|
||
* @returns {Root}
|
||
*
|
||
* @overload
|
||
* @param {Value} value
|
||
* @param {Options | null | undefined} [options]
|
||
* @returns {Root}
|
||
*
|
||
* @param {Value} value
|
||
* Markdown to parse.
|
||
* @param {Encoding | Options | null | undefined} [encoding]
|
||
* Character encoding for when `value` is `Buffer`.
|
||
* @param {Options | null | undefined} [options]
|
||
* Configuration.
|
||
* @returns {Root}
|
||
* mdast tree.
|
||
*/
|
||
export function fromMarkdown(value, encoding, options) {
|
||
if (typeof encoding !== 'string') {
|
||
options = encoding
|
||
encoding = undefined
|
||
}
|
||
|
||
return compiler(options)(
|
||
postprocess(
|
||
parse(options)
|
||
.document()
|
||
.write(preprocess()(value, encoding, true))
|
||
)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Note this compiler only understand complete buffering, not streaming.
|
||
*
|
||
* @param {Options | null | undefined} [options]
|
||
*/
|
||
function compiler(options) {
|
||
/** @type {Config} */
|
||
const config = {
|
||
transforms: [],
|
||
canContainEols: ['emphasis', 'fragment', 'heading', 'paragraph', 'strong'],
|
||
enter: {
|
||
autolink: opener(link),
|
||
autolinkProtocol: onenterdata,
|
||
autolinkEmail: onenterdata,
|
||
atxHeading: opener(heading),
|
||
blockQuote: opener(blockQuote),
|
||
characterEscape: onenterdata,
|
||
characterReference: onenterdata,
|
||
codeFenced: opener(codeFlow),
|
||
codeFencedFenceInfo: buffer,
|
||
codeFencedFenceMeta: buffer,
|
||
codeIndented: opener(codeFlow, buffer),
|
||
codeText: opener(codeText, buffer),
|
||
codeTextData: onenterdata,
|
||
data: onenterdata,
|
||
codeFlowValue: onenterdata,
|
||
definition: opener(definition),
|
||
definitionDestinationString: buffer,
|
||
definitionLabelString: buffer,
|
||
definitionTitleString: buffer,
|
||
emphasis: opener(emphasis),
|
||
hardBreakEscape: opener(hardBreak),
|
||
hardBreakTrailing: opener(hardBreak),
|
||
htmlFlow: opener(html, buffer),
|
||
htmlFlowData: onenterdata,
|
||
htmlText: opener(html, buffer),
|
||
htmlTextData: onenterdata,
|
||
image: opener(image),
|
||
label: buffer,
|
||
link: opener(link),
|
||
listItem: opener(listItem),
|
||
listItemValue: onenterlistitemvalue,
|
||
listOrdered: opener(list, onenterlistordered),
|
||
listUnordered: opener(list),
|
||
paragraph: opener(paragraph),
|
||
reference: onenterreference,
|
||
referenceString: buffer,
|
||
resourceDestinationString: buffer,
|
||
resourceTitleString: buffer,
|
||
setextHeading: opener(heading),
|
||
strong: opener(strong),
|
||
thematicBreak: opener(thematicBreak)
|
||
},
|
||
exit: {
|
||
atxHeading: closer(),
|
||
atxHeadingSequence: onexitatxheadingsequence,
|
||
autolink: closer(),
|
||
autolinkEmail: onexitautolinkemail,
|
||
autolinkProtocol: onexitautolinkprotocol,
|
||
blockQuote: closer(),
|
||
characterEscapeValue: onexitdata,
|
||
characterReferenceMarkerHexadecimal: onexitcharacterreferencemarker,
|
||
characterReferenceMarkerNumeric: onexitcharacterreferencemarker,
|
||
characterReferenceValue: onexitcharacterreferencevalue,
|
||
characterReference: onexitcharacterreference,
|
||
codeFenced: closer(onexitcodefenced),
|
||
codeFencedFence: onexitcodefencedfence,
|
||
codeFencedFenceInfo: onexitcodefencedfenceinfo,
|
||
codeFencedFenceMeta: onexitcodefencedfencemeta,
|
||
codeFlowValue: onexitdata,
|
||
codeIndented: closer(onexitcodeindented),
|
||
codeText: closer(onexitcodetext),
|
||
codeTextData: onexitdata,
|
||
data: onexitdata,
|
||
definition: closer(),
|
||
definitionDestinationString: onexitdefinitiondestinationstring,
|
||
definitionLabelString: onexitdefinitionlabelstring,
|
||
definitionTitleString: onexitdefinitiontitlestring,
|
||
emphasis: closer(),
|
||
hardBreakEscape: closer(onexithardbreak),
|
||
hardBreakTrailing: closer(onexithardbreak),
|
||
htmlFlow: closer(onexithtmlflow),
|
||
htmlFlowData: onexitdata,
|
||
htmlText: closer(onexithtmltext),
|
||
htmlTextData: onexitdata,
|
||
image: closer(onexitimage),
|
||
label: onexitlabel,
|
||
labelText: onexitlabeltext,
|
||
lineEnding: onexitlineending,
|
||
link: closer(onexitlink),
|
||
listItem: closer(),
|
||
listOrdered: closer(),
|
||
listUnordered: closer(),
|
||
paragraph: closer(),
|
||
referenceString: onexitreferencestring,
|
||
resourceDestinationString: onexitresourcedestinationstring,
|
||
resourceTitleString: onexitresourcetitlestring,
|
||
resource: onexitresource,
|
||
setextHeading: closer(onexitsetextheading),
|
||
setextHeadingLineSequence: onexitsetextheadinglinesequence,
|
||
setextHeadingText: onexitsetextheadingtext,
|
||
strong: closer(),
|
||
thematicBreak: closer()
|
||
}
|
||
}
|
||
|
||
configure(config, (options || {}).mdastExtensions || [])
|
||
|
||
/** @type {CompileData} */
|
||
const data = {}
|
||
|
||
return compile
|
||
|
||
/**
|
||
* Turn micromark events into an mdast tree.
|
||
*
|
||
* @param {Array<Event>} events
|
||
* Events.
|
||
* @returns {Root}
|
||
* mdast tree.
|
||
*/
|
||
function compile(events) {
|
||
/** @type {Root} */
|
||
let tree = {type: 'root', children: []}
|
||
/** @type {Omit<CompileContext, 'sliceSerialize'>} */
|
||
const context = {
|
||
stack: [tree],
|
||
tokenStack: [],
|
||
config,
|
||
enter,
|
||
exit,
|
||
buffer,
|
||
resume,
|
||
data
|
||
}
|
||
/** @type {Array<number>} */
|
||
const listStack = []
|
||
let index = -1
|
||
|
||
while (++index < events.length) {
|
||
// We preprocess lists to add `listItem` tokens, and to infer whether
|
||
// items the list itself are spread out.
|
||
if (
|
||
events[index][1].type === types.listOrdered ||
|
||
events[index][1].type === types.listUnordered
|
||
) {
|
||
if (events[index][0] === 'enter') {
|
||
listStack.push(index)
|
||
} else {
|
||
const tail = listStack.pop()
|
||
assert(typeof tail === 'number', 'expected list ot be open')
|
||
index = prepareList(events, tail, index)
|
||
}
|
||
}
|
||
}
|
||
|
||
index = -1
|
||
|
||
while (++index < events.length) {
|
||
const handler = config[events[index][0]]
|
||
|
||
if (own.call(handler, events[index][1].type)) {
|
||
handler[events[index][1].type].call(
|
||
Object.assign(
|
||
{sliceSerialize: events[index][2].sliceSerialize},
|
||
context
|
||
),
|
||
events[index][1]
|
||
)
|
||
}
|
||
}
|
||
|
||
// Handle tokens still being open.
|
||
if (context.tokenStack.length > 0) {
|
||
const tail = context.tokenStack[context.tokenStack.length - 1]
|
||
const handler = tail[1] || defaultOnError
|
||
handler.call(context, undefined, tail[0])
|
||
}
|
||
|
||
// Figure out `root` position.
|
||
tree.position = {
|
||
start: point(
|
||
events.length > 0 ? events[0][1].start : {line: 1, column: 1, offset: 0}
|
||
),
|
||
end: point(
|
||
events.length > 0
|
||
? events[events.length - 2][1].end
|
||
: {line: 1, column: 1, offset: 0}
|
||
)
|
||
}
|
||
|
||
// Call transforms.
|
||
index = -1
|
||
while (++index < config.transforms.length) {
|
||
tree = config.transforms[index](tree) || tree
|
||
}
|
||
|
||
return tree
|
||
}
|
||
|
||
/**
|
||
* @param {Array<Event>} events
|
||
* @param {number} start
|
||
* @param {number} length
|
||
* @returns {number}
|
||
*/
|
||
function prepareList(events, start, length) {
|
||
let index = start - 1
|
||
let containerBalance = -1
|
||
let listSpread = false
|
||
/** @type {Token | undefined} */
|
||
let listItem
|
||
/** @type {number | undefined} */
|
||
let lineIndex
|
||
/** @type {number | undefined} */
|
||
let firstBlankLineIndex
|
||
/** @type {boolean | undefined} */
|
||
let atMarker
|
||
|
||
while (++index <= length) {
|
||
const event = events[index]
|
||
|
||
switch (event[1].type) {
|
||
case types.listUnordered:
|
||
case types.listOrdered:
|
||
case types.blockQuote: {
|
||
if (event[0] === 'enter') {
|
||
containerBalance++
|
||
} else {
|
||
containerBalance--
|
||
}
|
||
|
||
atMarker = undefined
|
||
|
||
break
|
||
}
|
||
|
||
case types.lineEndingBlank: {
|
||
if (event[0] === 'enter') {
|
||
if (
|
||
listItem &&
|
||
!atMarker &&
|
||
!containerBalance &&
|
||
!firstBlankLineIndex
|
||
) {
|
||
firstBlankLineIndex = index
|
||
}
|
||
|
||
atMarker = undefined
|
||
}
|
||
|
||
break
|
||
}
|
||
|
||
case types.linePrefix:
|
||
case types.listItemValue:
|
||
case types.listItemMarker:
|
||
case types.listItemPrefix:
|
||
case types.listItemPrefixWhitespace: {
|
||
// Empty.
|
||
|
||
break
|
||
}
|
||
|
||
default: {
|
||
atMarker = undefined
|
||
}
|
||
}
|
||
|
||
if (
|
||
(!containerBalance &&
|
||
event[0] === 'enter' &&
|
||
event[1].type === types.listItemPrefix) ||
|
||
(containerBalance === -1 &&
|
||
event[0] === 'exit' &&
|
||
(event[1].type === types.listUnordered ||
|
||
event[1].type === types.listOrdered))
|
||
) {
|
||
if (listItem) {
|
||
let tailIndex = index
|
||
lineIndex = undefined
|
||
|
||
while (tailIndex--) {
|
||
const tailEvent = events[tailIndex]
|
||
|
||
if (
|
||
tailEvent[1].type === types.lineEnding ||
|
||
tailEvent[1].type === types.lineEndingBlank
|
||
) {
|
||
if (tailEvent[0] === 'exit') continue
|
||
|
||
if (lineIndex) {
|
||
events[lineIndex][1].type = types.lineEndingBlank
|
||
listSpread = true
|
||
}
|
||
|
||
tailEvent[1].type = types.lineEnding
|
||
lineIndex = tailIndex
|
||
} else if (
|
||
tailEvent[1].type === types.linePrefix ||
|
||
tailEvent[1].type === types.blockQuotePrefix ||
|
||
tailEvent[1].type === types.blockQuotePrefixWhitespace ||
|
||
tailEvent[1].type === types.blockQuoteMarker ||
|
||
tailEvent[1].type === types.listItemIndent
|
||
) {
|
||
// Empty
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
if (
|
||
firstBlankLineIndex &&
|
||
(!lineIndex || firstBlankLineIndex < lineIndex)
|
||
) {
|
||
listItem._spread = true
|
||
}
|
||
|
||
// Fix position.
|
||
listItem.end = Object.assign(
|
||
{},
|
||
lineIndex ? events[lineIndex][1].start : event[1].end
|
||
)
|
||
|
||
events.splice(lineIndex || index, 0, ['exit', listItem, event[2]])
|
||
index++
|
||
length++
|
||
}
|
||
|
||
// Create a new list item.
|
||
if (event[1].type === types.listItemPrefix) {
|
||
/** @type {Token} */
|
||
const item = {
|
||
type: 'listItem',
|
||
_spread: false,
|
||
start: Object.assign({}, event[1].start),
|
||
// @ts-expect-error: we’ll add `end` in a second.
|
||
end: undefined
|
||
}
|
||
listItem = item
|
||
events.splice(index, 0, ['enter', item, event[2]])
|
||
index++
|
||
length++
|
||
firstBlankLineIndex = undefined
|
||
atMarker = true
|
||
}
|
||
}
|
||
}
|
||
|
||
events[start][1]._spread = listSpread
|
||
return length
|
||
}
|
||
|
||
/**
|
||
* Create an opener handle.
|
||
*
|
||
* @param {(token: Token) => Nodes} create
|
||
* Create a node.
|
||
* @param {Handle | undefined} [and]
|
||
* Optional function to also run.
|
||
* @returns {Handle}
|
||
* Handle.
|
||
*/
|
||
function opener(create, and) {
|
||
return open
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @param {Token} token
|
||
* @returns {undefined}
|
||
*/
|
||
function open(token) {
|
||
enter.call(this, create(token), token)
|
||
if (and) and.call(this, token)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @type {CompileContext['buffer']}
|
||
*/
|
||
function buffer() {
|
||
this.stack.push({type: 'fragment', children: []})
|
||
}
|
||
|
||
/**
|
||
* @type {CompileContext['enter']}
|
||
*/
|
||
function enter(node, token, errorHandler) {
|
||
const parent = this.stack[this.stack.length - 1]
|
||
assert(parent, 'expected `parent`')
|
||
assert('children' in parent, 'expected `parent`')
|
||
/** @type {Array<Nodes>} */
|
||
const siblings = parent.children
|
||
siblings.push(node)
|
||
this.stack.push(node)
|
||
this.tokenStack.push([token, errorHandler || undefined])
|
||
node.position = {
|
||
start: point(token.start),
|
||
// @ts-expect-error: `end` will be patched later.
|
||
end: undefined
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a closer handle.
|
||
*
|
||
* @param {Handle | undefined} [and]
|
||
* Optional function to also run.
|
||
* @returns {Handle}
|
||
* Handle.
|
||
*/
|
||
function closer(and) {
|
||
return close
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @param {Token} token
|
||
* @returns {undefined}
|
||
*/
|
||
function close(token) {
|
||
if (and) and.call(this, token)
|
||
exit.call(this, token)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @type {CompileContext['exit']}
|
||
*/
|
||
function exit(token, onExitError) {
|
||
const node = this.stack.pop()
|
||
assert(node, 'expected `node`')
|
||
const open = this.tokenStack.pop()
|
||
|
||
if (!open) {
|
||
throw new Error(
|
||
'Cannot close `' +
|
||
token.type +
|
||
'` (' +
|
||
stringifyPosition({start: token.start, end: token.end}) +
|
||
'): it’s not open'
|
||
)
|
||
} else if (open[0].type !== token.type) {
|
||
if (onExitError) {
|
||
onExitError.call(this, token, open[0])
|
||
} else {
|
||
const handler = open[1] || defaultOnError
|
||
handler.call(this, token, open[0])
|
||
}
|
||
}
|
||
|
||
assert(node.type !== 'fragment', 'unexpected fragment `exit`ed')
|
||
assert(node.position, 'expected `position` to be defined')
|
||
node.position.end = point(token.end)
|
||
}
|
||
|
||
/**
|
||
* @type {CompileContext['resume']}
|
||
*/
|
||
function resume() {
|
||
return toString(this.stack.pop())
|
||
}
|
||
|
||
//
|
||
// Handlers.
|
||
//
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onenterlistordered() {
|
||
this.data.expectingFirstListItemValue = true
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onenterlistitemvalue(token) {
|
||
if (this.data.expectingFirstListItemValue) {
|
||
const ancestor = this.stack[this.stack.length - 2]
|
||
assert(ancestor, 'expected nodes on stack')
|
||
assert(ancestor.type === 'list', 'expected list on stack')
|
||
ancestor.start = Number.parseInt(
|
||
this.sliceSerialize(token),
|
||
constants.numericBaseDecimal
|
||
)
|
||
this.data.expectingFirstListItemValue = undefined
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitcodefencedfenceinfo() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'code', 'expected code on stack')
|
||
node.lang = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitcodefencedfencemeta() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'code', 'expected code on stack')
|
||
node.meta = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitcodefencedfence() {
|
||
// Exit if this is the closing fence.
|
||
if (this.data.flowCodeInside) return
|
||
this.buffer()
|
||
this.data.flowCodeInside = true
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitcodefenced() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'code', 'expected code on stack')
|
||
|
||
node.value = data.replace(/^(\r?\n|\r)|(\r?\n|\r)$/g, '')
|
||
this.data.flowCodeInside = undefined
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitcodeindented() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'code', 'expected code on stack')
|
||
|
||
node.value = data.replace(/(\r?\n|\r)$/g, '')
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitdefinitionlabelstring(token) {
|
||
const label = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'definition', 'expected definition on stack')
|
||
|
||
node.label = label
|
||
node.identifier = normalizeIdentifier(
|
||
this.sliceSerialize(token)
|
||
).toLowerCase()
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitdefinitiontitlestring() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'definition', 'expected definition on stack')
|
||
|
||
node.title = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitdefinitiondestinationstring() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'definition', 'expected definition on stack')
|
||
|
||
node.url = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitatxheadingsequence(token) {
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'heading', 'expected heading on stack')
|
||
|
||
if (!node.depth) {
|
||
const depth = this.sliceSerialize(token).length
|
||
|
||
assert(
|
||
depth === 1 ||
|
||
depth === 2 ||
|
||
depth === 3 ||
|
||
depth === 4 ||
|
||
depth === 5 ||
|
||
depth === 6,
|
||
'expected `depth` between `1` and `6`'
|
||
)
|
||
|
||
node.depth = depth
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitsetextheadingtext() {
|
||
this.data.setextHeadingSlurpLineEnding = true
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitsetextheadinglinesequence(token) {
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'heading', 'expected heading on stack')
|
||
|
||
node.depth =
|
||
this.sliceSerialize(token).codePointAt(0) === codes.equalsTo ? 1 : 2
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitsetextheading() {
|
||
this.data.setextHeadingSlurpLineEnding = undefined
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onenterdata(token) {
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert('children' in node, 'expected parent on stack')
|
||
/** @type {Array<Nodes>} */
|
||
const siblings = node.children
|
||
|
||
let tail = siblings[siblings.length - 1]
|
||
|
||
if (!tail || tail.type !== 'text') {
|
||
// Add a new text node.
|
||
tail = text()
|
||
tail.position = {
|
||
start: point(token.start),
|
||
// @ts-expect-error: we’ll add `end` later.
|
||
end: undefined
|
||
}
|
||
siblings.push(tail)
|
||
}
|
||
|
||
this.stack.push(tail)
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitdata(token) {
|
||
const tail = this.stack.pop()
|
||
assert(tail, 'expected a `node` to be on the stack')
|
||
assert('value' in tail, 'expected a `literal` to be on the stack')
|
||
assert(tail.position, 'expected `node` to have an open position')
|
||
tail.value += this.sliceSerialize(token)
|
||
tail.position.end = point(token.end)
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitlineending(token) {
|
||
const context = this.stack[this.stack.length - 1]
|
||
assert(context, 'expected `node`')
|
||
|
||
// If we’re at a hard break, include the line ending in there.
|
||
if (this.data.atHardBreak) {
|
||
assert('children' in context, 'expected `parent`')
|
||
const tail = context.children[context.children.length - 1]
|
||
assert(tail.position, 'expected tail to have a starting position')
|
||
tail.position.end = point(token.end)
|
||
this.data.atHardBreak = undefined
|
||
return
|
||
}
|
||
|
||
if (
|
||
!this.data.setextHeadingSlurpLineEnding &&
|
||
config.canContainEols.includes(context.type)
|
||
) {
|
||
onenterdata.call(this, token)
|
||
onexitdata.call(this, token)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexithardbreak() {
|
||
this.data.atHardBreak = true
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexithtmlflow() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'html', 'expected html on stack')
|
||
|
||
node.value = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexithtmltext() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'html', 'expected html on stack')
|
||
|
||
node.value = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitcodetext() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'inlineCode', 'expected inline code on stack')
|
||
|
||
node.value = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitlink() {
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'link', 'expected link on stack')
|
||
|
||
// Note: there are also `identifier` and `label` fields on this link node!
|
||
// These are used / cleaned here.
|
||
|
||
// To do: clean.
|
||
if (this.data.inReference) {
|
||
/** @type {ReferenceType} */
|
||
const referenceType = this.data.referenceType || 'shortcut'
|
||
|
||
node.type += 'Reference'
|
||
// @ts-expect-error: mutate.
|
||
node.referenceType = referenceType
|
||
// @ts-expect-error: mutate.
|
||
delete node.url
|
||
delete node.title
|
||
} else {
|
||
// @ts-expect-error: mutate.
|
||
delete node.identifier
|
||
// @ts-expect-error: mutate.
|
||
delete node.label
|
||
}
|
||
|
||
this.data.referenceType = undefined
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitimage() {
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'image', 'expected image on stack')
|
||
|
||
// Note: there are also `identifier` and `label` fields on this link node!
|
||
// These are used / cleaned here.
|
||
|
||
// To do: clean.
|
||
if (this.data.inReference) {
|
||
/** @type {ReferenceType} */
|
||
const referenceType = this.data.referenceType || 'shortcut'
|
||
|
||
node.type += 'Reference'
|
||
// @ts-expect-error: mutate.
|
||
node.referenceType = referenceType
|
||
// @ts-expect-error: mutate.
|
||
delete node.url
|
||
delete node.title
|
||
} else {
|
||
// @ts-expect-error: mutate.
|
||
delete node.identifier
|
||
// @ts-expect-error: mutate.
|
||
delete node.label
|
||
}
|
||
|
||
this.data.referenceType = undefined
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitlabeltext(token) {
|
||
const string = this.sliceSerialize(token)
|
||
const ancestor = this.stack[this.stack.length - 2]
|
||
assert(ancestor, 'expected ancestor on stack')
|
||
assert(
|
||
ancestor.type === 'image' || ancestor.type === 'link',
|
||
'expected image or link on stack'
|
||
)
|
||
|
||
// @ts-expect-error: stash this on the node, as it might become a reference
|
||
// later.
|
||
ancestor.label = decodeString(string)
|
||
// @ts-expect-error: same as above.
|
||
ancestor.identifier = normalizeIdentifier(string).toLowerCase()
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitlabel() {
|
||
const fragment = this.stack[this.stack.length - 1]
|
||
assert(fragment, 'expected node on stack')
|
||
assert(fragment.type === 'fragment', 'expected fragment on stack')
|
||
const value = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(
|
||
node.type === 'image' || node.type === 'link',
|
||
'expected image or link on stack'
|
||
)
|
||
|
||
// Assume a reference.
|
||
this.data.inReference = true
|
||
|
||
if (node.type === 'link') {
|
||
/** @type {Array<PhrasingContent>} */
|
||
const children = fragment.children
|
||
|
||
node.children = children
|
||
} else {
|
||
node.alt = value
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitresourcedestinationstring() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(
|
||
node.type === 'image' || node.type === 'link',
|
||
'expected image or link on stack'
|
||
)
|
||
node.url = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitresourcetitlestring() {
|
||
const data = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(
|
||
node.type === 'image' || node.type === 'link',
|
||
'expected image or link on stack'
|
||
)
|
||
node.title = data
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitresource() {
|
||
this.data.inReference = undefined
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onenterreference() {
|
||
this.data.referenceType = 'collapsed'
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitreferencestring(token) {
|
||
const label = this.resume()
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(
|
||
node.type === 'image' || node.type === 'link',
|
||
'expected image reference or link reference on stack'
|
||
)
|
||
|
||
// @ts-expect-error: stash this on the node, as it might become a reference
|
||
// later.
|
||
node.label = label
|
||
// @ts-expect-error: same as above.
|
||
node.identifier = normalizeIdentifier(
|
||
this.sliceSerialize(token)
|
||
).toLowerCase()
|
||
this.data.referenceType = 'full'
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
|
||
function onexitcharacterreferencemarker(token) {
|
||
assert(
|
||
token.type === 'characterReferenceMarkerNumeric' ||
|
||
token.type === 'characterReferenceMarkerHexadecimal'
|
||
)
|
||
this.data.characterReferenceType = token.type
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitcharacterreferencevalue(token) {
|
||
const data = this.sliceSerialize(token)
|
||
const type = this.data.characterReferenceType
|
||
/** @type {string} */
|
||
let value
|
||
|
||
if (type) {
|
||
value = decodeNumericCharacterReference(
|
||
data,
|
||
type === types.characterReferenceMarkerNumeric
|
||
? constants.numericBaseDecimal
|
||
: constants.numericBaseHexadecimal
|
||
)
|
||
this.data.characterReferenceType = undefined
|
||
} else {
|
||
const result = decodeNamedCharacterReference(data)
|
||
assert(result !== false, 'expected reference to decode')
|
||
value = result
|
||
}
|
||
|
||
const tail = this.stack[this.stack.length - 1]
|
||
assert(tail, 'expected `node`')
|
||
assert('value' in tail, 'expected `node.value`')
|
||
tail.value += value
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitcharacterreference(token) {
|
||
const tail = this.stack.pop()
|
||
assert(tail, 'expected `node`')
|
||
assert(tail.position, 'expected `node.position`')
|
||
tail.position.end = point(token.end)
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitautolinkprotocol(token) {
|
||
onexitdata.call(this, token)
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'link', 'expected link on stack')
|
||
|
||
node.url = this.sliceSerialize(token)
|
||
}
|
||
|
||
/**
|
||
* @this {CompileContext}
|
||
* @type {Handle}
|
||
*/
|
||
function onexitautolinkemail(token) {
|
||
onexitdata.call(this, token)
|
||
const node = this.stack[this.stack.length - 1]
|
||
assert(node, 'expected node on stack')
|
||
assert(node.type === 'link', 'expected link on stack')
|
||
|
||
node.url = 'mailto:' + this.sliceSerialize(token)
|
||
}
|
||
|
||
//
|
||
// Creaters.
|
||
//
|
||
|
||
/** @returns {Blockquote} */
|
||
function blockQuote() {
|
||
return {type: 'blockquote', children: []}
|
||
}
|
||
|
||
/** @returns {Code} */
|
||
function codeFlow() {
|
||
return {type: 'code', lang: null, meta: null, value: ''}
|
||
}
|
||
|
||
/** @returns {InlineCode} */
|
||
function codeText() {
|
||
return {type: 'inlineCode', value: ''}
|
||
}
|
||
|
||
/** @returns {Definition} */
|
||
function definition() {
|
||
return {
|
||
type: 'definition',
|
||
identifier: '',
|
||
label: null,
|
||
title: null,
|
||
url: ''
|
||
}
|
||
}
|
||
|
||
/** @returns {Emphasis} */
|
||
function emphasis() {
|
||
return {type: 'emphasis', children: []}
|
||
}
|
||
|
||
/** @returns {Heading} */
|
||
function heading() {
|
||
return {
|
||
type: 'heading',
|
||
// @ts-expect-error `depth` will be set later.
|
||
depth: 0,
|
||
children: []
|
||
}
|
||
}
|
||
|
||
/** @returns {Break} */
|
||
function hardBreak() {
|
||
return {type: 'break'}
|
||
}
|
||
|
||
/** @returns {Html} */
|
||
function html() {
|
||
return {type: 'html', value: ''}
|
||
}
|
||
|
||
/** @returns {Image} */
|
||
function image() {
|
||
return {type: 'image', title: null, url: '', alt: null}
|
||
}
|
||
|
||
/** @returns {Link} */
|
||
function link() {
|
||
return {type: 'link', title: null, url: '', children: []}
|
||
}
|
||
|
||
/**
|
||
* @param {Token} token
|
||
* @returns {List}
|
||
*/
|
||
function list(token) {
|
||
return {
|
||
type: 'list',
|
||
ordered: token.type === 'listOrdered',
|
||
start: null,
|
||
spread: token._spread,
|
||
children: []
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Token} token
|
||
* @returns {ListItem}
|
||
*/
|
||
function listItem(token) {
|
||
return {
|
||
type: 'listItem',
|
||
spread: token._spread,
|
||
checked: null,
|
||
children: []
|
||
}
|
||
}
|
||
|
||
/** @returns {Paragraph} */
|
||
function paragraph() {
|
||
return {type: 'paragraph', children: []}
|
||
}
|
||
|
||
/** @returns {Strong} */
|
||
function strong() {
|
||
return {type: 'strong', children: []}
|
||
}
|
||
|
||
/** @returns {Text} */
|
||
function text() {
|
||
return {type: 'text', value: ''}
|
||
}
|
||
|
||
/** @returns {ThematicBreak} */
|
||
function thematicBreak() {
|
||
return {type: 'thematicBreak'}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Copy a point-like value.
|
||
*
|
||
* @param {Point} d
|
||
* Point-like value.
|
||
* @returns {Point}
|
||
* unist point.
|
||
*/
|
||
function point(d) {
|
||
return {line: d.line, column: d.column, offset: d.offset}
|
||
}
|
||
|
||
/**
|
||
* @param {Config} combined
|
||
* @param {Array<Array<Extension> | Extension>} extensions
|
||
* @returns {undefined}
|
||
*/
|
||
function configure(combined, extensions) {
|
||
let index = -1
|
||
|
||
while (++index < extensions.length) {
|
||
const value = extensions[index]
|
||
|
||
if (Array.isArray(value)) {
|
||
configure(combined, value)
|
||
} else {
|
||
extension(combined, value)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Config} combined
|
||
* @param {Extension} extension
|
||
* @returns {undefined}
|
||
*/
|
||
function extension(combined, extension) {
|
||
/** @type {keyof Extension} */
|
||
let key
|
||
|
||
for (key in extension) {
|
||
if (own.call(extension, key)) {
|
||
switch (key) {
|
||
case 'canContainEols': {
|
||
const right = extension[key]
|
||
if (right) {
|
||
combined[key].push(...right)
|
||
}
|
||
|
||
break
|
||
}
|
||
|
||
case 'transforms': {
|
||
const right = extension[key]
|
||
if (right) {
|
||
combined[key].push(...right)
|
||
}
|
||
|
||
break
|
||
}
|
||
|
||
case 'enter':
|
||
case 'exit': {
|
||
const right = extension[key]
|
||
if (right) {
|
||
Object.assign(combined[key], right)
|
||
}
|
||
|
||
break
|
||
}
|
||
// No default
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** @type {OnEnterError} */
|
||
function defaultOnError(left, right) {
|
||
if (left) {
|
||
throw new Error(
|
||
'Cannot close `' +
|
||
left.type +
|
||
'` (' +
|
||
stringifyPosition({start: left.start, end: left.end}) +
|
||
'): a different token (`' +
|
||
right.type +
|
||
'`, ' +
|
||
stringifyPosition({start: right.start, end: right.end}) +
|
||
') is open'
|
||
)
|
||
} else {
|
||
throw new Error(
|
||
'Cannot close document, a token (`' +
|
||
right.type +
|
||
'`, ' +
|
||
stringifyPosition({start: right.start, end: right.end}) +
|
||
') is still open'
|
||
)
|
||
}
|
||
}
|