/** * ⚠️ Security note: `makeHtml()` output is NOT sanitized — raw HTML in the * markdown source passes through. If the markdown can contain user-generated * content, run the result through `sanitizeHtml()` (from utils) before * binding it with v-html. */ /** * Showdown configuration options */ export interface ShowdownOptions { /** Omit the default extra whiteline added to code blocks */ omitExtraWLInCodeBlocks?: boolean /** Turn on/off generated header id */ noHeaderId?: boolean /** Add a prefix to the generated header ids */ prefixHeaderId?: boolean | string /** Prevent showdown from modifying the prefix */ rawPrefixHeaderId?: boolean /** Generate header ids compatible with github style */ ghCompatibleHeaderId?: boolean /** Remove only spaces, ' and " from generated header ids */ rawHeaderId?: boolean /** The header blocks level start */ headerLevelStart?: number /** Turn on/off image dimension parsing */ parseImgDimensions?: boolean /** Turn on/off GFM autolink style */ simplifiedAutoLink?: boolean /** Excludes trailing punctuation from links generated with autoLinking */ excludeTrailingPunctuationFromURLs?: boolean /** Parse midword underscores as literal underscores */ literalMidWordUnderscores?: boolean /** Parse midword asterisks as literal asterisks */ literalMidWordAsterisks?: boolean /** Turn on/off strikethrough support */ strikethrough?: boolean /** Turn on/off tables support */ tables?: boolean /** Add an id to table headers */ tablesHeaderId?: boolean /** Turn on/off GFM fenced code blocks support */ ghCodeBlocks?: boolean /** Turn on/off GFM tasklist support */ tasklists?: boolean /** Prevents weird effects in live previews due to incomplete input */ smoothLivePreview?: boolean /** Try to smartly fix indentation problems */ smartIndentationFix?: boolean /** Disable the automatic generation of header ids */ disableForced4SpacesIndentedSublists?: boolean /** Turn on/off support for syntax highlighting of code blocks */ simpleLineBreaks?: boolean /** Parse line breaks as
like GitHub does */ requireSpaceBeforeHeadingText?: boolean /** Makes adding a space between # and the header text mandatory */ ghMentions?: boolean /** Enables github @mentions */ ghMentionsLink?: string /** Changes the link generated by @mentions */ encodeEmails?: boolean /** Encode emails with hex entities */ openLinksInNewWindow?: boolean /** Open all links in new windows */ backslashEscapesHTMLTags?: boolean /** Support for HTML Tag escaping */ customizedHeaderId?: boolean /** Enable custom header IDs using {#custom-id} syntax */ tableHeaderId?: boolean /** Alias for tablesHeaderId */ underline?: boolean /** Enable support for underline */ completeHTMLDocument?: boolean /** Outputs a complete html document */ splitAdjacentBlockquotes?: boolean /** Split adjacent blockquote blocks */ [key: string]: any } const defaultOptions: ShowdownOptions = { omitExtraWLInCodeBlocks: false, noHeaderId: false, prefixHeaderId: false, rawPrefixHeaderId: false, ghCompatibleHeaderId: false, rawHeaderId: false, headerLevelStart: 0, parseImgDimensions: false, simplifiedAutoLink: true, excludeTrailingPunctuationFromURLs: false, literalMidWordUnderscores: false, literalMidWordAsterisks: false, strikethrough: false, tables: false, tablesHeaderId: false, ghCodeBlocks: true, tasklists: false, smoothLivePreview: false, smartIndentationFix: false, disableForced4SpacesIndentedSublists: false, simpleLineBreaks: false, requireSpaceBeforeHeadingText: false, ghMentions: false, encodeEmails: true, openLinksInNewWindow: true, backslashEscapesHTMLTags: false, customizedHeaderId: false, tableHeaderId: false, underline: false, completeHTMLDocument: false, splitAdjacentBlockquotes: false, } /** * Internal Showdown Converter interface (simplified) */ export interface ShowdownConverter { makeHtml: (text: string) => string makeMarkdown: (src: string, HTMLParser?: any) => string } /** * Internal Showdown object structure (simplified) */ interface ShowdownStatic { Converter: new (options?: ShowdownOptions) => ShowdownConverter helper: Record subParser: ((name: string) => any) & ((name: string, func: any) => void) [key: string]: any } // Declare showdown globally for internal use const parsers: Record = {} const showdown: ShowdownStatic = { helper: {} as any, subParser: ((name: string, func?: any) => { if (func !== undefined) { // Register a subparser parsers[name] = func } else { // Retrieve a subparser return parsers[name] } }) as any, Converter: undefined as any } /** * showdownjs helper functions */ /** * Check if var is string * @static * @param {string} a * @returns {boolean} */ showdown.helper.isString = function (a: any): boolean { return (typeof a === 'string' || a instanceof String) } /** * Check if var is a function * @static * @param {*} a * @returns {boolean} */ showdown.helper.isFunction = function (a: any): boolean { let getType = {} return a && getType.toString.call(a) === '[object Function]' } /** * isArray helper function * @static * @param {*} a * @returns {boolean} */ showdown.helper.isArray = function (a: any): boolean { return Array.isArray(a) } /** * Check if value is undefined * @static * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. */ showdown.helper.isUndefined = function (value: any): boolean { return typeof value === 'undefined' } /** * ForEach helper function * Iterates over Arrays and Objects (own properties only) * @static * @param {*} obj * @param {Function} callback Accepts 3 params: 1. value, 2. key, 3. the original array/object */ showdown.helper.forEach = function (obj: any, callback: (item: any, index?: number | string, obj?: any) => void): void { // check if obj is defined if (showdown.helper.isUndefined(obj)) { throw new TypeError('obj param is required') } if (showdown.helper.isUndefined(callback)) { throw new TypeError('callback param is required') } if (!showdown.helper.isFunction(callback)) { throw new TypeError('callback param must be a function/closure') } if (typeof obj.forEach === 'function') { obj.forEach(callback) } else if (showdown.helper.isArray(obj)) { for (let i = 0; i < obj.length; i++) { callback(obj[i], i, obj) } } else if (typeof (obj) === 'object') { for (let prop in obj) { if (obj.hasOwnProperty(prop)) { callback(obj[prop], prop, obj) } } } else { throw new TypeError('obj does not seem to be an array or an iterable object') } } function escapeCharactersCallback(wholeMatch: string, m1: string): string { let charCodeToEscape = m1.charCodeAt(0) return `¨E${charCodeToEscape}E` } /** * Callback used to escape characters when passing through String.replace * @static * @param {string} wholeMatch * @param {string} m1 * @returns {string} */ showdown.helper.escapeCharactersCallback = escapeCharactersCallback /** * Escape characters in a string * @static * @param {string} text * @param {string} charsToEscape * @param {boolean} afterBackslash * @returns {XML|string|void|*} */ showdown.helper.escapeCharacters = function (text: string, charsToEscape: string, afterBackslash?: boolean): string { // First we have to escape the escape characters so that // we can build a character class out of them let regexString = `([${charsToEscape.replace(/([[\]\\])/g, '\\$1')}])` if (afterBackslash) { regexString = `\\\\${regexString}` } let regex = new RegExp(regexString, 'g') text = text.replace(regex, escapeCharactersCallback) return text } /** * Unescape HTML entities * @param txt * @returns {string} */ showdown.helper.unescapeHTMLEntities = function (txt: string): string { return txt .replace(/"/g, '"') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') } interface MatchPosition { left: { start: number, end: number } match: { start: number, end: number } right: { start: number, end: number } wholeMatch: { start: number, end: number } } let rgxFindMatchPos = function (str: string, left: string, right: string, flags?: string): MatchPosition[] { let f = flags || '' let g = f.includes('g') let x = new RegExp(`${left}|${right}`, `g${f.replace(/g/g, '')}`) let l = new RegExp(left, f.replace(/g/g, '')) let pos: MatchPosition[] = [] let t: number = 0 let s: number = 0 let m: RegExpExecArray | null let start: number = 0 let end: number = 0 do { t = 0 while ((m = x.exec(str))) { if (l.test(m[0])) { if (!(t++)) { s = x.lastIndex start = s - m[0].length } } else if (t) { if (!--t) { end = m.index + m[0].length let obj = { left: { start, end: s }, match: { start: s, end: m.index }, right: { start: m.index, end }, wholeMatch: { start, end } } pos.push(obj) if (!g) { return pos } } } } } while (t && (x.lastIndex = s)) return pos } /** * matchRecursiveRegExp * Accepts a string to search, a left and right format delimiter * as regex patterns, and optional regex flags. Returns an array * of matches, allowing nested instances of left/right delimiters. * Use the "g" flag to return all matches, otherwise only the * first is returned. Be careful to ensure that the left and * right format delimiters produce mutually exclusive matches. * Backreferences are not supported within the right delimiter * due to how it is internally combined with the left delimiter. * When matching strings whose format delimiters are unbalanced * to the left or right, the output is intentionally as a * conventional regex library with recursion support would * produce, e.g. "<" and ">" both produce ["x"] when using * "<" and ">" as the delimiters (both strings contain a single, * balanced instance of ""). * * examples: * matchRecursiveRegExp("test", "\\(", "\\)") * returns: [] * matchRecursiveRegExp(">>t<>", "<", ">", "g") * returns: ["t<>", ""] * matchRecursiveRegExp("
test
", "]*>", "", "gi") * returns: ["test"] */ showdown.helper.matchRecursiveRegExp = function (str: string, left: string, right: string, flags?: string): string[][] { let matchPos = rgxFindMatchPos (str, left, right, flags) let results: string[][] = [] for (let i = 0; i < matchPos.length; ++i) { results.push([ str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end), str.slice(matchPos[i].match.start, matchPos[i].match.end), str.slice(matchPos[i].left.start, matchPos[i].left.end), str.slice(matchPos[i].right.start, matchPos[i].right.end) ]) } return results } /** * * @param {string} str * @param {string | Function} replacement * @param {string} left * @param {string} right * @param {string} flags * @returns {string} */ showdown.helper.replaceRecursiveRegExp = function (str: string, replacement: string | ((match: string, left: string, match2: string, right: string) => string), left: string, right: string, flags?: string): string { let replacementFn: (match: string, left: string, match2: string, right: string) => string if (!showdown.helper.isFunction(replacement)) { let repStr = replacement as string replacementFn = function () { return repStr } } else { replacementFn = replacement as (match: string, left: string, match2: string, right: string) => string } let matchPos = rgxFindMatchPos(str, left, right, flags) let finalStr = str let lng = matchPos.length if (lng > 0) { let bits = [] if (matchPos[0].wholeMatch.start !== 0) { bits.push(str.slice(0, matchPos[0].wholeMatch.start)) } for (let i = 0; i < lng; ++i) { bits.push( replacementFn( str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end), str.slice(matchPos[i].match.start, matchPos[i].match.end), str.slice(matchPos[i].left.start, matchPos[i].left.end), str.slice(matchPos[i].right.start, matchPos[i].right.end) ) ) if (i < lng - 1) { bits.push(str.slice(matchPos[i].wholeMatch.end, matchPos[i + 1].wholeMatch.start)) } } if (matchPos[lng - 1].wholeMatch.end < str.length) { bits.push(str.slice(matchPos[lng - 1].wholeMatch.end)) } finalStr = bits.join('') } return finalStr } /** * Returns the index within the passed String object of the first occurrence of the specified regex, * starting the search at fromIndex. Returns -1 if the value is not found. * * @param {string} str string to search * @param {RegExp} regex Regular expression to search * @param {int} [fromIndex = 0] Index to start the search * @returns {number} * @throws InvalidArgumentError */ showdown.helper.regexIndexOf = function (str: string, regex: RegExp, fromIndex?: number): number { if (!showdown.helper.isString(str)) { throw 'InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string' } if (regex instanceof RegExp === false) { throw 'InvalidArgumentError: second parameter of showdown.helper.regexIndexOf function must be an instance of RegExp' } let indexOf = str.substring(fromIndex || 0).search(regex) return (indexOf >= 0) ? (indexOf + (fromIndex || 0)) : indexOf } /** * Splits the passed string object at the defined index, and returns an array composed of the two substrings * @param {string} str string to split * @param {int} index index to split string at * @returns {[string,string]} * @throws InvalidArgumentError */ showdown.helper.splitAtIndex = function (str: string, index: number): [string, string] { if (!showdown.helper.isString(str)) { throw 'InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string' } return [str.substring(0, index), str.substring(index)] } /** * Obfuscate an e-mail address through the use of Character Entities, * transforming ASCII characters into their equivalent decimal or hex entities. * * Since it has a random component, subsequent calls to this function produce different results * * @param {string} mail * @returns {string} */ showdown.helper.encodeEmailAddress = function (mail: string): string { let encode = [ function (ch: string): string { return `&#${ch.charCodeAt(0)};` }, function (ch: string): string { return `&#x${ch.charCodeAt(0).toString(16)};` }, function (ch: string): string { return ch } ] mail = mail.replace(/./g, (ch) => { if (ch === '@') { // this *must* be encoded. I insist. ch = encode[Math.floor(Math.random() * 2)](ch) } else { let r = Math.random() // roughly 10% raw, 45% hex, 45% dec ch = ( r > 0.9 ? encode[2](ch) : r > 0.45 ? encode[1](ch) : encode[0](ch) ) } return ch }) return mail } /** * * @param str * @param targetLength * @param padString * @returns {string} */ showdown.helper.padEnd = function padEnd(str: string, targetLength: number, padString?: string): string { /* jshint bitwise: false */ targetLength = targetLength >> 0 // floor if number or convert non-number to 0; /* jshint bitwise: true */ padString = String(padString || ' ') if (str.length > targetLength) { return String(str) } else { targetLength = targetLength - str.length if (targetLength > padString.length) { padString += padString.repeat(targetLength / padString.length) // append to original to ensure we are longer than needed } return String(str) + padString.slice(0, targetLength) } } /** * POLYFILLS */ // use this instead of builtin is undefined for IE8 compatibility if (typeof (console) === 'undefined') { (console as any) = { warn(msg: any) { alert(msg) }, log(msg: any) { alert(msg) }, error(msg: any) { throw msg } } } /** * Common regexes. * We declare some common regexes to improve performance */ showdown.helper.regexes = { asteriskDashAndColon: /([*_:~])/g } /** * Showdown Converter class * @class * @param {object} [converterOptions] * @returns {Converter} */ // @ts-ignore - Constructor function pattern showdown.Converter = function (this: any, converterOptions?: ShowdownOptions) { let /** * Options used by this converter * @private * @type {{}} */ options: any = {} _constructor() /** * Converter constructor * @private */ function _constructor() { converterOptions = converterOptions || {} if (typeof converterOptions !== 'object') { throw new TypeError(`Converter expects the passed parameter to be an object, but ${typeof converterOptions} was passed instead.`) } // Merge defaults with overrides options = { ...defaultOptions, ...converterOptions } } function rTrimInputText(text: string): string { let rsp = text.match(/^\s*/)?.[0].length || 0 let rgx = new RegExp(`^\\s{0,${rsp}}`, 'gm') return text.replace(rgx, '') } /** * Converts a markdown string into HTML * @param {string} text * @returns {*} */ this.makeHtml = function (text: string): string { // check if text is not falsy if (!text) { return text } let globals = { gHtmlBlocks: [], gHtmlMdBlocks: [], gHtmlSpans: [], gUrls: {}, gTitles: {}, gDimensions: {}, gListLevel: 0, hashLinkCounts: {}, converter: this, ghCodeBlocks: [], metadata: { parsed: {}, raw: '', format: '' } } // This lets us use ¨ trema as an escape char to avoid md5 hashes // The choice of character is arbitrary; anything that isn't // magic in Markdown will work. text = text.replace(/¨/g, '¨T') // Replace $ with ¨D // RegExp interprets $ as a special character // when it's in a replacement string text = text.replace(/\$/g, '¨D') // Standardize line endings text = text.replace(/\r\n/g, '\n') // DOS to Unix text = text.replace(/\r/g, '\n') // Mac to Unix // Stardardize line spaces text = text.replace(/\u00A0/g, ' ') if (options.smartIndentationFix) { text = rTrimInputText(text) } // Make sure text begins and ends with a couple of newlines: text = `\n\n${text}\n\n` // detab text = showdown.subParser('detab')(text, options, globals) /** * Strip any lines consisting only of spaces and tabs. * This makes subsequent regexs easier to write, because we can * match consecutive blank lines with /\n+/ instead of something * contorted like /[ \t]*\n+/ */ text = text.replace(/^[ \t]+$/gm, '') // run the sub parsers text = showdown.subParser('metadata')(text, options, globals) text = showdown.subParser('hashPreCodeTags')(text, options, globals) text = showdown.subParser('githubCodeBlocks')(text, options, globals) text = showdown.subParser('hashHTMLBlocks')(text, options, globals) text = showdown.subParser('hashCodeTags')(text, options, globals) text = showdown.subParser('stripLinkDefinitions')(text, options, globals) text = showdown.subParser('blockGamut')(text, options, globals) text = showdown.subParser('unhashHTMLSpans')(text, options, globals) text = showdown.subParser('unescapeSpecialChars')(text, options, globals) // attacklab: Restore dollar signs text = text.replace(/¨D/g, '$$') // attacklab: Restore tremas text = text.replace(/¨T/g, '¨') // render a complete html document instead of a partial if the option is enabled text = showdown.subParser('completeHTMLDocument')(text, options, globals) return text } /** * Converts an HTML string into a markdown string * @param src * @param [HTMLParser] A WHATWG DOM and HTML parser, such as JSDOM. If none is supplied, window.document will be used. * @returns {string} */ this.makeMarkdown = this.makeMd = function (src: string, HTMLParser?: any): string { // replace \r\n with \n src = src.replace(/\r\n/g, '\n') src = src.replace(/\r/g, '\n') // old macs // due to an edge case, we need to find this: > < // to prevent removing of non silent white spaces // ex: this is sparta src = src.replace(/>[ \t]+¨NBSP;<') if (!HTMLParser) { if (typeof window !== 'undefined' && window.document) { HTMLParser = window.document } else { throw new Error('HTMLParser is undefined. If in a webworker or nodejs environment, you need to provide a WHATWG DOM and HTML such as JSDOM') } } let doc = HTMLParser.createElement('div') doc.innerHTML = src let globals = { preList: substitutePreCodeTags(doc) } // remove all newlines and collapse spaces clean(doc) // some stuff, like accidental reference links must now be escaped // TODO // doc.innerHTML = doc.innerHTML.replace(/\[[\S\t ]]/); let nodes = doc.childNodes let mdDoc = '' for (let i = 0; i < nodes.length; i++) { mdDoc += showdown.subParser('makeMarkdown.node')(nodes[i], globals) } function clean(node: any): void { for (let n = 0; n < node.childNodes.length; ++n) { let child = node.childNodes[n] if (child.nodeType === 3) { if (!/\S/.test(child.nodeValue) && !/^ +$/.test(child.nodeValue)) { node.removeChild(child) --n } else { child.nodeValue = child.nodeValue.split('\n').join(' ') child.nodeValue = child.nodeValue.replace(/(\s)+/g, '$1') } } else if (child.nodeType === 1) { clean(child) } } } // find all pre tags and replace contents with placeholder // we need this so that we can remove all indentation from html // to ease up parsing function substitutePreCodeTags(doc: any): string[] { let pres = doc.querySelectorAll('pre') let presPH = [] for (let i = 0; i < pres.length; ++i) { if (pres[i].childElementCount === 1 && pres[i].firstChild.tagName.toLowerCase() === 'code') { let content = pres[i].firstChild.innerHTML.trim() let language = pres[i].firstChild.getAttribute('data-language') || '' // if data-language attribute is not defined, then we look for class language-* if (language === '') { let classes = pres[i].firstChild.className.split(' ') for (let c = 0; c < classes.length; ++c) { let matches = classes[c].match(/^language-(.+)$/) if (matches !== null) { language = matches[1] break } } } // unescape html entities in content content = showdown.helper.unescapeHTMLEntities(content) presPH.push(content) pres[i].outerHTML = `` } else { presPH.push(pres[i].innerHTML) pres[i].innerHTML = '' pres[i].setAttribute('prenum', i.toString()) } } return presPH } return mdDoc } } /** * Turn Markdown link shortcuts into XHTML tags. */ showdown.subParser('anchors', (text: string, options: any, globals: any): string => { let writeAnchorTag = function (wholeMatch: string, linkText: string, linkId: string, url: string, m5: string, m6: string, title?: string): string { if (showdown.helper.isUndefined(title)) { title = '' } linkId = linkId.toLowerCase() // Special case for explicit empty url if (wholeMatch.search(/\(? ?(['"].*['"])?\)$/m) > -1) { url = '' } else if (!url) { if (!linkId) { // lower-case and turn embedded newlines into spaces linkId = linkText.toLowerCase().replace(/ ?\n/g, ' ') } url = `#${linkId}` if (!showdown.helper.isUndefined(globals.gUrls[linkId])) { url = globals.gUrls[linkId] if (!showdown.helper.isUndefined(globals.gTitles[linkId])) { title = globals.gTitles[linkId] } } else { return wholeMatch } } // url = showdown.helper.escapeCharacters(url, '*_', false); // replaced line to improve performance url = url.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback) let result = `${linkText}` return result } // First, handle reference-style links: [link text] [id] text = text.replace(/\[((?:\[[^\]]*\]|[^[\]])*)\] ?(?:\n *)?\[(.*?)\]()()()()/g, writeAnchorTag) // Next, inline-style links: [link text](url "optional title") // cases with crazy urls like ./image/cat1).png text = text.replace(/\[((?:\[[^\]]*\]|[^[\]])*)\]()[ \t]*\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g, writeAnchorTag) // normal cases text = text.replace(/\[((?:\[[^\]]*\]|[^[\]])*)\]()[ \t]*\([ \t]??(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g, writeAnchorTag) // handle reference-style shortcuts: [link text] // These must come last in case you've also got [link test][1] // or [link test](/foo) text = text.replace(/\[([^[\]]+)\]()()()()()/g, writeAnchorTag) // Lastly handle GithubMentions if option is enabled if (options.ghMentions) { text = text.replace(/(^|\s)(\\)?(@([a-z\d]+(?:[a-z\d.-]+?[a-z\d]+)*))/gim, (wm, st, escape, mentions, username) => { if (escape === '\\') { return st + mentions } // check if options.ghMentionsLink is a string if (!showdown.helper.isString(options.ghMentionsLink)) { throw new TypeError('ghMentionsLink option must be a string') } let lnk = options.ghMentionsLink.replace(/\{u\}/g, username) let target = '' if (options.openLinksInNewWindow) { target = ' rel="noopener noreferrer" target="¨E95Eblank"' } return `${st}${mentions}` }) } return text }) // url allowed chars [a-z\d_.~:/?#[]@!$&'()*+,;=-] let simpleURLRegex = /([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+?\.[^'">\s]+?)()(\1)?(?=\s|$)(?!["<>])/gi let simpleURLRegex2 = /([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?,()[\]])?(\1)?(?=\s|$)(?!["<>])/gi let delimUrlRegex = /()<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)()>()/gi let simpleMailRegex = /(^|\s)(?:mailto:)?([\w!#$%&'*+-/=?^`{|}~]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gim let delimMailRegex = /<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi let replaceLink = function (options: any) { return function (wm: string, leadingMagicChars: string, link: string, m2: string, m3: string, trailingPunctuation: string, trailingMagicChars: string): string { link = link.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback) let lnkTxt = link let append = '' let target = '' let lmc = leadingMagicChars || '' let tmc = trailingMagicChars || '' if (/^www\./i.test(link)) { link = link.replace(/^www\./i, 'http://www.') } if (options.excludeTrailingPunctuationFromURLs && trailingPunctuation) { append = trailingPunctuation } if (options.openLinksInNewWindow) { target = ' rel="noopener noreferrer" target="¨E95Eblank"' } return `${lmc}${lnkTxt}${append}${tmc}` } } let replaceMail = function (options: any, globals: any) { return function (wholeMatch: string, b: string, mail: string): string { let href = 'mailto:' b = b || '' mail = showdown.subParser('unescapeSpecialChars')(mail, options, globals) if (options.encodeEmails) { href = showdown.helper.encodeEmailAddress(href + mail) mail = showdown.helper.encodeEmailAddress(mail) } else { href = href + mail } return `${b}${mail}` } } showdown.subParser('autoLinks', (text: string, options: any, globals: any): string => { text = text.replace(delimUrlRegex, replaceLink(options)) text = text.replace(delimMailRegex, replaceMail(options, globals)) return text }) showdown.subParser('simplifiedAutoLinks', (text: string, options: any, globals: any): string => { if (!options.simplifiedAutoLink) { return text } if (options.excludeTrailingPunctuationFromURLs) { text = text.replace(simpleURLRegex2, replaceLink(options)) } else { text = text.replace(simpleURLRegex, replaceLink(options)) } text = text.replace(simpleMailRegex, replaceMail(options, globals)) return text }) /** * These are all the transformations that form block-level * tags like paragraphs, headers, and list items. */ showdown.subParser('blockGamut', (text: string, options: any, globals: any): string => { // we parse blockquotes first so that we can have headings and hrs // inside blockquotes text = showdown.subParser('blockQuotes')(text, options, globals) text = showdown.subParser('headers')(text, options, globals) // Do Horizontal Rules: text = showdown.subParser('horizontalRule')(text, options, globals) text = showdown.subParser('lists')(text, options, globals) text = showdown.subParser('codeBlocks')(text, options, globals) text = showdown.subParser('tables')(text, options, globals) // We already ran _HashHTMLBlocks() before, in Markdown(), but that // was to escape raw HTML in the original Markdown source. This time, // we're escaping the markup we've just created, so that we don't wrap //

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 = `` // 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]??(?: =([*\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]??(?: =([*\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(/\(? ?(['"].*['"])?\)$/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 = `${altText}x "optional title") // 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 = '
  • 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)}\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)}\n` } })(list) } else { let style = styleStartNumber(list, listType) result = `\n\n<${listType}${style}>\n${processListItems(list, !!trimTrailing)}\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]*)??(?: =([*\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 += `![${node.getAttribute('alt')}](` txt += `<${node.getAttribute('src')}>` 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 }