tags around block-level tags. text = showdown.subParser('hashHTMLBlocks')(text, options, globals) text = showdown.subParser('paragraphs')(text, options, globals) return text }) showdown.subParser('blockQuotes', (text: string, options: any, globals: any): string => { // add a couple extra lines after the text and endtext mark text = `${text}\n\n` let rgx = /(^ {0,3}>.+\n(.+\n)*\n*)+/gm if (options.splitAdjacentBlockquotes) { rgx = /^ {0,3}>[\s\S]*?\n\n/gm } text = text.replace(rgx, (bq) => { // attacklab: hack around Konqueror 3.5.4 bug: // "----------bug".replace(/^-/g,"") == "bug" bq = bq.replace(/^[ \t]*>[ \t]?/gm, '') // trim one level of quoting // attacklab: clean up hack bq = bq.replace(/¨0/g, '') bq = bq.replace(/^[ \t]+$/gm, '') // trim whitespace-only lines bq = showdown.subParser('githubCodeBlocks')(bq, options, globals) bq = showdown.subParser('blockGamut')(bq, options, globals) // recurse bq = bq.replace(/(^|\n)/g, '$1 ') // These leading spaces screw with
content, so we need to fix that:
bq = bq.replace(/(\s*[^\r]+?<\/pre>)/g, (wholeMatch, m1) => {
let pre = m1
// attacklab: hack around Konqueror 3.5.4 bug:
pre = pre.replace(/^ {2}/gm, '¨0')
pre = pre.replace(/¨0/g, '')
return pre
})
return showdown.subParser('hashBlock')(`\n${bq}\n
`, options, globals)
})
return text
})
/**
* Process Markdown `` blocks.
*/
showdown.subParser('codeBlocks', (text: string, options: any, globals: any): string => {
// sentinel workarounds for lack of \A and \Z, safari\khtml bug
text += '¨0'
let pattern = /(?:\n\n|^)((?:(?: {4}|\t).*\n+)+)(\n* {0,3}[^ \t\n]|(?=¨0))/g
text = text.replace(pattern, (wholeMatch, m1, m2) => {
let codeblock = m1
let nextChar = m2
let end = '\n'
codeblock = showdown.subParser('outdent')(codeblock, options, globals)
codeblock = showdown.subParser('encodeCode')(codeblock, options, globals)
codeblock = showdown.subParser('detab')(codeblock, options, globals)
codeblock = codeblock.replace(/^\n+/g, '') // trim leading newlines
codeblock = codeblock.replace(/\n+$/g, '') // trim trailing newlines
if (options.omitExtraWLInCodeBlocks) {
end = ''
}
codeblock = `${codeblock}${end}
`
return showdown.subParser('hashBlock')(codeblock, options, globals) + nextChar
})
// strip sentinel
text = text.replace(/¨0/, '')
return text
})
/**
*
* Backtick quotes are used for spans.
*
* You can use multiple backticks as the delimiters if you want to
* include literal backticks in the code span. So, this input:
*
* Just type ``foo `bar` baz`` at the prompt.
*
* Will translate to:
*
* Just type foo `bar` baz at the prompt.
*
* There's no arbitrary limit to the number of backticks you
* can use as delimters. If you need three consecutive backticks
* in your code, use four for delimiters, etc.
*
* You can use spaces to get literal backticks at the edges:
*
* ... type `` `bar` `` ...
*
* Turns to:
*
* ... type `bar` ...
*/
showdown.subParser('codeSpans', (text: string, options: any, globals: any): string => {
if (typeof (text) === 'undefined') {
text = ''
}
text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, (wholeMatch, m1, m2, m3) => {
let c = m3
c = c.replace(/^([ \t]*)/g, '') // leading whitespace
c = c.replace(/[ \t]*$/g, '') // trailing whitespace
c = showdown.subParser('encodeCode')(c, options, globals)
c = `${m1}${c}`
c = showdown.subParser('hashHTMLSpans')(c, options, globals)
return c
})
return text
})
/**
* Create a full HTML document from the processed markdown
*/
showdown.subParser('completeHTMLDocument', (text: string, options: any, globals: any): string => {
if (!options.completeHTMLDocument) {
return text
}
let doctype = 'html'
let doctypeParsed = '\n'
let title = ''
let charset = '\n'
let lang = ''
let metadata = ''
if (typeof globals.metadata.parsed.doctype !== 'undefined') {
doctypeParsed = `\n`
doctype = globals.metadata.parsed.doctype.toString().toLowerCase()
if (doctype === 'html' || doctype === 'html5') {
charset = ''
}
}
for (let meta in globals.metadata.parsed) {
if (globals.metadata.parsed.hasOwnProperty(meta)) {
switch (meta.toLowerCase()) {
case 'doctype':
break
case 'title':
title = `${globals.metadata.parsed.title} \n`
break
case 'charset':
if (doctype === 'html' || doctype === 'html5') {
charset = `\n`
} else {
charset = `\n`
}
break
case 'language':
case 'lang':
lang = ` lang="${globals.metadata.parsed[meta]}"`
metadata += `\n`
break
default:
metadata += `\n`
}
}
}
text = `${doctypeParsed}\n\n${title}${charset}${metadata}\n\n${text.trim()}\n\n`
return text
})
/**
* Convert all tabs to spaces
*/
showdown.subParser('detab', (text: string, options: any, globals: any): string => {
// expand first n-1 tabs
text = text.replace(/\t(?=\t)/g, ' ') // g_tab_width
// replace the nth with two sentinels
text = text.replace(/\t/g, '¨A¨B')
// use the sentinel to anchor our regex so it doesn't explode
text = text.replace(/¨B(.+?)¨A/g, (wholeMatch, m1) => {
let leadingText = m1
let numSpaces = 4 - leadingText.length % 4 // g_tab_width
// there *must* be a better way to do this:
for (let i = 0; i < numSpaces; i++) {
leadingText += ' '
}
return leadingText
})
// clean up sentinels
text = text.replace(/¨A/g, ' ') // g_tab_width
text = text.replace(/¨B/g, '')
return text
})
showdown.subParser('ellipsis', (text: string, options: any, globals: any): string => {
if (!options.ellipsis) {
return text
}
text = text.replace(/\.\.\./g, '…')
return text
})
/**
* Smart processing for ampersands and angle brackets that need to be encoded.
*/
showdown.subParser('encodeAmpsAndAngles', (text: string, options: any, globals: any): string => {
// Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
// http://bumppo.net/projects/amputator/
text = text.replace(/&(?!#?x?(?:[0-9a-f]+|\w+);)/gi, '&')
// Encode naked <'s
text = text.replace(/<(?![a-z/?$!])/gi, '<')
// Encode <
text = text.replace(/
text = text.replace(/>/g, '>')
return text
})
/**
* Returns the string, with after processing the following backslash escape sequences.
*
* attacklab: The polite way to do this is with the new escapeCharacters() function:
*
* text = escapeCharacters(text,"\\",true);
* text = escapeCharacters(text,"`*_{}[]()>#+-.!",true);
*
* ...but we're sidestepping its use of the (slow) RegExp constructor
* as an optimization for Firefox. This function gets called a LOT.
*/
showdown.subParser('encodeBackslashEscapes', (text: string, options: any, globals: any): string => {
text = text.replace(/\\(\\)/g, showdown.helper.escapeCharactersCallback)
text = text.replace(/\\([`*_{}[\]()>#+.!~=|:-])/g, showdown.helper.escapeCharactersCallback)
return text
})
/**
* Encode/escape certain characters inside Markdown code runs.
* The point is that in code, these characters are literals,
* and lose their special Markdown meanings.
*/
showdown.subParser('encodeCode', (text: string, options: any, globals: any): string => {
// Encode all ampersands; HTML entities are not
// entities within a Markdown code span.
text = text
.replace(/&/g, '&')
// Do the angle bracket song and dance:
.replace(//g, '>')
// Now, escape characters that are magic in Markdown:
.replace(/([*_{}[\]\\=~-])/g, showdown.helper.escapeCharactersCallback)
return text
})
/**
* Within tags -- meaning between < and > -- encode [\ ` * _ ~ =] so they
* don't conflict with their use in Markdown for code, italics and strong.
*/
showdown.subParser('escapeSpecialCharsWithinTagAttributes', (text: string, options: any, globals: any): string => {
// Build a regex to find HTML tags.
let tags = /<\/?[\w:-]+(?:\s[\s\S]+?)?>/g
let comments = /-]|-[^>])(?:[^-]|-[^-])*--)>/g
text = text.replace(tags, (wholeMatch) => {
return wholeMatch
.replace(/(.)<\/?code>(?=.)/g, '$1`')
.replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback)
})
text = text.replace(comments, (wholeMatch) => {
return wholeMatch
.replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback)
})
return text
})
/**
* Handle github codeblocks prior to running HashHTML so that
* HTML contained within the codeblock gets escaped properly
* Example:
* ```ruby
* def hello_world(x)
* puts "Hello, #{x}"
* end
* ```
*/
showdown.subParser('githubCodeBlocks', (text: string, options: any, globals: any): string => {
// early exit if option is not enabled
if (!options.ghCodeBlocks) {
return text
}
text += '¨0'
text = text.replace(/(?:^|\n) {0,3}(`{3,}|~{3,}) *([^\s`~]*)\n([\s\S]*?)\n {0,3}\1/g, (wholeMatch, delim, language, codeblock) => {
let end = (options.omitExtraWLInCodeBlocks) ? '' : '\n'
// First parse the github code block
codeblock = showdown.subParser('encodeCode')(codeblock, options, globals)
codeblock = showdown.subParser('detab')(codeblock, options, globals)
codeblock = codeblock.replace(/^\n+/g, '') // trim leading newlines
codeblock = codeblock.replace(/\n+$/g, '') // trim trailing whitespace
codeblock = `${codeblock}${end}
`
codeblock = showdown.subParser('hashBlock')(codeblock, options, globals)
// Since GHCodeblocks can be false positives, we need to
// store the primitive text and the parsed text in a global var,
// and then return a token
return `\n\n¨G${globals.ghCodeBlocks.push({ text: wholeMatch, codeblock }) - 1}G\n\n`
})
// attacklab: strip sentinel
text = text.replace(/¨0/, '')
return text
})
showdown.subParser('hashBlock', (text: string, options: any, globals: any): string => {
text = text.replace(/(^\n+|\n+$)/g, '')
text = `\n\n¨K${globals.gHtmlBlocks.push(text) - 1}K\n\n`
return text
})
/**
* Hash and escape elements that should not be parsed as markdown
*/
showdown.subParser('hashCodeTags', (text: string, options: any, globals: any): string => {
let repFunc = function (wholeMatch: string, match: string, left: string, right: string): string {
let codeblock = left + showdown.subParser('encodeCode')(match, options, globals) + right
return `¨C${globals.gHtmlSpans.push(codeblock) - 1}C`
}
// Hash naked
text = showdown.helper.replaceRecursiveRegExp(text, repFunc, ']*>', '', 'gim')
return text
})
showdown.subParser('hashElement', (text: string, options: any, globals: any): ((wholeMatch: string, m1: string) => string) => {
return function (wholeMatch: string, m1: string): string {
let blockText = m1
// Undo double lines
blockText = blockText.replace(/\n\n/g, '\n')
blockText = blockText.replace(/^\n/, '')
// strip trailing blank lines
blockText = blockText.replace(/\n+$/g, '')
// Replace the element text with a marker ("¨KxK" where x is its key)
blockText = `\n\n¨K${globals.gHtmlBlocks.push(blockText) - 1}K\n\n`
return blockText
}
})
showdown.subParser('hashHTMLBlocks', (text: string, options: any, globals: any): string => {
let blockTags = [
'pre',
'div',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote',
'table',
'dl',
'ol',
'ul',
'script',
'noscript',
'form',
'fieldset',
'iframe',
'math',
'style',
'section',
'header',
'footer',
'nav',
'article',
'aside',
'address',
'audio',
'canvas',
'figure',
'hgroup',
'output',
'video',
'p'
]
let repFunc = function (wholeMatch: string, match: string, left: string, right: string): string {
let txt = wholeMatch
// check if this html element is marked as markdown
// if so, it's contents should be parsed as markdown
if (left.search(/\bmarkdown\b/) !== -1) {
txt = left + globals.converter.makeHtml(match) + right
}
return `\n\n¨K${globals.gHtmlBlocks.push(txt) - 1}K\n\n`
}
if (options.backslashEscapesHTMLTags) {
// encode backslash escaped HTML tags
text = text.replace(/\\<([^>]+)>/g, (wm, inside) => {
return `<${inside}>`
})
}
// hash HTML Blocks
for (let i = 0; i < blockTags.length; ++i) {
var opTagPos
let rgx1 = new RegExp(`^ {0,3}(<${blockTags[i]}\\b[^>]*>)`, 'im')
let patLeft = `<${blockTags[i]}\\b[^>]*>`
let patRight = `${blockTags[i]}>`
// 1. Look for the first position of the first opening HTML tag in the text
while ((opTagPos = showdown.helper.regexIndexOf(text, rgx1)) !== -1) {
// if the HTML tag is \ escaped, we need to escape it and break
// 2. Split the text in that position
let subTexts = showdown.helper.splitAtIndex(text, opTagPos)
// 3. Match recursively
let newSubText1 = showdown.helper.replaceRecursiveRegExp(subTexts[1], repFunc, patLeft, patRight, 'im')
// prevent an infinite loop
if (newSubText1 === subTexts[1]) {
break
}
text = subTexts[0].concat(newSubText1)
}
}
// HR SPECIAL CASE
text = text.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, showdown.subParser('hashElement')(text, options, globals))
// Special case for standalone HTML comments
text = showdown.helper.replaceRecursiveRegExp(text, (txt: string): string => {
return `\n\n¨K${globals.gHtmlBlocks.push(txt) - 1}K\n\n`
}, '^ {0,3}', 'gm')
// PHP and ASP-style processor instructions (...?> and <%...%>)
text = text.replace(/\n\n( {0,3}<([?%])[^\r]*?\2>[ \t]*(?=\n{2,}))/g, showdown.subParser('hashElement')(text, options, globals))
return text
})
/**
* Hash span elements that should not be parsed as markdown
*/
showdown.subParser('hashHTMLSpans', (text: string, options: any, globals: any): string => {
function hashHTMLSpan(html: string): string {
return `¨C${globals.gHtmlSpans.push(html) - 1}C`
}
// Hash Self Closing tags
text = text.replace(/<[^>]+?\/>/g, (wm: string): string => {
return hashHTMLSpan(wm)
})
// Hash tags without properties
text = text.replace(/<([^>]+)>[\s\S]*?<\/\1>/g, (wm) => {
return hashHTMLSpan(wm)
})
// Hash tags with properties
text = text.replace(/<([^>]+?)\s[^>]+>[\s\S]*?<\/\1>/g, (wm) => {
return hashHTMLSpan(wm)
})
// Hash self closing tags without />
text = text.replace(/<[^>]+>/g, (wm) => {
return hashHTMLSpan(wm)
})
/* showdown.helper.matchRecursiveRegExp(text, ']*>', '', 'gi'); */
return text
})
/**
* Unhash HTML spans
*/
showdown.subParser('unhashHTMLSpans', (text: string, options: any, globals: any): string => {
for (let i = 0; i < globals.gHtmlSpans.length; ++i) {
let repText = globals.gHtmlSpans[i]
// limiter to prevent infinite loop (assume 10 as limit for recurse)
let limit = 0
while (/¨C(\d+)C/.test(repText)) {
let num = RegExp.$1
repText = repText.replace(`¨C${num}C`, globals.gHtmlSpans[num])
if (limit === 10) {
console.error('maximum nesting of 10 spans reached!!!')
break
}
++limit
}
text = text.replace(`¨C${i}C`, repText)
}
return text
})
/**
* Hash and escape elements that should not be parsed as markdown
*/
showdown.subParser('hashPreCodeTags', (text: string, options: any, globals: any): string => {
let repFunc = function (wholeMatch: string, match: string, left: string, right: string): string {
// encode html entities
let codeblock = left + showdown.subParser('encodeCode')(match, options, globals) + right
return `\n\n¨G${globals.ghCodeBlocks.push({ text: wholeMatch, codeblock }) - 1}G\n\n`
}
// Hash
text = showdown.helper.replaceRecursiveRegExp(text, repFunc, '^ {0,3}]*>\\s*]*>', '^ {0,3}\\s*
', 'gim')
return text
})
showdown.subParser('headers', (text: string, options: any, globals: any): string => {
let headerLevelStart = (isNaN(Number.parseInt(options.headerLevelStart))) ? 1 : Number.parseInt(options.headerLevelStart)
// Set text-style headers:
// Header 1
// ========
//
// Header 2
// --------
//
let setextRegexH1 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n={2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n=+[ \t]*\n+/gm
let setextRegexH2 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n-+[ \t]*\n+/gm
text = text.replace(setextRegexH1, (wholeMatch, m1) => {
let spanGamut = showdown.subParser('spanGamut')(m1, options, globals)
let hID = (options.noHeaderId) ? '' : ` id="${headerId(m1)}"`
let hLevel = headerLevelStart
let hashBlock = `${spanGamut} `
return showdown.subParser('hashBlock')(hashBlock, options, globals)
})
text = text.replace(setextRegexH2, (matchFound, m1) => {
let spanGamut = showdown.subParser('spanGamut')(m1, options, globals)
let hID = (options.noHeaderId) ? '' : ` id="${headerId(m1)}"`
let hLevel = headerLevelStart + 1
let hashBlock = `${spanGamut} `
return showdown.subParser('hashBlock')(hashBlock, options, globals)
})
// atx-style headers:
// # Header 1
// ## Header 2
// ## Header 2 with closing hashes ##
// ...
// ###### Header 6
//
let atxStyle = (options.requireSpaceBeforeHeadingText) ? /^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm : /^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm
text = text.replace(atxStyle, (_wholeMatch, m1, m2) => {
let hText = m2
if (options.customizedHeaderId) {
hText = m2.replace(/\s?\{([^{]+?)\}\s*$/, '')
}
let span = showdown.subParser('spanGamut')(hText, options, globals)
let hID = (options.noHeaderId) ? '' : ` id="${headerId(m2)}"`
let hLevel = headerLevelStart - 1 + m1.length
let header = `${span} `
return showdown.subParser('hashBlock')(header, options, globals)
})
function headerId(m: string): string {
let title: string = ''
let prefix: string
// It is separate from other options to allow combining prefix and customized
if (options.customizedHeaderId) {
let match = m.match(/\{([^{]+?)\}\s*$/)
if (match && match[1]) {
m = match[1]
}
}
title = m
// Prefix id to prevent causing inadvertent pre-existing style matches.
if (showdown.helper.isString(options.prefixHeaderId)) {
prefix = options.prefixHeaderId
} else if (options.prefixHeaderId === true) {
prefix = 'section-'
} else {
prefix = ''
}
if (!options.rawPrefixHeaderId) {
title = prefix + title
}
if (options.ghCompatibleHeaderId) {
title = title
.replace(/ /g, '-')
// replace previously escaped chars (&, ¨ and $)
.replace(/&/g, '')
.replace(/¨T/g, '')
.replace(/¨D/g, '')
// replace rest of the chars (&~$ are repeated as they might have been escaped)
// borrowed from github's redcarpet (some they should produce similar results)
.replace(/[&+$,/:;=?@"#{}|^¨~[\]`\\*)(%.!'<>]/g, '')
.toLowerCase()
} else if (options.rawHeaderId) {
title = title
.replace(/ /g, '-')
// replace previously escaped chars (&, ¨ and $)
.replace(/&/g, '&')
.replace(/¨T/g, '¨')
.replace(/¨D/g, '$')
// replace " and '
.replace(/["']/g, '-')
.toLowerCase()
} else {
title = title
.replace(/\W/g, '')
.toLowerCase()
}
if (options.rawPrefixHeaderId) {
title = prefix + title
}
if (globals.hashLinkCounts[title]) {
title = `${title}-${globals.hashLinkCounts[title]++}`
} else {
globals.hashLinkCounts[title] = 1
}
return title
}
return text
})
/**
* Turn Markdown link shortcuts into XHTML tags.
*/
showdown.subParser('horizontalRule', (text: string, options: any, globals: any): string => {
let key = showdown.subParser('hashBlock')('
', options, globals)
text = text.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm, key)
text = text.replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm, key)
text = text.replace(/^ {0,2}( ?_){3,}[ \t]*$/gm, key)
return text
})
/**
* Turn Markdown image shortcuts into
tags.
*/
showdown.subParser('images', (text: string, options: any, globals: any): string => {
let inlineRegExp = /!\[([^\]]*)\][ \t]*()\([ \t]?(\S+?(?:\(\S*?\)\S*?)?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g
let crazyRegExp = /!\[([^\]]*)\][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g
let base64RegExp = /!\[([^\]]*)\][ \t]*()\([ \t]?(data:.+?\/.+?;base64,[A-Za-z0-9+/=\n]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g
let referenceRegExp = /!\[([^\]]*)\] ?(?:\n *)?\[([\s\S]*?)\]()()()()()/g
let refShortcutRegExp = /!\[([^[\]]+)\]()()()()()/g
function writeImageTagBase64(wholeMatch: string, altText: string, linkId: string, url: string, width: string, height: string, m5: string, title: string): string {
url = url.replace(/\s/g, '')
return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title)
}
function writeImageTag(wholeMatch: string, altText: string, linkId: string, url: string, width: string, height: string, m5: string, title: string): string {
let { gUrls } = globals
let { gTitles } = globals
let gDims = globals.gDimensions
linkId = linkId.toLowerCase()
if (!title) {
title = ''
}
// Special case for explicit empty url
if (wholeMatch.search(/\(\s*>? ?(['"].*['"])?\)$/m) > -1) {
url = ''
} else if (url === '' || url === null) {
if (linkId === '' || linkId === null) {
// lower-case and turn embedded newlines into spaces
linkId = altText.toLowerCase().replace(/ ?\n/g, ' ')
}
url = `#${linkId}`
if (!showdown.helper.isUndefined(gUrls[linkId])) {
url = gUrls[linkId]
if (!showdown.helper.isUndefined(gTitles[linkId])) {
title = gTitles[linkId]
}
if (!showdown.helper.isUndefined(gDims[linkId])) {
width = gDims[linkId].width
height = gDims[linkId].height
}
} else {
return wholeMatch
}
}
altText = altText
.replace(/"/g, '"')
// altText = showdown.helper.escapeCharacters(altText, '*_', false);
.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback)
// url = showdown.helper.escapeCharacters(url, '*_', false);
url = url.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback)
let result = `
'
return result
}
// First, handle reference-style labeled images: ![alt text][id]
text = text.replace(referenceRegExp, writeImageTag)
// Next, handle inline images: 
// base64 encoded images
text = text.replace(base64RegExp, writeImageTagBase64)
// cases with crazy urls like ./image/cat1).png
text = text.replace(crazyRegExp, writeImageTag)
// normal cases
text = text.replace(inlineRegExp, writeImageTag)
// handle reference-style shortcuts: ![img text]
text = text.replace(refShortcutRegExp, writeImageTag)
return text
})
showdown.subParser('italicsAndBold', (text: string, options: any, globals: any): string => {
// it's faster to have 3 separate regexes for each case than have just one
// because of backtracing, in some cases, it could lead to an exponential effect
// called "catastrophic backtrace". Ominous!
function parseInside(txt: string, left: string, right: string): string {
/*
if (options.simplifiedAutoLink) {
txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals);
}
*/
return left + txt + right
}
// Parse underscores
if (options.literalMidWordUnderscores) {
text = text.replace(/\b___(\S[\s\S]*?)___\b/g, (wm, txt) => {
return parseInside (txt, '', '')
})
text = text.replace(/\b__(\S[\s\S]*?)__\b/g, (wm, txt) => {
return parseInside (txt, '', '')
})
text = text.replace(/\b_(\S[\s\S]*?)_\b/g, (wm, txt) => {
return parseInside (txt, '', '')
})
} else {
text = text.replace(/___(\S[\s\S]*?)___/g, (wm, m) => {
return (/\S$/.test(m)) ? parseInside (m, '', '') : wm
})
text = text.replace(/__(\S[\s\S]*?)__/g, (wm, m) => {
return (/\S$/.test(m)) ? parseInside (m, '', '') : wm
})
text = text.replace(/_([^\s_][\s\S]*?)_/g, (wm, m) => {
// !/^_[^_]/.test(m) - test if it doesn't start with __ (since it seems redundant, we removed it)
return (/\S$/.test(m)) ? parseInside (m, '', '') : wm
})
}
// Now parse asterisks
if (options.literalMidWordAsterisks) {
text = text.replace(/([^*]|^)\B\*\*\*(\S[\s\S]*?)\*\*\*\B(?!\*)/g, (wm, lead, txt) => {
return parseInside (txt, `${lead}`, '')
})
text = text.replace(/([^*]|^)\B\*\*(\S[\s\S]*?)\*\*\B(?!\*)/g, (wm, lead, txt) => {
return parseInside (txt, `${lead}`, '')
})
text = text.replace(/([^*]|^)\B\*(\S[\s\S]*?)\*\B(?!\*)/g, (wm, lead, txt) => {
return parseInside (txt, `${lead}`, '')
})
} else {
text = text.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g, (wm, m) => {
return (/\S$/.test(m)) ? parseInside (m, '', '') : wm
})
text = text.replace(/\*\*(\S[\s\S]*?)\*\*/g, (wm, m) => {
return (/\S$/.test(m)) ? parseInside (m, '', '') : wm
})
text = text.replace(/\*([^\s*][\s\S]*?)\*/g, (wm, m) => {
// !/^\*[^*]/.test(m) - test if it doesn't start with ** (since it seems redundant, we removed it)
return (/\S$/.test(m)) ? parseInside (m, '', '') : wm
})
}
return text
})
/**
* Form HTML ordered (numbered) and unordered (bulleted) lists.
*/
showdown.subParser('lists', (text: string, options: any, globals: any): string => {
/**
* Process the contents of a single ordered or unordered list, splitting it
* into individual list items.
* @param {string} listStr
* @param {boolean} trimTrailing
* @returns {string}
*/
function processListItems(listStr: string, trimTrailing?: boolean): string {
// The $g_list_level global keeps track of when we're inside a list.
// Each time we enter a list, we increment it; when we leave a list,
// we decrement. If it's zero, we're not in a list anymore.
//
// We do this because when we're not inside a list, we want to treat
// something like this:
//
// I recommend upgrading to version
// 8. Oops, now this line is treated
// as a sub-list.
//
// As a single paragraph, despite the fact that the second line starts
// with a digit-period-space sequence.
//
// Whereas when we're inside a list (or sub-list), that line will be
// treated as the start of a sub-list. What a kludge, huh? This is
// an aspect of Markdown's syntax that's hard to parse perfectly
// without resorting to mind-reading. Perhaps the solution is to
// change the syntax rules such that sub-lists must start with a
// starting cardinal number; e.g. "1." or "a.".
globals.gListLevel++
// trim trailing blank lines:
listStr = listStr.replace(/\n{2,}$/, '\n')
// attacklab: add sentinel to emulate \z
listStr += '¨0'
let rgx = /(\n)?(^ {0,3})([*+-]|\d+\.)[ \t]+((\[([x ])?\])?[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+\.)[ \t]+))/gim
let isParagraphed = (/\n[ \t]*\n(?!¨0)/.test(listStr))
// Since version 1.5, nesting sublists requires 4 spaces (or 1 tab) indentation,
// which is a syntax breaking change
// activating this option reverts to old behavior
if (options.disableForced4SpacesIndentedSublists) {
rgx = /(\n)?(^ {0,3})([*+-]|\d+\.)[ \t]+((\[([x ])?\])?[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+\.)[ \t]+))/gim
}
listStr = listStr.replace(rgx, (wholeMatch: string, m1: string, m2: string, m3: string, m4: string, taskbtn: string, checked: string): string => {
let isChecked = (checked && checked.trim() !== '')
let item = showdown.subParser('outdent')(m4, options, globals)
let bulletStyle = ''
// Support for github tasklists
if (taskbtn && options.tasklists) {
bulletStyle = ' class="task-list-item" style="list-style-type: none;"'
item = item.replace(/^[ \t]*\[([x ])?\]/im, () => {
let otp = ''
return otp
})
}
// ISSUE #312
// This input: - - - a
// causes trouble to the parser, since it interprets it as:
// - a
// instead of:
// - - - a
// So, to prevent it, we will put a marker (¨A)in the beginning of the line
// Kind of hackish/monkey patching, but seems more effective than overcomplicating the list parser
item = item.replace(/^([-*+]|\d\.)[ \t]+[\S\n ]*/g, (wm2: string): string => {
return `¨A${wm2}`
})
// m1 - Leading line or
// Has a double return (multi paragraph) or
// Has sublist
if (m1 || (item.search(/\n{2,}/) > -1)) {
item = showdown.subParser('githubCodeBlocks')(item, options, globals)
item = showdown.subParser('blockGamut')(item, options, globals)
} else {
// Recursion for sub-lists:
item = showdown.subParser('lists')(item, options, globals)
item = item.replace(/\n$/, '') // chomp(item)
item = showdown.subParser('hashHTMLBlocks')(item, options, globals)
// Colapse double linebreaks
item = item.replace(/\n{2,}/g, '\n\n')
if (isParagraphed) {
item = showdown.subParser('paragraphs')(item, options, globals)
} else {
item = showdown.subParser('spanGamut')(item, options, globals)
}
}
// now we need to remove the marker (¨A)
item = item.replace('¨A', '')
// we can finally wrap the line in list item tags
item = `${item} \n`
return item
})
// attacklab: strip sentinel
listStr = listStr.replace(/¨0/g, '')
globals.gListLevel--
if (trimTrailing) {
listStr = listStr.replace(/\s+$/, '')
}
return listStr
}
function styleStartNumber(list: string, listType: string): string {
// check if ol and starts by a number different than 1
if (listType === 'ol') {
let res = list.match(/^ *(\d+)\./)
if (res && res[1] !== '1') {
return ` start="${res[1]}"`
}
}
return ''
}
/**
* Check and parse consecutive lists (better fix for issue #142)
* @param {string} list
* @param {string} listType
* @param {boolean} trimTrailing
* @returns {string}
*/
function parseConsecutiveLists(list: string, listType: string, trimTrailing?: boolean): string {
// check if we caught 2 or more consecutive lists by mistake
// we use the counterRgx, meaning if listType is UL we look for OL and vice versa
let olRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?\d+\.[ \t]/gm : /^ {0,3}\d+\.[ \t]/gm
let ulRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?[*+-][ \t]/gm : /^ {0,3}[*+-][ \t]/gm
let counterRxg = (listType === 'ul') ? olRgx : ulRgx
let result = ''
if (list.search(counterRxg) !== -1) {
(function parseCL(txt) {
let pos = txt.search(counterRxg)
let style = styleStartNumber(list, listType)
if (pos !== -1) {
// slice
result += `\n\n<${listType}${style}>\n${processListItems(txt.slice(0, pos), !!trimTrailing)}${listType}>\n`
// invert counterType and listType
listType = (listType === 'ul') ? 'ol' : 'ul'
counterRxg = (listType === 'ul') ? olRgx : ulRgx
// recurse
parseCL(txt.slice(pos))
} else {
result += `\n\n<${listType}${style}>\n${processListItems(txt, !!trimTrailing)}${listType}>\n`
}
})(list)
} else {
let style = styleStartNumber(list, listType)
result = `\n\n<${listType}${style}>\n${processListItems(list, !!trimTrailing)}${listType}>\n`
}
return result
}
/** Start of list parsing */
// add sentinel to hack around khtml/safari bug:
// http://bugs.webkit.org/show_bug.cgi?id=11231
text += '¨0'
if (globals.gListLevel) {
text = text.replace(/^(( {0,3}([*+-]|\d+\.)[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+\.)[ \t]+)))/gm, (wholeMatch, list, m2) => {
let listType = (m2.search(/[*+-]/g) > -1) ? 'ul' : 'ol'
return parseConsecutiveLists(list, listType, true)
})
} else {
text = text.replace(/(\n\n|^\n?)(( {0,3}([*+-]|\d+\.)[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+\.)[ \t]+)))/gm, (wholeMatch, m1, list, m3) => {
let listType = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol'
return parseConsecutiveLists(list, listType, false)
})
}
// strip sentinel
text = text.replace(/¨0/, '')
return text
})
/**
* Parse metadata at the top of the document
*/
showdown.subParser('metadata', (text: string, options: any, globals: any): string => {
if (!options.metadata) {
return text
}
function parseMetadataContents(content: string): void {
// raw is raw so it's not changed in any way
globals.metadata.raw = content
// escape chars forbidden in html attributes
// double quotes
content = content
// ampersand first
.replace(/&/g, '&')
// double quotes
.replace(/"/g, '"')
content = content.replace(/\n {4}/g, ' ')
content.replace(/^([\S ]+): +([\s\S]+?)$/gm, (wm: string, key: string, value: string): string => {
globals.metadata.parsed[key] = value
return ''
})
}
text = text.replace(/^\s*«{3,}(\S*)\n([\s\S]+?)\n»{3,}\n/, (wholematch, format, content) => {
parseMetadataContents(content)
return '¨M'
})
text = text.replace(/^\s*-{3,}(\S*)\n([\s\S]+?)\n-{3,}\n/, (wholematch, format, content) => {
if (format) {
globals.metadata.format = format
}
parseMetadataContents(content)
return '¨M'
})
text = text.replace(/¨M/g, '')
return text
})
/**
* Remove one level of line-leading tabs or spaces
*/
showdown.subParser('outdent', (text: string, options: any, globals: any): string => {
// attacklab: hack around Konqueror 3.5.4 bug:
// "----------bug".replace(/^-/g,"") == "bug"
text = text.replace(/^(\t| {1,4})/gm, '¨0') // attacklab: g_tab_width
// attacklab: clean up hack
text = text.replace(/¨0/g, '')
return text
})
/**
*
*/
showdown.subParser('paragraphs', (text: string, options: any, globals: any): string => {
// Strip leading and trailing lines:
text = text.replace(/^\n+/g, '')
text = text.replace(/\n+$/g, '')
let grafs = text.split(/\n{2,}/)
let grafsOut = []
let end = grafs.length // Wrap tags
for (let i = 0; i < end; i++) {
let str = grafs[i]
// if this is an HTML marker, copy it
if (str.search(/¨(K|G)(\d+)\1/) >= 0) {
grafsOut.push(str)
// test for presence of characters to prevent empty lines being parsed
// as paragraphs (resulting in undesired extra empty paragraphs)
} else if (str.search(/\S/) >= 0) {
str = showdown.subParser('spanGamut')(str, options, globals)
str = str.replace(/^([ \t]*)/g, '
')
str += '
'
grafsOut.push(str)
}
}
/** Unhashify HTML blocks */
end = grafsOut.length
for (let i = 0; i < end; i++) {
let blockText = ''
let grafsOutIt: string = grafsOut[i]
let codeFlag = false
// if this is a marker for an html block...
// use RegExp.test instead of string.search because of QML bug
while (/¨(K|G)(\d+)\1/.test(grafsOutIt)) {
let delim = RegExp.$1
let num = RegExp.$2
if (delim === 'K') {
blockText = globals.gHtmlBlocks[num]
} else {
// we need to check if ghBlock is a false positive
if (codeFlag) {
// use encoded version of all text
blockText = showdown.subParser('encodeCode')(globals.ghCodeBlocks[num].text, options, globals)
} else {
blockText = globals.ghCodeBlocks[num].codeblock
}
}
blockText = blockText.replace(/\$/g, '$$$$') // Escape any dollar signs
grafsOutIt = grafsOutIt.replace(/(\n\n)?¨(K|G)\d+\2(\n\n)?/, blockText)
// Check if grafsOutIt is a pre->code
if (/^]*>\s*]*>/.test(grafsOutIt)) {
codeFlag = true
}
}
grafsOut[i] = grafsOutIt
}
text = grafsOut.join('\n')
// Strip leading and trailing lines:
text = text.replace(/^\n+/g, '')
text = text.replace(/\n+$/g, '')
return text
})
/**
* These are all the transformations that occur *within* block-level
* tags like paragraphs, headers, and list items.
*/
showdown.subParser('spanGamut', (text: string, options: any, globals: any): string => {
text = showdown.subParser('codeSpans')(text, options, globals)
text = showdown.subParser('escapeSpecialCharsWithinTagAttributes')(text, options, globals)
text = showdown.subParser('encodeBackslashEscapes')(text, options, globals)
// Process anchor and image tags. Images must come first,
// because ![foo][f] looks like an anchor.
text = showdown.subParser('images')(text, options, globals)
text = showdown.subParser('anchors')(text, options, globals)
// Make links out of things like ` `
// Must come after anchors, because you can use < and >
// delimiters in inline links like [this]().
text = showdown.subParser('autoLinks')(text, options, globals)
text = showdown.subParser('simplifiedAutoLinks')(text, options, globals)
text = showdown.subParser('underline')(text, options, globals)
text = showdown.subParser('italicsAndBold')(text, options, globals)
text = showdown.subParser('strikethrough')(text, options, globals)
text = showdown.subParser('ellipsis')(text, options, globals)
// we need to hash HTML tags inside spans
text = showdown.subParser('hashHTMLSpans')(text, options, globals)
// now we encode amps and angles
text = showdown.subParser('encodeAmpsAndAngles')(text, options, globals)
// Do hard breaks
if (options.simpleLineBreaks) {
// GFM style hard breaks
// only add line breaks if the text does not contain a block (special case for lists)
if (!/\n\n¨K/.test(text)) {
text = text.replace(/\n+/g, '
\n')
}
} else {
// Vanilla hard breaks
text = text.replace(/ {2,}\n/g, '
\n')
}
return text
})
showdown.subParser('strikethrough', (text: string, options: any, globals: any): string => {
function parseInside(txt: string): string {
if (options.simplifiedAutoLink) {
txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals)
}
return `${txt}`
}
if (options.strikethrough) {
text = text.replace(/~{2}([\s\S]+?)~{2}/g, (wm, txt) => { return parseInside(txt) })
}
return text
})
/**
* Strips link definitions from text, stores the URLs and titles in
* hash references.
* Link defs are in the form: ^[id]: url "optional title"
*/
showdown.subParser('stripLinkDefinitions', (text: string, options: any, globals: any): string => {
let regex = /^ {0,3}\[([^\]]+)\]:[ \t]*(?:\n[ \t]*)?([^>\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:\n[ \t]*)?(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm
let base64Regex = /^ {0,3}\[([^\]]+)\]:[ \t]*(?:\n[ \t]*)?(data:.+?\/.+?;base64,[A-Za-z0-9+/=\n]+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:\n[ \t]*)?(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm
// attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
text += '¨0'
let replaceFunc = function (wholeMatch: any, linkId: any, url: any, width: any, height: any, blankLines: any, title: any) {
// if there aren't two instances of linkId it must not be a reference link so back out
linkId = linkId.toLowerCase()
if (text.toLowerCase().split(linkId).length - 1 < 2) {
return wholeMatch
}
if (url.match(/^data:.+?\/.+?;base64,/)) {
// remove newlines
globals.gUrls[linkId] = url.replace(/\s/g, '')
} else {
globals.gUrls[linkId] = showdown.subParser('encodeAmpsAndAngles')(url, options, globals) // Link IDs are case-insensitive
}
if (blankLines) {
// Oops, found blank lines, so it's not a title.
// Put back the parenthetical statement we stole.
return blankLines + title
} else {
if (title) {
globals.gTitles[linkId] = title.replace(/"|'/g, '"')
}
if (options.parseImgDimensions && width && height) {
globals.gDimensions[linkId] = {
width,
height
}
}
}
// Completely remove the definition from the text
return ''
}
// first we try to find base64 link references
text = text.replace(base64Regex, replaceFunc)
text = text.replace(regex, replaceFunc)
// attacklab: strip sentinel
text = text.replace(/¨0/, '')
return text
})
showdown.subParser('tables', (text: string, options: any, globals: any): string => {
if (!options.tables) {
return text
}
let tableRgx = /^.[^\n\r|\u2028\u2029]*\|.+\n {0,3}\|?[ \t]*(?::[ \t]*)?[-=]{2,}[ \t]*(?::[ \t]*)?\|[ \t]*(?::[ \t]*)?[-=]{2}[\s\S]+?(?:\n\n|¨0)/gm
// singeColTblRgx = /^ {0,3}\|.+\|\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n(?: {0,3}\|.+\|\n)+(?:\n\n|¨0)/gm;
let singeColTblRgx = /^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*(?::[ \t]*)?[-=]{2,}[ \t]*(?::[ \t]*)?\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm
function parseStyles(sLine: string): string {
if (/^:[ \t]*-+$/.test(sLine)) {
return ' style="text-align:left;"'
} else if (/^-+[ \t]*:[ \t]*$/.test(sLine)) {
return ' style="text-align:right;"'
} else if (/^:[ \t]*-+[ \t]*:$/.test(sLine)) {
return ' style="text-align:center;"'
} else {
return ''
}
}
function parseHeaders(header: string, style: string): string {
let id = ''
header = header.trim()
// support both tablesHeaderId and tableHeaderId due to error in documentation so we don't break backwards compatibility
if (options.tablesHeaderId || options.tableHeaderId) {
id = ` id="${header.replace(/ /g, '_').toLowerCase()}"`
}
header = showdown.subParser('spanGamut')(header, options, globals)
return `${header} \n`
}
function parseCells(cell: string, style: string): string {
let subText = showdown.subParser('spanGamut')(cell, options, globals)
return `${subText} \n`
}
function buildTable(headers: string[], cells: string[][]): string {
let tb = '\n\n\n'
let tblLgn = headers.length
for (let i = 0; i < tblLgn; ++i) {
tb += headers[i]
}
tb += ' \n\n\n'
for (let i = 0; i < cells.length; ++i) {
tb += '\n'
for (let ii = 0; ii < tblLgn; ++ii) {
tb += cells[i][ii]
}
tb += ' \n'
}
tb += '\n
\n'
return tb
}
function parseTable(rawTable: string): string {
let tableLines = rawTable.split('\n')
for (let i = 0; i < tableLines.length; ++i) {
// strip wrong first and last column if wrapped tables are used
if (/^ {0,3}\|/.test(tableLines[i])) {
tableLines[i] = tableLines[i].replace(/^ {0,3}\|/, '')
}
if (/\|[ \t]*$/.test(tableLines[i])) {
tableLines[i] = tableLines[i].replace(/\|[ \t]*$/, '')
}
// parse code spans first, but we only support one line code spans
tableLines[i] = showdown.subParser('codeSpans')(tableLines[i], options, globals)
}
let rawHeaders = tableLines[0].split('|').map((s: string): string => { return s.trim() })
let rawStyles = tableLines[1].split('|').map((s: string): string => { return s.trim() })
let rawCells = []
let headers = []
let styles = []
let cells = []
tableLines.shift()
tableLines.shift()
for (let i = 0; i < tableLines.length; ++i) {
if (tableLines[i].trim() === '') {
continue
}
rawCells.push(
tableLines[i]
.split('|')
.map((s: string): string => {
return s.trim()
})
)
}
if (rawHeaders.length < rawStyles.length) {
return rawTable
}
for (let i = 0; i < rawStyles.length; ++i) {
styles.push(parseStyles(rawStyles[i]))
}
for (let i = 0; i < rawHeaders.length; ++i) {
if (showdown.helper.isUndefined(styles[i])) {
styles[i] = ''
}
headers.push(parseHeaders(rawHeaders[i], styles[i]))
}
for (let i = 0; i < rawCells.length; ++i) {
let row = []
for (let ii = 0; ii < headers.length; ++ii) {
if (showdown.helper.isUndefined(rawCells[i][ii])) {
}
row.push(parseCells(rawCells[i][ii], styles[ii]))
}
cells.push(row)
}
return buildTable(headers, cells)
}
// find escaped pipe characters
text = text.replace(/\\(\|)/g, showdown.helper.escapeCharactersCallback)
// parse multi column tables
text = text.replace(tableRgx, parseTable)
// parse one column tables
text = text.replace(singeColTblRgx, parseTable)
return text
})
showdown.subParser('underline', (text: string, options: any, globals: any): string => {
if (!options.underline) {
return text
}
if (options.literalMidWordUnderscores) {
text = text.replace(/\b___(\S[\s\S]*?)___\b/g, (wm, txt) => {
return `${txt}`
})
text = text.replace(/\b__(\S[\s\S]*?)__\b/g, (wm, txt) => {
return `${txt}`
})
} else {
text = text.replace(/___(\S[\s\S]*?)___/g, (wm, m) => {
return (/\S$/.test(m)) ? `${m}` : wm
})
text = text.replace(/__(\S[\s\S]*?)__/g, (wm, m) => {
return (/\S$/.test(m)) ? `${m}` : wm
})
}
// escape remaining underscores to prevent them being parsed by italic and bold
text = text.replace(/(_)/g, showdown.helper.escapeCharactersCallback)
return text
})
/**
* Swap back in all the special characters we've hidden.
*/
showdown.subParser('unescapeSpecialChars', (text: string, options: any, globals: any): string => {
text = text.replace(/¨E(\d+)E/g, (wholeMatch, m1) => {
let charCodeToReplace = Number.parseInt(m1)
return String.fromCharCode(charCodeToReplace)
})
return text
})
showdown.subParser('makeMarkdown.blockquote', (node: any, globals: any): string => {
let txt = ''
if (node.hasChildNodes()) {
let children = node.childNodes
let childrenLength = children.length
for (let i = 0; i < childrenLength; ++i) {
let innerTxt = showdown.subParser('makeMarkdown.node')(children[i], globals)
if (innerTxt === '') {
continue
}
txt += innerTxt
}
}
// cleanup
txt = txt.trim()
txt = `> ${txt.split('\n').join('\n> ')}`
return txt
})
showdown.subParser('makeMarkdown.codeBlock', (node: any, globals: any): string => {
let lang = node.getAttribute('language')
let num = node.getAttribute('precodenum')
return `\`\`\`${lang}\n${globals.preList[num]}\n\`\`\``
})
showdown.subParser('makeMarkdown.codeSpan', (node: any): string => {
return `\`${node.innerHTML}\``
})
showdown.subParser('makeMarkdown.emphasis', (node: any, globals: any): string => {
let txt = ''
if (node.hasChildNodes()) {
txt += '*'
let children = node.childNodes
let childrenLength = children.length
for (let i = 0; i < childrenLength; ++i) {
txt += showdown.subParser('makeMarkdown.node')(children[i], globals)
}
txt += '*'
}
return txt
})
showdown.subParser('makeMarkdown.header', (node: any, globals: any, headerLevel: any): string => {
let headerMark = new Array(headerLevel + 1).join('#')
let txt = ''
if (node.hasChildNodes()) {
txt = `${headerMark} `
let children = node.childNodes
let childrenLength = children.length
for (let i = 0; i < childrenLength; ++i) {
txt += showdown.subParser('makeMarkdown.node')(children[i], globals)
}
}
return txt
})
showdown.subParser('makeMarkdown.hr', () => {
return '---'
})
showdown.subParser('makeMarkdown.image', (node: any): string => {
let txt = ''
if (node.hasAttribute('src')) {
txt += `}>`
if (node.hasAttribute('width') && node.hasAttribute('height')) {
txt += ` =${node.getAttribute('width')}x${node.getAttribute('height')}`
}
if (node.hasAttribute('title')) {
txt += ` "${node.getAttribute('title')}"`
}
txt += ')'
}
return txt
})
showdown.subParser('makeMarkdown.links', (node: any, globals: any): string => {
let txt = ''
if (node.hasChildNodes() && node.hasAttribute('href')) {
let children = node.childNodes
let childrenLength = children.length
txt = '['
for (let i = 0; i < childrenLength; ++i) {
txt += showdown.subParser('makeMarkdown.node')(children[i], globals)
}
txt += ']('
txt += `<${node.getAttribute('href')}>`
if (node.hasAttribute('title')) {
txt += ` "${node.getAttribute('title')}"`
}
txt += ')'
}
return txt
})
showdown.subParser('makeMarkdown.list', (node: any, globals: any, type: any): string => {
let txt = ''
if (!node.hasChildNodes()) {
return ''
}
let listItems = node.childNodes
let listItemsLenght = listItems.length
let listNum = node.getAttribute('start') || 1
for (let i = 0; i < listItemsLenght; ++i) {
if (typeof listItems[i].tagName === 'undefined' || listItems[i].tagName.toLowerCase() !== 'li') {
continue
}
// define the bullet to use in list
let bullet = ''
if (type === 'ol') {
bullet = `${listNum.toString()}. `
} else {
bullet = '- '
}
// parse list item
txt += bullet + showdown.subParser('makeMarkdown.listItem')(listItems[i], globals)
++listNum
}
// add comment at the end to prevent consecutive lists to be parsed as one
txt += '\n\n'
return txt.trim()
})
showdown.subParser('makeMarkdown.listItem', (node: any, globals: any): string => {
let listItemTxt = ''
let children = node.childNodes
let childrenLenght = children.length
for (let i = 0; i < childrenLenght; ++i) {
listItemTxt += showdown.subParser('makeMarkdown.node')(children[i], globals)
}
// if it's only one liner, we need to add a newline at the end
if (!/\n$/.test(listItemTxt)) {
listItemTxt += '\n'
} else {
// it's multiparagraph, so we need to indent
listItemTxt = listItemTxt
.split('\n')
.join('\n ')
.replace(/^ {4}$/gm, '')
.replace(/\n{2,}/g, '\n\n')
}
return listItemTxt
})
showdown.subParser('makeMarkdown.node', (node: any, globals: any, spansOnly: any): string => {
spansOnly = spansOnly || false
let txt = ''
// edge case of text without wrapper paragraph
if (node.nodeType === 3) {
return showdown.subParser('makeMarkdown.txt')(node, globals)
}
// HTML comment
if (node.nodeType === 8) {
return `\n\n`
}
// process only node elements
if (node.nodeType !== 1) {
return ''
}
let tagName = node.tagName.toLowerCase()
switch (tagName) {
//
// BLOCKS
//
case 'h1':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.header')(node, globals, 1)}\n\n` }
break
case 'h2':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.header')(node, globals, 2)}\n\n` }
break
case 'h3':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.header')(node, globals, 3)}\n\n` }
break
case 'h4':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.header')(node, globals, 4)}\n\n` }
break
case 'h5':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.header')(node, globals, 5)}\n\n` }
break
case 'h6':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.header')(node, globals, 6)}\n\n` }
break
case 'p':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.paragraph')(node, globals)}\n\n` }
break
case 'blockquote':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.blockquote')(node, globals)}\n\n` }
break
case 'hr':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.hr')(node, globals)}\n\n` }
break
case 'ol':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.list')(node, globals, 'ol')}\n\n` }
break
case 'ul':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.list')(node, globals, 'ul')}\n\n` }
break
case 'precode':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.codeBlock')(node, globals)}\n\n` }
break
case 'pre':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.pre')(node, globals)}\n\n` }
break
case 'table':
if (!spansOnly) { txt = `${showdown.subParser('makeMarkdown.table')(node, globals)}\n\n` }
break
//
// SPANS
//
case 'code':
txt = showdown.subParser('makeMarkdown.codeSpan')(node, globals)
break
case 'em':
case 'i':
txt = showdown.subParser('makeMarkdown.emphasis')(node, globals)
break
case 'strong':
case 'b':
txt = showdown.subParser('makeMarkdown.strong')(node, globals)
break
case 'del':
txt = showdown.subParser('makeMarkdown.strikethrough')(node, globals)
break
case 'a':
txt = showdown.subParser('makeMarkdown.links')(node, globals)
break
case 'img':
txt = showdown.subParser('makeMarkdown.image')(node, globals)
break
default:
txt = `${node.outerHTML}\n\n`
}
// common normalization
// TODO eventually
return txt
})
showdown.subParser('makeMarkdown.paragraph', (node: any, globals: any): string => {
let txt = ''
if (node.hasChildNodes()) {
let children = node.childNodes
let childrenLength = children.length
for (let i = 0; i < childrenLength; ++i) {
txt += showdown.subParser('makeMarkdown.node')(children[i], globals)
}
}
// some text normalization
txt = txt.trim()
return txt
})
showdown.subParser('makeMarkdown.pre', (node: any, globals: any): string => {
let num = node.getAttribute('prenum')
return `${globals.preList[num]}`
})
showdown.subParser('makeMarkdown.strikethrough', (node: any, globals: any): string => {
let txt = ''
if (node.hasChildNodes()) {
txt += '~~'
let children = node.childNodes
let childrenLength = children.length
for (let i = 0; i < childrenLength; ++i) {
txt += showdown.subParser('makeMarkdown.node')(children[i], globals)
}
txt += '~~'
}
return txt
})
showdown.subParser('makeMarkdown.strong', (node: any, globals: any): string => {
let txt = ''
if (node.hasChildNodes()) {
txt += '**'
let children = node.childNodes
let childrenLength = children.length
for (let i = 0; i < childrenLength; ++i) {
txt += showdown.subParser('makeMarkdown.node')(children[i], globals)
}
txt += '**'
}
return txt
})
showdown.subParser('makeMarkdown.table', (node: any, globals: any): string => {
let txt = ''
let tableArray: string[][] = [[], []]
let headings = node.querySelectorAll('thead>tr>th')
let rows = node.querySelectorAll('tbody>tr')
for (let i = 0; i < headings.length; ++i) {
let headContent = showdown.subParser('makeMarkdown.tableCell')(headings[i], globals)
let allign = '---'
if (headings[i].hasAttribute('style')) {
let style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, '')
switch (style) {
case 'text-align:left;':
allign = ':---'
break
case 'text-align:right;':
allign = '---:'
break
case 'text-align:center;':
allign = ':---:'
break
}
}
tableArray[0][i] = headContent.trim()
tableArray[1][i] = allign
}
for (let i = 0; i < rows.length; ++i) {
let r = tableArray.push([]) - 1
let cols = rows[i].getElementsByTagName('td')
for (let ii = 0; ii < headings.length; ++ii) {
let cellContent = ' '
if (typeof cols[ii] !== 'undefined') {
cellContent = showdown.subParser('makeMarkdown.tableCell')(cols[ii], globals)
}
tableArray[r].push(cellContent)
}
}
let cellSpacesCount = 3
for (let i = 0; i < tableArray.length; ++i) {
for (let ii = 0; ii < tableArray[i].length; ++ii) {
let strLen = tableArray[i][ii].length
if (strLen > cellSpacesCount) {
cellSpacesCount = strLen
}
}
}
for (let i = 0; i < tableArray.length; ++i) {
for (let ii = 0; ii < tableArray[i].length; ++ii) {
if (i === 1) {
if (tableArray[i][ii].slice(-1) === ':') {
tableArray[i][ii] = `${showdown.helper.padEnd(tableArray[i][ii].slice(-1), cellSpacesCount - 1, '-')}:`
} else {
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-')
}
} else {
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount)
}
}
txt += `| ${tableArray[i].join(' | ')} |\n`
}
return txt.trim()
})
showdown.subParser('makeMarkdown.tableCell', (node: any, globals: any): string => {
let txt = ''
if (!node.hasChildNodes()) {
return ''
}
let children = node.childNodes
let childrenLength = children.length
for (let i = 0; i < childrenLength; ++i) {
txt += showdown.subParser('makeMarkdown.node')(children[i], globals, true)
}
return txt.trim()
})
showdown.subParser('makeMarkdown.txt', (node: any): string => {
let txt = node.nodeValue
// multiple spaces are collapsed
txt = txt.replace(/ +/g, ' ')
// replace the custom ¨NBSP; with a space
txt = txt.replace(/¨NBSP;/g, ' ')
// ", <, > and & should replace escaped html entities
txt = showdown.helper.unescapeHTMLEntities(txt)
// escape markdown magic characters
// emphasis, strong and strikethrough - can appear everywhere
// we also escape pipe (|) because of tables
// and escape ` because of code blocks and spans
txt = txt.replace(/([*_~|`])/g, '\\$1')
// escape > because of blockquotes
txt = txt.replace(/^(\s*)>/g, '\\$1>')
// hash character, only troublesome at the beginning of a line because of headers
txt = txt.replace(/^#/gm, '\\#')
// horizontal rules
txt = txt.replace(/^(\s*)([-=]{3,})(\s*)$/, '$1\\$2$3')
// dot, because of ordered lists, only troublesome at the beginning of a line when preceded by an integer
txt = txt.replace(/^( {0,3}\d+)\./gm, '$1\\.')
// +, * and -, at the beginning of a line becomes a list, so we need to escape them also (asterisk was already escaped)
txt = txt.replace(/^( {0,3})([+-])/gm, '$1\\$2')
// images and links, ] followed by ( is problematic, so we escape it
txt = txt.replace(/\](\s*)\(/g, '\\]$1\\(')
// reference URIs must also be escaped
txt = txt.replace(/^ {0,3}\[([\S \t]*?)\]:/gm, '\\[$1]:')
return txt
})
export { defaultOptions, showdown }