'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]) } } }