You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
823 lines
21 KiB
823 lines
21 KiB
'use strict' |
|
|
|
module.exports = fromMarkdown |
|
|
|
// These three are compiled away in the `dist/` |
|
|
|
var toString = require('mdast-util-to-string') |
|
var assign = require('micromark/dist/constant/assign') |
|
var own = require('micromark/dist/constant/has-own-property') |
|
var normalizeIdentifier = require('micromark/dist/util/normalize-identifier') |
|
var safeFromInt = require('micromark/dist/util/safe-from-int') |
|
var parser = require('micromark/dist/parse') |
|
var preprocessor = require('micromark/dist/preprocess') |
|
var postprocess = require('micromark/dist/postprocess') |
|
var decode = require('parse-entities/decode-entity') |
|
var stringifyPosition = require('unist-util-stringify-position') |
|
|
|
function fromMarkdown(value, encoding, options) { |
|
if (typeof encoding !== 'string') { |
|
options = encoding |
|
encoding = undefined |
|
} |
|
|
|
return compiler(options)( |
|
postprocess( |
|
parser(options).document().write(preprocessor()(value, encoding, true)) |
|
) |
|
) |
|
} |
|
|
|
// Note this compiler only understand complete buffering, not streaming. |
|
function compiler(options) { |
|
var settings = options || {} |
|
var config = configure( |
|
{ |
|
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, |
|
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() |
|
} |
|
}, |
|
|
|
settings.mdastExtensions || [] |
|
) |
|
|
|
var data = {} |
|
|
|
return compile |
|
|
|
function compile(events) { |
|
var tree = {type: 'root', children: []} |
|
var stack = [tree] |
|
var tokenStack = [] |
|
var listStack = [] |
|
var index = -1 |
|
var handler |
|
var listStart |
|
|
|
var context = { |
|
stack: stack, |
|
tokenStack: tokenStack, |
|
config: config, |
|
enter: enter, |
|
exit: exit, |
|
buffer: buffer, |
|
resume: resume, |
|
setData: setData, |
|
getData: getData |
|
} |
|
|
|
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 === 'listOrdered' || |
|
events[index][1].type === 'listUnordered' |
|
) { |
|
if (events[index][0] === 'enter') { |
|
listStack.push(index) |
|
} else { |
|
listStart = listStack.pop(index) |
|
index = prepareList(events, listStart, index) |
|
} |
|
} |
|
} |
|
|
|
index = -1 |
|
|
|
while (++index < events.length) { |
|
handler = config[events[index][0]] |
|
|
|
if (own.call(handler, events[index][1].type)) { |
|
handler[events[index][1].type].call( |
|
assign({sliceSerialize: events[index][2].sliceSerialize}, context), |
|
events[index][1] |
|
) |
|
} |
|
} |
|
|
|
if (tokenStack.length) { |
|
throw new Error( |
|
'Cannot close document, a token (`' + |
|
tokenStack[tokenStack.length - 1].type + |
|
'`, ' + |
|
stringifyPosition({ |
|
start: tokenStack[tokenStack.length - 1].start, |
|
end: tokenStack[tokenStack.length - 1].end |
|
}) + |
|
') is still open' |
|
) |
|
} |
|
|
|
// Figure out `root` position. |
|
tree.position = { |
|
start: point( |
|
events.length ? events[0][1].start : {line: 1, column: 1, offset: 0} |
|
), |
|
|
|
end: point( |
|
events.length |
|
? events[events.length - 2][1].end |
|
: {line: 1, column: 1, offset: 0} |
|
) |
|
} |
|
|
|
index = -1 |
|
while (++index < config.transforms.length) { |
|
tree = config.transforms[index](tree) || tree |
|
} |
|
|
|
return tree |
|
} |
|
|
|
function prepareList(events, start, length) { |
|
var index = start - 1 |
|
var containerBalance = -1 |
|
var listSpread = false |
|
var listItem |
|
var tailIndex |
|
var lineIndex |
|
var tailEvent |
|
var event |
|
var firstBlankLineIndex |
|
var atMarker |
|
|
|
while (++index <= length) { |
|
event = events[index] |
|
|
|
if ( |
|
event[1].type === 'listUnordered' || |
|
event[1].type === 'listOrdered' || |
|
event[1].type === 'blockQuote' |
|
) { |
|
if (event[0] === 'enter') { |
|
containerBalance++ |
|
} else { |
|
containerBalance-- |
|
} |
|
|
|
atMarker = undefined |
|
} else if (event[1].type === 'lineEndingBlank') { |
|
if (event[0] === 'enter') { |
|
if ( |
|
listItem && |
|
!atMarker && |
|
!containerBalance && |
|
!firstBlankLineIndex |
|
) { |
|
firstBlankLineIndex = index |
|
} |
|
|
|
atMarker = undefined |
|
} |
|
} else if ( |
|
event[1].type === 'linePrefix' || |
|
event[1].type === 'listItemValue' || |
|
event[1].type === 'listItemMarker' || |
|
event[1].type === 'listItemPrefix' || |
|
event[1].type === 'listItemPrefixWhitespace' |
|
) { |
|
// Empty. |
|
} else { |
|
atMarker = undefined |
|
} |
|
|
|
if ( |
|
(!containerBalance && |
|
event[0] === 'enter' && |
|
event[1].type === 'listItemPrefix') || |
|
(containerBalance === -1 && |
|
event[0] === 'exit' && |
|
(event[1].type === 'listUnordered' || |
|
event[1].type === 'listOrdered')) |
|
) { |
|
if (listItem) { |
|
tailIndex = index |
|
lineIndex = undefined |
|
|
|
while (tailIndex--) { |
|
tailEvent = events[tailIndex] |
|
|
|
if ( |
|
tailEvent[1].type === 'lineEnding' || |
|
tailEvent[1].type === 'lineEndingBlank' |
|
) { |
|
if (tailEvent[0] === 'exit') continue |
|
|
|
if (lineIndex) { |
|
events[lineIndex][1].type = 'lineEndingBlank' |
|
listSpread = true |
|
} |
|
|
|
tailEvent[1].type = 'lineEnding' |
|
lineIndex = tailIndex |
|
} else if ( |
|
tailEvent[1].type === 'linePrefix' || |
|
tailEvent[1].type === 'blockQuotePrefix' || |
|
tailEvent[1].type === 'blockQuotePrefixWhitespace' || |
|
tailEvent[1].type === 'blockQuoteMarker' || |
|
tailEvent[1].type === 'listItemIndent' |
|
) { |
|
// Empty |
|
} else { |
|
break |
|
} |
|
} |
|
|
|
if ( |
|
firstBlankLineIndex && |
|
(!lineIndex || firstBlankLineIndex < lineIndex) |
|
) { |
|
listItem._spread = true |
|
} |
|
|
|
// Fix position. |
|
listItem.end = point( |
|
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 === 'listItemPrefix') { |
|
listItem = { |
|
type: 'listItem', |
|
_spread: false, |
|
start: point(event[1].start) |
|
} |
|
|
|
events.splice(index, 0, ['enter', listItem, event[2]]) |
|
index++ |
|
length++ |
|
firstBlankLineIndex = undefined |
|
atMarker = true |
|
} |
|
} |
|
} |
|
|
|
events[start][1]._spread = listSpread |
|
return length |
|
} |
|
|
|
function setData(key, value) { |
|
data[key] = value |
|
} |
|
|
|
function getData(key) { |
|
return data[key] |
|
} |
|
|
|
function point(d) { |
|
return {line: d.line, column: d.column, offset: d.offset} |
|
} |
|
|
|
function opener(create, and) { |
|
return open |
|
|
|
function open(token) { |
|
enter.call(this, create(token), token) |
|
if (and) and.call(this, token) |
|
} |
|
} |
|
|
|
function buffer() { |
|
this.stack.push({type: 'fragment', children: []}) |
|
} |
|
|
|
function enter(node, token) { |
|
this.stack[this.stack.length - 1].children.push(node) |
|
this.stack.push(node) |
|
this.tokenStack.push(token) |
|
node.position = {start: point(token.start)} |
|
return node |
|
} |
|
|
|
function closer(and) { |
|
return close |
|
|
|
function close(token) { |
|
if (and) and.call(this, token) |
|
exit.call(this, token) |
|
} |
|
} |
|
|
|
function exit(token) { |
|
var node = this.stack.pop() |
|
var 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.type !== token.type) { |
|
throw new Error( |
|
'Cannot close `' + |
|
token.type + |
|
'` (' + |
|
stringifyPosition({start: token.start, end: token.end}) + |
|
'): a different token (`' + |
|
open.type + |
|
'`, ' + |
|
stringifyPosition({start: open.start, end: open.end}) + |
|
') is open' |
|
) |
|
} |
|
|
|
node.position.end = point(token.end) |
|
return node |
|
} |
|
|
|
function resume() { |
|
return toString(this.stack.pop()) |
|
} |
|
|
|
// |
|
// Handlers. |
|
// |
|
|
|
function onenterlistordered() { |
|
setData('expectingFirstListItemValue', true) |
|
} |
|
|
|
function onenterlistitemvalue(token) { |
|
if (getData('expectingFirstListItemValue')) { |
|
this.stack[this.stack.length - 2].start = parseInt( |
|
this.sliceSerialize(token), |
|
10 |
|
) |
|
|
|
setData('expectingFirstListItemValue') |
|
} |
|
} |
|
|
|
function onexitcodefencedfenceinfo() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].lang = data |
|
} |
|
|
|
function onexitcodefencedfencemeta() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].meta = data |
|
} |
|
|
|
function onexitcodefencedfence() { |
|
// Exit if this is the closing fence. |
|
if (getData('flowCodeInside')) return |
|
this.buffer() |
|
setData('flowCodeInside', true) |
|
} |
|
|
|
function onexitcodefenced() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].value = data.replace( |
|
/^(\r?\n|\r)|(\r?\n|\r)$/g, |
|
'' |
|
) |
|
|
|
setData('flowCodeInside') |
|
} |
|
|
|
function onexitcodeindented() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].value = data |
|
} |
|
|
|
function onexitdefinitionlabelstring(token) { |
|
// Discard label, use the source content instead. |
|
var label = this.resume() |
|
this.stack[this.stack.length - 1].label = label |
|
this.stack[this.stack.length - 1].identifier = normalizeIdentifier( |
|
this.sliceSerialize(token) |
|
).toLowerCase() |
|
} |
|
|
|
function onexitdefinitiontitlestring() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].title = data |
|
} |
|
|
|
function onexitdefinitiondestinationstring() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].url = data |
|
} |
|
|
|
function onexitatxheadingsequence(token) { |
|
if (!this.stack[this.stack.length - 1].depth) { |
|
this.stack[this.stack.length - 1].depth = this.sliceSerialize( |
|
token |
|
).length |
|
} |
|
} |
|
|
|
function onexitsetextheadingtext() { |
|
setData('setextHeadingSlurpLineEnding', true) |
|
} |
|
|
|
function onexitsetextheadinglinesequence(token) { |
|
this.stack[this.stack.length - 1].depth = |
|
this.sliceSerialize(token).charCodeAt(0) === 61 ? 1 : 2 |
|
} |
|
|
|
function onexitsetextheading() { |
|
setData('setextHeadingSlurpLineEnding') |
|
} |
|
|
|
function onenterdata(token) { |
|
var siblings = this.stack[this.stack.length - 1].children |
|
var tail = siblings[siblings.length - 1] |
|
|
|
if (!tail || tail.type !== 'text') { |
|
// Add a new text node. |
|
tail = text() |
|
tail.position = {start: point(token.start)} |
|
this.stack[this.stack.length - 1].children.push(tail) |
|
} |
|
|
|
this.stack.push(tail) |
|
} |
|
|
|
function onexitdata(token) { |
|
var tail = this.stack.pop() |
|
tail.value += this.sliceSerialize(token) |
|
tail.position.end = point(token.end) |
|
} |
|
|
|
function onexitlineending(token) { |
|
var context = this.stack[this.stack.length - 1] |
|
|
|
// If we’re at a hard break, include the line ending in there. |
|
if (getData('atHardBreak')) { |
|
context.children[context.children.length - 1].position.end = point( |
|
token.end |
|
) |
|
|
|
setData('atHardBreak') |
|
return |
|
} |
|
|
|
if ( |
|
!getData('setextHeadingSlurpLineEnding') && |
|
config.canContainEols.indexOf(context.type) > -1 |
|
) { |
|
onenterdata.call(this, token) |
|
onexitdata.call(this, token) |
|
} |
|
} |
|
|
|
function onexithardbreak() { |
|
setData('atHardBreak', true) |
|
} |
|
|
|
function onexithtmlflow() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].value = data |
|
} |
|
|
|
function onexithtmltext() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].value = data |
|
} |
|
|
|
function onexitcodetext() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].value = data |
|
} |
|
|
|
function onexitlink() { |
|
var context = this.stack[this.stack.length - 1] |
|
|
|
// To do: clean. |
|
if (getData('inReference')) { |
|
context.type += 'Reference' |
|
context.referenceType = getData('referenceType') || 'shortcut' |
|
delete context.url |
|
delete context.title |
|
} else { |
|
delete context.identifier |
|
delete context.label |
|
delete context.referenceType |
|
} |
|
|
|
setData('referenceType') |
|
} |
|
|
|
function onexitimage() { |
|
var context = this.stack[this.stack.length - 1] |
|
|
|
// To do: clean. |
|
if (getData('inReference')) { |
|
context.type += 'Reference' |
|
context.referenceType = getData('referenceType') || 'shortcut' |
|
delete context.url |
|
delete context.title |
|
} else { |
|
delete context.identifier |
|
delete context.label |
|
delete context.referenceType |
|
} |
|
|
|
setData('referenceType') |
|
} |
|
|
|
function onexitlabeltext(token) { |
|
this.stack[this.stack.length - 2].identifier = normalizeIdentifier( |
|
this.sliceSerialize(token) |
|
).toLowerCase() |
|
} |
|
|
|
function onexitlabel() { |
|
var fragment = this.stack[this.stack.length - 1] |
|
var value = this.resume() |
|
|
|
this.stack[this.stack.length - 1].label = value |
|
|
|
// Assume a reference. |
|
setData('inReference', true) |
|
|
|
if (this.stack[this.stack.length - 1].type === 'link') { |
|
this.stack[this.stack.length - 1].children = fragment.children |
|
} else { |
|
this.stack[this.stack.length - 1].alt = value |
|
} |
|
} |
|
|
|
function onexitresourcedestinationstring() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].url = data |
|
} |
|
|
|
function onexitresourcetitlestring() { |
|
var data = this.resume() |
|
this.stack[this.stack.length - 1].title = data |
|
} |
|
|
|
function onexitresource() { |
|
setData('inReference') |
|
} |
|
|
|
function onenterreference() { |
|
setData('referenceType', 'collapsed') |
|
} |
|
|
|
function onexitreferencestring(token) { |
|
var label = this.resume() |
|
this.stack[this.stack.length - 1].label = label |
|
this.stack[this.stack.length - 1].identifier = normalizeIdentifier( |
|
this.sliceSerialize(token) |
|
).toLowerCase() |
|
setData('referenceType', 'full') |
|
} |
|
|
|
function onexitcharacterreferencemarker(token) { |
|
setData('characterReferenceType', token.type) |
|
} |
|
|
|
function onexitcharacterreferencevalue(token) { |
|
var data = this.sliceSerialize(token) |
|
var type = getData('characterReferenceType') |
|
var value |
|
var tail |
|
|
|
if (type) { |
|
value = safeFromInt( |
|
data, |
|
type === 'characterReferenceMarkerNumeric' ? 10 : 16 |
|
) |
|
|
|
setData('characterReferenceType') |
|
} else { |
|
value = decode(data) |
|
} |
|
|
|
tail = this.stack.pop() |
|
tail.value += value |
|
tail.position.end = point(token.end) |
|
} |
|
|
|
function onexitautolinkprotocol(token) { |
|
onexitdata.call(this, token) |
|
this.stack[this.stack.length - 1].url = this.sliceSerialize(token) |
|
} |
|
|
|
function onexitautolinkemail(token) { |
|
onexitdata.call(this, token) |
|
this.stack[this.stack.length - 1].url = |
|
'mailto:' + this.sliceSerialize(token) |
|
} |
|
|
|
// |
|
// Creaters. |
|
// |
|
|
|
function blockQuote() { |
|
return {type: 'blockquote', children: []} |
|
} |
|
|
|
function codeFlow() { |
|
return {type: 'code', lang: null, meta: null, value: ''} |
|
} |
|
|
|
function codeText() { |
|
return {type: 'inlineCode', value: ''} |
|
} |
|
|
|
function definition() { |
|
return { |
|
type: 'definition', |
|
identifier: '', |
|
label: null, |
|
title: null, |
|
url: '' |
|
} |
|
} |
|
|
|
function emphasis() { |
|
return {type: 'emphasis', children: []} |
|
} |
|
|
|
function heading() { |
|
return {type: 'heading', depth: undefined, children: []} |
|
} |
|
|
|
function hardBreak() { |
|
return {type: 'break'} |
|
} |
|
|
|
function html() { |
|
return {type: 'html', value: ''} |
|
} |
|
|
|
function image() { |
|
return {type: 'image', title: null, url: '', alt: null} |
|
} |
|
|
|
function link() { |
|
return {type: 'link', title: null, url: '', children: []} |
|
} |
|
|
|
function list(token) { |
|
return { |
|
type: 'list', |
|
ordered: token.type === 'listOrdered', |
|
start: null, |
|
spread: token._spread, |
|
children: [] |
|
} |
|
} |
|
|
|
function listItem(token) { |
|
return { |
|
type: 'listItem', |
|
spread: token._spread, |
|
checked: null, |
|
children: [] |
|
} |
|
} |
|
|
|
function paragraph() { |
|
return {type: 'paragraph', children: []} |
|
} |
|
|
|
function strong() { |
|
return {type: 'strong', children: []} |
|
} |
|
|
|
function text() { |
|
return {type: 'text', value: ''} |
|
} |
|
|
|
function thematicBreak() { |
|
return {type: 'thematicBreak'} |
|
} |
|
} |
|
|
|
function configure(config, extensions) { |
|
var index = -1 |
|
|
|
while (++index < extensions.length) { |
|
extension(config, extensions[index]) |
|
} |
|
|
|
return config |
|
} |
|
|
|
function extension(config, extension) { |
|
var key |
|
var left |
|
|
|
for (key in extension) { |
|
left = own.call(config, key) ? config[key] : (config[key] = {}) |
|
|
|
if (key === 'canContainEols' || key === 'transforms') { |
|
config[key] = [].concat(left, extension[key]) |
|
} else { |
|
Object.assign(left, extension[key]) |
|
} |
|
} |
|
}
|
|
|