|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import {decodeNamedCharacterReference} from 'decode-named-character-reference' |
|
import {push} from 'micromark-util-chunked' |
|
import {combineHtmlExtensions} from 'micromark-util-combine-extensions' |
|
import {decodeNumericCharacterReference} from 'micromark-util-decode-numeric-character-reference' |
|
import {encode as _encode} from 'micromark-util-encode' |
|
import {normalizeIdentifier} from 'micromark-util-normalize-identifier' |
|
import {sanitizeUri} from 'micromark-util-sanitize-uri' |
|
const hasOwnProperty = {}.hasOwnProperty |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const protocolHref = /^(https?|ircs?|mailto|xmpp)$/i |
|
const protocolSrc = /^https?$/i |
|
|
|
|
|
|
|
|
|
|
|
export function compile(options) { |
|
const settings = options || {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let tags = true |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const definitions = {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const buffers = [[]] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mediaStack = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const tightStack = [] |
|
|
|
|
|
const defaultHandlers = { |
|
enter: { |
|
blockQuote: onenterblockquote, |
|
codeFenced: onentercodefenced, |
|
codeFencedFenceInfo: buffer, |
|
codeFencedFenceMeta: buffer, |
|
codeIndented: onentercodeindented, |
|
codeText: onentercodetext, |
|
content: onentercontent, |
|
definition: onenterdefinition, |
|
definitionDestinationString: onenterdefinitiondestinationstring, |
|
definitionLabelString: buffer, |
|
definitionTitleString: buffer, |
|
emphasis: onenteremphasis, |
|
htmlFlow: onenterhtmlflow, |
|
htmlText: onenterhtml, |
|
image: onenterimage, |
|
label: buffer, |
|
link: onenterlink, |
|
listItemMarker: onenterlistitemmarker, |
|
listItemValue: onenterlistitemvalue, |
|
listOrdered: onenterlistordered, |
|
listUnordered: onenterlistunordered, |
|
paragraph: onenterparagraph, |
|
reference: buffer, |
|
resource: onenterresource, |
|
resourceDestinationString: onenterresourcedestinationstring, |
|
resourceTitleString: buffer, |
|
setextHeading: onentersetextheading, |
|
strong: onenterstrong |
|
}, |
|
exit: { |
|
atxHeading: onexitatxheading, |
|
atxHeadingSequence: onexitatxheadingsequence, |
|
autolinkEmail: onexitautolinkemail, |
|
autolinkProtocol: onexitautolinkprotocol, |
|
blockQuote: onexitblockquote, |
|
characterEscapeValue: onexitdata, |
|
characterReferenceMarkerHexadecimal: onexitcharacterreferencemarker, |
|
characterReferenceMarkerNumeric: onexitcharacterreferencemarker, |
|
characterReferenceValue: onexitcharacterreferencevalue, |
|
codeFenced: onexitflowcode, |
|
codeFencedFence: onexitcodefencedfence, |
|
codeFencedFenceInfo: onexitcodefencedfenceinfo, |
|
codeFencedFenceMeta: resume, |
|
codeFlowValue: onexitcodeflowvalue, |
|
codeIndented: onexitflowcode, |
|
codeText: onexitcodetext, |
|
codeTextData: onexitdata, |
|
data: onexitdata, |
|
definition: onexitdefinition, |
|
definitionDestinationString: onexitdefinitiondestinationstring, |
|
definitionLabelString: onexitdefinitionlabelstring, |
|
definitionTitleString: onexitdefinitiontitlestring, |
|
emphasis: onexitemphasis, |
|
hardBreakEscape: onexithardbreak, |
|
hardBreakTrailing: onexithardbreak, |
|
htmlFlow: onexithtml, |
|
htmlFlowData: onexitdata, |
|
htmlText: onexithtml, |
|
htmlTextData: onexitdata, |
|
image: onexitmedia, |
|
label: onexitlabel, |
|
labelText: onexitlabeltext, |
|
lineEnding: onexitlineending, |
|
link: onexitmedia, |
|
listOrdered: onexitlistordered, |
|
listUnordered: onexitlistunordered, |
|
paragraph: onexitparagraph, |
|
reference: resume, |
|
referenceString: onexitreferencestring, |
|
resource: resume, |
|
resourceDestinationString: onexitresourcedestinationstring, |
|
resourceTitleString: onexitresourcetitlestring, |
|
setextHeading: onexitsetextheading, |
|
setextHeadingLineSequence: onexitsetextheadinglinesequence, |
|
setextHeadingText: onexitsetextheadingtext, |
|
strong: onexitstrong, |
|
thematicBreak: onexitthematicbreak |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handlers = |
|
|
|
combineHtmlExtensions( |
|
[defaultHandlers].concat(settings.htmlExtensions || []) |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const data = { |
|
tightStack, |
|
definitions |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const context = { |
|
lineEndingIfNeeded, |
|
options: settings, |
|
encode, |
|
raw, |
|
tag, |
|
buffer, |
|
resume, |
|
setData, |
|
getData |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let lineEndingStyle = settings.defaultLineEnding |
|
|
|
|
|
return compile |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function compile(events) { |
|
let index = -1 |
|
let start = 0 |
|
|
|
const listStack = [] |
|
|
|
|
|
|
|
|
|
|
|
let head = [] |
|
|
|
let body = [] |
|
while (++index < events.length) { |
|
|
|
if ( |
|
!lineEndingStyle && |
|
(events[index][1].type === 'lineEnding' || |
|
events[index][1].type === 'lineEndingBlank') |
|
) { |
|
|
|
lineEndingStyle = events[index][2].sliceSerialize(events[index][1]) |
|
} |
|
|
|
|
|
if ( |
|
events[index][1].type === 'listOrdered' || |
|
events[index][1].type === 'listUnordered' |
|
) { |
|
if (events[index][0] === 'enter') { |
|
listStack.push(index) |
|
} else { |
|
prepareList(events.slice(listStack.pop(), index)) |
|
} |
|
} |
|
|
|
|
|
if (events[index][1].type === 'definition') { |
|
if (events[index][0] === 'enter') { |
|
body = push(body, events.slice(start, index)) |
|
start = index |
|
} else { |
|
head = push(head, events.slice(start, index + 1)) |
|
start = index + 1 |
|
} |
|
} |
|
} |
|
head = push(head, body) |
|
head = push(head, events.slice(start)) |
|
index = -1 |
|
const result = head |
|
|
|
|
|
if (handlers.enter.null) { |
|
handlers.enter.null.call(context) |
|
} |
|
|
|
|
|
while (++index < events.length) { |
|
const handles = handlers[result[index][0]] |
|
const kind = result[index][1].type |
|
const handle = handles[kind] |
|
if (hasOwnProperty.call(handles, kind) && handle) { |
|
handle.call( |
|
Object.assign( |
|
{ |
|
sliceSerialize: result[index][2].sliceSerialize |
|
}, |
|
context |
|
), |
|
result[index][1] |
|
) |
|
} |
|
} |
|
|
|
|
|
if (handlers.exit.null) { |
|
handlers.exit.null.call(context) |
|
} |
|
return buffers[0].join('') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function prepareList(slice) { |
|
const length = slice.length |
|
let index = 0 |
|
let containerBalance = 0 |
|
let loose = false |
|
|
|
let atMarker |
|
while (++index < length) { |
|
const event = slice[index] |
|
if (event[1]._container) { |
|
atMarker = undefined |
|
if (event[0] === 'enter') { |
|
containerBalance++ |
|
} else { |
|
containerBalance-- |
|
} |
|
} else |
|
switch (event[1].type) { |
|
case 'listItemPrefix': { |
|
if (event[0] === 'exit') { |
|
atMarker = true |
|
} |
|
break |
|
} |
|
case 'linePrefix': { |
|
|
|
|
|
break |
|
} |
|
case 'lineEndingBlank': { |
|
if (event[0] === 'enter' && !containerBalance) { |
|
if (atMarker) { |
|
atMarker = undefined |
|
} else { |
|
loose = true |
|
} |
|
} |
|
break |
|
} |
|
default: { |
|
atMarker = undefined |
|
} |
|
} |
|
} |
|
slice[0][1]._loose = loose |
|
} |
|
|
|
|
|
|
|
|
|
function setData(key, value) { |
|
|
|
|
|
data[key] = value |
|
} |
|
|
|
|
|
|
|
|
|
function getData(key) { |
|
return data[key] |
|
} |
|
|
|
|
|
function buffer() { |
|
buffers.push([]) |
|
} |
|
|
|
|
|
function resume() { |
|
const buf = buffers.pop() |
|
return buf.join('') |
|
} |
|
|
|
|
|
function tag(value) { |
|
if (!tags) return |
|
setData('lastWasTag', true) |
|
buffers[buffers.length - 1].push(value) |
|
} |
|
|
|
|
|
function raw(value) { |
|
setData('lastWasTag') |
|
buffers[buffers.length - 1].push(value) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function lineEnding() { |
|
raw(lineEndingStyle || '\n') |
|
} |
|
|
|
|
|
function lineEndingIfNeeded() { |
|
const buffer = buffers[buffers.length - 1] |
|
const slice = buffer[buffer.length - 1] |
|
const previous = slice ? slice.charCodeAt(slice.length - 1) : null |
|
if (previous === 10 || previous === 13 || previous === null) { |
|
return |
|
} |
|
lineEnding() |
|
} |
|
|
|
|
|
function encode(value) { |
|
return getData('ignoreEncode') ? value : _encode(value) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function onenterlistordered(token) { |
|
tightStack.push(!token._loose) |
|
lineEndingIfNeeded() |
|
tag('<ol') |
|
setData('expectFirstItem', true) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterlistunordered(token) { |
|
tightStack.push(!token._loose) |
|
lineEndingIfNeeded() |
|
tag('<ul') |
|
setData('expectFirstItem', true) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterlistitemvalue(token) { |
|
if (getData('expectFirstItem')) { |
|
const value = Number.parseInt(this.sliceSerialize(token), 10) |
|
if (value !== 1) { |
|
tag(' start="' + encode(String(value)) + '"') |
|
} |
|
} |
|
} |
|
function onenterlistitemmarker() { |
|
if (getData('expectFirstItem')) { |
|
tag('>') |
|
} else { |
|
onexitlistitem() |
|
} |
|
lineEndingIfNeeded() |
|
tag('<li>') |
|
setData('expectFirstItem') |
|
|
|
setData('lastWasTag') |
|
} |
|
function onexitlistordered() { |
|
onexitlistitem() |
|
tightStack.pop() |
|
lineEnding() |
|
tag('</ol>') |
|
} |
|
function onexitlistunordered() { |
|
onexitlistitem() |
|
tightStack.pop() |
|
lineEnding() |
|
tag('</ul>') |
|
} |
|
function onexitlistitem() { |
|
if (getData('lastWasTag') && !getData('slurpAllLineEndings')) { |
|
lineEndingIfNeeded() |
|
} |
|
tag('</li>') |
|
setData('slurpAllLineEndings') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterblockquote() { |
|
tightStack.push(false) |
|
lineEndingIfNeeded() |
|
tag('<blockquote>') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitblockquote() { |
|
tightStack.pop() |
|
lineEndingIfNeeded() |
|
tag('</blockquote>') |
|
setData('slurpAllLineEndings') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterparagraph() { |
|
if (!tightStack[tightStack.length - 1]) { |
|
lineEndingIfNeeded() |
|
tag('<p>') |
|
} |
|
setData('slurpAllLineEndings') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitparagraph() { |
|
if (tightStack[tightStack.length - 1]) { |
|
setData('slurpAllLineEndings', true) |
|
} else { |
|
tag('</p>') |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onentercodefenced() { |
|
lineEndingIfNeeded() |
|
tag('<pre><code') |
|
setData('fencesCount', 0) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitcodefencedfenceinfo() { |
|
const value = resume() |
|
tag(' class="language-' + value + '"') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitcodefencedfence() { |
|
const count = getData('fencesCount') || 0 |
|
if (!count) { |
|
tag('>') |
|
setData('slurpOneLineEnding', true) |
|
} |
|
setData('fencesCount', count + 1) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onentercodeindented() { |
|
lineEndingIfNeeded() |
|
tag('<pre><code>') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitflowcode() { |
|
const count = getData('fencesCount') |
|
|
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
count !== undefined && |
|
count < 2 && |
|
data.tightStack.length > 0 && |
|
!getData('lastWasTag') |
|
) { |
|
lineEnding() |
|
} |
|
|
|
|
|
|
|
if (getData('flowCodeSeenData')) { |
|
lineEndingIfNeeded() |
|
} |
|
tag('</code></pre>') |
|
if (count !== undefined && count < 2) lineEndingIfNeeded() |
|
setData('flowCodeSeenData') |
|
setData('fencesCount') |
|
setData('slurpOneLineEnding') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterimage() { |
|
mediaStack.push({ |
|
image: true |
|
}) |
|
tags = undefined |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterlink() { |
|
mediaStack.push({}) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitlabeltext(token) { |
|
mediaStack[mediaStack.length - 1].labelId = this.sliceSerialize(token) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitlabel() { |
|
mediaStack[mediaStack.length - 1].label = resume() |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitreferencestring(token) { |
|
mediaStack[mediaStack.length - 1].referenceId = this.sliceSerialize(token) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterresource() { |
|
buffer() |
|
mediaStack[mediaStack.length - 1].destination = '' |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterresourcedestinationstring() { |
|
buffer() |
|
|
|
|
|
setData('ignoreEncode', true) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitresourcedestinationstring() { |
|
mediaStack[mediaStack.length - 1].destination = resume() |
|
setData('ignoreEncode') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitresourcetitlestring() { |
|
mediaStack[mediaStack.length - 1].title = resume() |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitmedia() { |
|
let index = mediaStack.length - 1 |
|
const media = mediaStack[index] |
|
const id = media.referenceId || media.labelId |
|
const context = |
|
media.destination === undefined |
|
? definitions[normalizeIdentifier(id)] |
|
: media |
|
tags = true |
|
while (index--) { |
|
if (mediaStack[index].image) { |
|
tags = undefined |
|
break |
|
} |
|
} |
|
if (media.image) { |
|
tag( |
|
'<img src="' + |
|
sanitizeUri( |
|
context.destination, |
|
settings.allowDangerousProtocol ? undefined : protocolSrc |
|
) + |
|
'" alt="' |
|
) |
|
raw(media.label) |
|
tag('"') |
|
} else { |
|
tag( |
|
'<a href="' + |
|
sanitizeUri( |
|
context.destination, |
|
settings.allowDangerousProtocol ? undefined : protocolHref |
|
) + |
|
'"' |
|
) |
|
} |
|
tag(context.title ? ' title="' + context.title + '"' : '') |
|
if (media.image) { |
|
tag(' />') |
|
} else { |
|
tag('>') |
|
raw(media.label) |
|
tag('</a>') |
|
} |
|
mediaStack.pop() |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterdefinition() { |
|
buffer() |
|
mediaStack.push({}) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitdefinitionlabelstring(token) { |
|
|
|
resume() |
|
mediaStack[mediaStack.length - 1].labelId = this.sliceSerialize(token) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onenterdefinitiondestinationstring() { |
|
buffer() |
|
setData('ignoreEncode', true) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitdefinitiondestinationstring() { |
|
mediaStack[mediaStack.length - 1].destination = resume() |
|
setData('ignoreEncode') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitdefinitiontitlestring() { |
|
mediaStack[mediaStack.length - 1].title = resume() |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitdefinition() { |
|
const media = mediaStack[mediaStack.length - 1] |
|
const id = normalizeIdentifier(media.labelId) |
|
resume() |
|
if (!hasOwnProperty.call(definitions, id)) { |
|
definitions[id] = mediaStack[mediaStack.length - 1] |
|
} |
|
mediaStack.pop() |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onentercontent() { |
|
setData('slurpAllLineEndings', true) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitatxheadingsequence(token) { |
|
|
|
if (getData('headingRank')) return |
|
setData('headingRank', this.sliceSerialize(token).length) |
|
lineEndingIfNeeded() |
|
tag('<h' + getData('headingRank') + '>') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onentersetextheading() { |
|
buffer() |
|
setData('slurpAllLineEndings') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitsetextheadingtext() { |
|
setData('slurpAllLineEndings', true) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitatxheading() { |
|
tag('</h' + getData('headingRank') + '>') |
|
setData('headingRank') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitsetextheadinglinesequence(token) { |
|
setData( |
|
'headingRank', |
|
this.sliceSerialize(token).charCodeAt(0) === 61 ? 1 : 2 |
|
) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitsetextheading() { |
|
const value = resume() |
|
lineEndingIfNeeded() |
|
tag('<h' + getData('headingRank') + '>') |
|
raw(value) |
|
tag('</h' + getData('headingRank') + '>') |
|
setData('slurpAllLineEndings') |
|
setData('headingRank') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitdata(token) { |
|
raw(encode(this.sliceSerialize(token))) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitlineending(token) { |
|
if (getData('slurpAllLineEndings')) { |
|
return |
|
} |
|
if (getData('slurpOneLineEnding')) { |
|
setData('slurpOneLineEnding') |
|
return |
|
} |
|
if (getData('inCodeText')) { |
|
raw(' ') |
|
return |
|
} |
|
raw(encode(this.sliceSerialize(token))) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitcodeflowvalue(token) { |
|
raw(encode(this.sliceSerialize(token))) |
|
setData('flowCodeSeenData', true) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexithardbreak() { |
|
tag('<br />') |
|
} |
|
function onenterhtmlflow() { |
|
lineEndingIfNeeded() |
|
onenterhtml() |
|
} |
|
function onexithtml() { |
|
setData('ignoreEncode') |
|
} |
|
function onenterhtml() { |
|
if (settings.allowDangerousHtml) { |
|
setData('ignoreEncode', true) |
|
} |
|
} |
|
function onenteremphasis() { |
|
tag('<em>') |
|
} |
|
function onenterstrong() { |
|
tag('<strong>') |
|
} |
|
function onentercodetext() { |
|
setData('inCodeText', true) |
|
tag('<code>') |
|
} |
|
function onexitcodetext() { |
|
setData('inCodeText') |
|
tag('</code>') |
|
} |
|
function onexitemphasis() { |
|
tag('</em>') |
|
} |
|
function onexitstrong() { |
|
tag('</strong>') |
|
} |
|
function onexitthematicbreak() { |
|
lineEndingIfNeeded() |
|
tag('<hr />') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitcharacterreferencemarker(token) { |
|
setData('characterReferenceType', token.type) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitcharacterreferencevalue(token) { |
|
let value = this.sliceSerialize(token) |
|
|
|
|
|
|
|
|
|
value = getData('characterReferenceType') |
|
? decodeNumericCharacterReference( |
|
value, |
|
getData('characterReferenceType') === |
|
'characterReferenceMarkerNumeric' |
|
? 10 |
|
: 16 |
|
) |
|
: decodeNamedCharacterReference(value) |
|
raw(encode(value)) |
|
setData('characterReferenceType') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitautolinkprotocol(token) { |
|
const uri = this.sliceSerialize(token) |
|
tag( |
|
'<a href="' + |
|
sanitizeUri( |
|
uri, |
|
settings.allowDangerousProtocol ? undefined : protocolHref |
|
) + |
|
'">' |
|
) |
|
raw(encode(uri)) |
|
tag('</a>') |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onexitautolinkemail(token) { |
|
const uri = this.sliceSerialize(token) |
|
tag('<a href="' + sanitizeUri('mailto:' + uri) + '">') |
|
raw(encode(uri)) |
|
tag('</a>') |
|
} |
|
} |
|
|