UNPKG

@skybloxsystems/ticket-bot

Version:
1,415 lines (1,296 loc) 72.4 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.SimpleMarkdown = factory()); }(this, (function () { 'use strict'; /* @flow */ /* @ts-check */ /** * Simple-Markdown * =============== * * Simple-Markdown's primary goal is to be easy to adapt. It aims * to be compliant with John Gruber's [Markdown Syntax page][1], * but compatiblity with other markdown implementations' edge-cases * will be sacrificed where it conflicts with simplicity or * extensibility. * * If your goal is to simply embed a standard markdown implementation * in your website, simple-markdown is probably not the best library * for you (although it should work). But if you have struggled to * customize an existing library to meet your needs, simple-markdown * might be able to help. * * Many of the regexes and original logic has been adapted from * the wonderful [marked.js](https://github.com/chjj/marked) * * LICENSE (MIT): * New code copyright (c) 2014-2019 Khan Academy & Aria Buckles. * * Portions adapted from marked.js copyright (c) 2011-2014 * Christopher Jeffrey (https://github.com/chjj/). * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ // Typescript language & simple-markdown.d.ts references: /// <reference lib="ES2018" /> /// <reference path="../simple-markdown.d.ts" /> /*:: // Flow Type Definitions: type Capture = Array<string> & {index: number} | Array<string> & {index?: number}; type Attr = string | number | boolean | null | void; type SingleASTNode = { type: string, [string]: any, }; type UnTypedASTNode = { [string]: any }; type ASTNode = SingleASTNode | Array<SingleASTNode>; type State = { key?: string | number | void, inline?: ?boolean, [string]: any, }; type ReactElement = React$Element<any>; type ReactElements = React$Node; type MatchFunction = { regex?: RegExp } & ( source: string, state: State, prevCapture: string ) => ?Capture; type Parser = ( source: string, state?: ?State ) => Array<SingleASTNode>; type ParseFunction = ( capture: Capture, nestedParse: Parser, state: State, ) => (UnTypedASTNode | ASTNode); type SingleNodeParseFunction = ( capture: Capture, nestedParse: Parser, state: State, ) => UnTypedASTNode; type Output<Result> = ( node: ASTNode, state?: ?State ) => Result; type NodeOutput<Result> = ( node: SingleASTNode, nestedOutput: Output<Result>, state: State ) => Result; type ArrayNodeOutput<Result> = ( node: Array<SingleASTNode>, nestedOutput: Output<Result>, state: State ) => Result; type ReactOutput = Output<ReactElements>; type ReactNodeOutput = NodeOutput<ReactElements>; type HtmlOutput = Output<string>; type HtmlNodeOutput = NodeOutput<string>; type ParserRule = { +order: number, +match: MatchFunction, +quality?: (capture: Capture, state: State, prevCapture: string) => number, +parse: ParseFunction, }; type SingleNodeParserRule = { +order: number, +match: MatchFunction, +quality?: (capture: Capture, state: State, prevCapture: string) => number, +parse: SingleNodeParseFunction, }; type ReactOutputRule = { // we allow null because some rules are never output results, and that's // legal as long as no parsers return an AST node matching that rule. // We don't use ? because this makes it be explicitly defined as either // a valid function or null, so it can't be forgotten. +react: ReactNodeOutput | null, }; type HtmlOutputRule = { +html: HtmlNodeOutput | null, }; type ArrayRule = { +react?: ArrayNodeOutput<ReactElements>, +html?: ArrayNodeOutput<string>, +[string]: ArrayNodeOutput<any>, }; type ParserRules = { +Array?: ArrayRule, +[type: string]: ParserRule, }; type OutputRules<Rule> = { +Array?: ArrayRule, +[type: string]: Rule }; type Rules<OutputRule> = { +Array?: ArrayRule, +[type: string]: ParserRule & OutputRule, }; type ReactRules = { +Array?: { +react: ArrayNodeOutput<ReactElements>, }, +[type: string]: ParserRule & ReactOutputRule, }; type HtmlRules = { +Array?: { +html: ArrayNodeOutput<string>, }, +[type: string]: ParserRule & HtmlOutputRule, }; // We want to clarify our defaultRules types a little bit more so clients can // reuse defaultRules built-ins. So we make some stronger guarantess when // we can: type NonNullReactOutputRule = { +react: ReactNodeOutput, }; type ElementReactOutputRule = { +react: NodeOutput<ReactElement>, }; type TextReactOutputRule = { +react: NodeOutput<string>, }; type NonNullHtmlOutputRule = { +html: HtmlNodeOutput, }; type DefaultInRule = SingleNodeParserRule & ReactOutputRule & HtmlOutputRule; type TextInOutRule = SingleNodeParserRule & TextReactOutputRule & NonNullHtmlOutputRule; type LenientInOutRule = SingleNodeParserRule & NonNullReactOutputRule & NonNullHtmlOutputRule; type DefaultInOutRule = SingleNodeParserRule & ElementReactOutputRule & NonNullHtmlOutputRule; type DefaultRules = { +Array: { +react: ArrayNodeOutput<ReactElements>, +html: ArrayNodeOutput<string> }, +heading: DefaultInOutRule, +nptable: DefaultInRule, +lheading: DefaultInRule, +hr: DefaultInOutRule, +codeBlock: DefaultInOutRule, +fence: DefaultInRule, +blockQuote: DefaultInOutRule, +list: DefaultInOutRule, +def: LenientInOutRule, +table: DefaultInOutRule, +tableSeparator: DefaultInRule, +newline: TextInOutRule, +paragraph: DefaultInOutRule, +escape: DefaultInRule, +autolink: DefaultInRule, +mailto: DefaultInRule, +url: DefaultInRule, +link: DefaultInOutRule, +image: DefaultInOutRule, +reflink: DefaultInRule, +refimage: DefaultInRule, +em: DefaultInOutRule, +strong: DefaultInOutRule, +u: DefaultInOutRule, +del: DefaultInOutRule, +inlineCode: DefaultInOutRule, +br: DefaultInOutRule, +text: TextInOutRule, }; type RefNode = { type: string, content?: ASTNode, target?: string, title?: string, alt?: string, }; // End Flow Definitions */ var CR_NEWLINE_R = /\r\n?/g; var TAB_R = /\t/g; var FORMFEED_R = /\f/g; /** * Turn various whitespace into easy-to-process whitespace * @param {string} source * @returns {string} */ var preprocess = function(source /* : string */) { return source.replace(CR_NEWLINE_R, '\n') .replace(FORMFEED_R, '') .replace(TAB_R, ' '); }; /** * @param {SimpleMarkdown.OptionalState} givenState * @param {SimpleMarkdown.OptionalState} defaultState * @returns {SimpleMarkdown.State} */ var populateInitialState = function( givenState /* : ?State */, defaultState /* : ?State */ ) /* : State */{ var state /* : State */ = givenState || {}; if (defaultState != null) { for (var prop in defaultState) { if (Object.prototype.hasOwnProperty.call(defaultState, prop)) { state[prop] = defaultState[prop]; } } } return state; }; /** * Creates a parser for a given set of rules, with the precedence * specified as a list of rules. * * @param {SimpleMarkdown.ParserRules} rules * an object containing * rule type -> {match, order, parse} objects * (lower order is higher precedence) * @param {SimpleMarkdown.OptionalState} [defaultState] * * @returns {SimpleMarkdown.Parser} * The resulting parse function, with the following parameters: * @source: the input source string to be parsed * @state: an optional object to be threaded through parse * calls. Allows clients to add stateful operations to * parsing, such as keeping track of how many levels deep * some nesting is. For an example use-case, see passage-ref * parsing in src/widgets/passage/passage-markdown.jsx */ var parserFor = function(rules /*: ParserRules */, defaultState /*: ?State */) { // Sorts rules in order of increasing order, then // ascending rule name in case of ties. var ruleList = Object.keys(rules).filter(function(type) { var rule = rules[type]; if (rule == null || rule.match == null) { return false; } var order = rule.order; if ((typeof order !== 'number' || !isFinite(order)) && typeof console !== 'undefined') { console.warn( "simple-markdown: Invalid order for rule `" + type + "`: " + String(order) ); } return true; }); ruleList.sort(function(typeA, typeB) { var ruleA /* : ParserRule */ = /** @type {SimpleMarkdown.ParserRule} */ (rules[typeA] /*:: :any */); var ruleB /* : ParserRule */ = /** @type {SimpleMarkdown.ParserRule} */ (rules[typeB] /*:: :any */); var orderA = ruleA.order; var orderB = ruleB.order; // First sort based on increasing order if (orderA !== orderB) { return orderA - orderB; } var secondaryOrderA = ruleA.quality ? 0 : 1; var secondaryOrderB = ruleB.quality ? 0 : 1; if (secondaryOrderA !== secondaryOrderB) { return secondaryOrderA - secondaryOrderB; // Then based on increasing unicode lexicographic ordering } else if (typeA < typeB) { return -1; } else if (typeA > typeB) { return 1; } else { // Rules should never have the same name, // but this is provided for completeness. return 0; } }); /** @type {SimpleMarkdown.State} */ var latestState; /** @type {SimpleMarkdown.Parser} */ var nestedParse = function(source /* : string */, state /* : ?State */) { /** @type Array<SimpleMarkdown.SingleASTNode> */ var result = []; state = state || latestState; latestState = state; while (source) { // store the best match, it's rule, and quality: var ruleType = null; var rule = null; var capture = null; var quality = NaN; // loop control variables: var i = 0; var currRuleType = ruleList[0]; var currRule /* : ParserRule */ = /** @type {SimpleMarkdown.ParserRule} */ ( rules[currRuleType] /*:: :any */ ); do { var currOrder = currRule.order; var prevCaptureStr = state.prevCapture == null ? "" : state.prevCapture[0]; var currCapture = currRule.match(source, state, prevCaptureStr); if (currCapture) { var currQuality = currRule.quality ? currRule.quality( currCapture, state, prevCaptureStr ) : 0; // This should always be true the first time because // the initial quality is NaN (that's why there's the // condition negation). if (!(currQuality <= quality)) { ruleType = currRuleType; rule = currRule; capture = currCapture; quality = currQuality; } } // Move on to the next item. // Note that this makes `currRule` be the next item i++; currRuleType = ruleList[i]; currRule = /*::((*/ /** @type {SimpleMarkdown.ParserRule} */ (rules[currRuleType]) /*:: : any) : ParserRule)*/; } while ( // keep looping while we're still within the ruleList currRule && ( // if we don't have a match yet, continue !capture || ( // or if we have a match, but the next rule is // at the same order, and has a quality measurement // functions, then this rule must have a quality // measurement function (since they are sorted before // those without), and we need to check if there is // a better quality match currRule.order === currOrder && currRule.quality ) ) ); // TODO(aria): Write tests for these if (rule == null || capture == null /*:: || ruleType == null */) { throw new Error( "Could not find a matching rule for the below " + "content. The rule with highest `order` should " + "always match content provided to it. Check " + "the definition of `match` for '" + ruleList[ruleList.length - 1] + "'. It seems to not match the following source:\n" + source ); } if (capture.index) { // If present and non-zero, i.e. a non-^ regexp result: throw new Error( "`match` must return a capture starting at index 0 " + "(the current parse index). Did you forget a ^ at the " + "start of the RegExp?" ); } var parsed = rule.parse(capture, nestedParse, state); // We maintain the same object here so that rules can // store references to the objects they return and // modify them later. (oops sorry! but this adds a lot // of power--see reflinks.) if (Array.isArray(parsed)) { Array.prototype.push.apply(result, parsed); } else { // We also let rules override the default type of // their parsed node if they would like to, so that // there can be a single output function for all links, // even if there are several rules to parse them. if (parsed.type == null) { parsed.type = ruleType; } result.push(/** @type {SimpleMarkdown.SingleASTNode} */ (parsed)); } state.prevCapture = capture; source = source.substring(state.prevCapture[0].length); } return result; }; /** @type {SimpleMarkdown.Parser} */ var outerParse = function(source /* : string */, state /* : ?State */) { latestState = populateInitialState(state, defaultState); if (!latestState.inline && !latestState.disableAutoBlockNewlines) { source = source + "\n\n"; } // We store the previous capture so that match functions can // use some limited amount of lookbehind. Lists use this to // ensure they don't match arbitrary '- ' or '* ' in inline // text (see the list rule for more information). This stores // the full regex capture object, if there is one. latestState.prevCapture = null; return nestedParse(preprocess(source), latestState); }; return outerParse; }; // Creates a match function for an inline scoped element from a regex /** @type {(regex: RegExp) => SimpleMarkdown.MatchFunction} */ var inlineRegex = function(regex /* : RegExp */) { /** @type {SimpleMarkdown.MatchFunction} */ var match /* : MatchFunction */ = function(source, state) { if (state.inline) { return regex.exec(source); } else { return null; } }; match.regex = regex; return match; }; // Creates a match function for a block scoped element from a regex /** @type {(regex: RegExp) => SimpleMarkdown.MatchFunction} */ var blockRegex = function(regex /* : RegExp */) { /** @type {SimpleMarkdown.MatchFunction} */ var match /* : MatchFunction */ = function(source, state) { if (state.inline) { return null; } else { return regex.exec(source); } }; match.regex = regex; return match; }; // Creates a match function from a regex, ignoring block/inline scope /** @type {(regex: RegExp) => SimpleMarkdown.MatchFunction} */ var anyScopeRegex = function(regex /* : RegExp */) { /** @type {SimpleMarkdown.MatchFunction} */ var match /* : MatchFunction */ = function(source, state) { return regex.exec(source); }; match.regex = regex; return match; }; var TYPE_SYMBOL = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || 0xeac7; /** * @param {string} type * @param {string | number | null | undefined} key * @param {Object<string, any>} props * @returns {SimpleMarkdown.ReactElement} */ var reactElement = function( type /* : string */, key /* : string | number | null | void */, props /* : { [string]: any } */ ) /* : ReactElement */ { var element /* : ReactElement */ = /** @type {SimpleMarkdown.ReactElement} */ ({ $$typeof: TYPE_SYMBOL, type: type, key: key == null ? undefined : key, ref: null, props: props, _owner: null } /* : any */); return element; }; /** Returns a closed HTML tag. * @param {string} tagName - Name of HTML tag (eg. "em" or "a") * @param {string} content - Inner content of tag * @param {{ [attr: string]: SimpleMarkdown.Attr }} [attributes] - Optional extra attributes of tag as an object of key-value pairs * eg. { "href": "http://google.com" }. Falsey attributes are filtered out. * @param {boolean} [isClosed] - boolean that controls whether tag is closed or not (eg. img tags). * defaults to true */ var htmlTag = function( tagName /* : string */, content /* : string */, attributes /* : ?{[any]: ?Attr} */, isClosed /* : ?boolean */ ) { attributes = attributes || {}; isClosed = typeof isClosed !== 'undefined' ? isClosed : true; var attributeString = ""; for (var attr in attributes) { var attribute = attributes[attr]; // Removes falsey attributes if (Object.prototype.hasOwnProperty.call(attributes, attr) && attribute) { attributeString += " " + sanitizeText(attr) + '="' + sanitizeText(attribute) + '"'; } } var unclosedTag = "<" + tagName + attributeString + ">"; if (isClosed) { return unclosedTag + content + "</" + tagName + ">"; } else { return unclosedTag; } }; var EMPTY_PROPS = {}; /** * @param {string | null | undefined} url - url to sanitize * @returns {string | null} - url if safe, or null if a safe url could not be made */ var sanitizeUrl = function(url /* : ?string */) { if (url == null) { return null; } try { var prot = decodeURIComponent(url) .replace(/[^A-Za-z0-9/:]/g, '') .toLowerCase(); if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { return null; } } catch (e) { // decodeURIComponent sometimes throws a URIError // See `decodeURIComponent('a%AFc');` // http://stackoverflow.com/questions/9064536/javascript-decodeuricomponent-malformed-uri-exception return null; } return url; }; var SANITIZE_TEXT_R = /[<>&"']/g; /** @type {any} */ var SANITIZE_TEXT_CODES = { '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;', "`": '&#96;' }; /** * @param {SimpleMarkdown.Attr} text * @returns {string} */ var sanitizeText = function(text /* : Attr */) { return String(text).replace(SANITIZE_TEXT_R, function(chr) { return SANITIZE_TEXT_CODES[chr]; }); }; var UNESCAPE_URL_R = /\\([^0-9A-Za-z\s])/g; /** * @param {string} rawUrlString * @returns {string} */ var unescapeUrl = function(rawUrlString /* : string */) { return rawUrlString.replace(UNESCAPE_URL_R, "$1"); }; /** * Parse some content with the parser `parse`, with state.inline * set to true. Useful for block elements; not generally necessary * to be used by inline elements (where state.inline is already true. * * @param {SimpleMarkdown.Parser} parse * @param {string} content * @param {SimpleMarkdown.State} state * @returns {SimpleMarkdown.ASTNode} */ var parseInline = function(parse, content, state) { var isCurrentlyInline = state.inline || false; state.inline = true; var result = parse(content, state); state.inline = isCurrentlyInline; return result; }; /** * @param {SimpleMarkdown.Parser} parse * @param {string} content * @param {SimpleMarkdown.State} state * @returns {SimpleMarkdown.ASTNode} */ var parseBlock = function(parse, content, state) { var isCurrentlyInline = state.inline || false; state.inline = false; var result = parse(content + "\n\n", state); state.inline = isCurrentlyInline; return result; }; /** * @param {SimpleMarkdown.Capture} capture * @param {SimpleMarkdown.Parser} parse * @param {SimpleMarkdown.State} state * @returns {SimpleMarkdown.UnTypedASTNode} */ var parseCaptureInline = function(capture, parse, state) { return { content: parseInline(parse, capture[1], state) }; }; /** * @returns {SimpleMarkdown.UnTypedASTNode} */ var ignoreCapture = function() { return {}; }; // recognize a `*` `-`, `+`, `1.`, `2.`... list bullet var LIST_BULLET = "(?:[*+-]|\\d+\\.)"; // recognize the start of a list item: // leading space plus a bullet plus a space (` * `) var LIST_ITEM_PREFIX = "( *)(" + LIST_BULLET + ") +"; var LIST_ITEM_PREFIX_R = new RegExp("^" + LIST_ITEM_PREFIX); // recognize an individual list item: // * hi // this is part of the same item // // as is this, which is a new paragraph in the same item // // * but this is not part of the same item var LIST_ITEM_R = new RegExp( LIST_ITEM_PREFIX + "[^\\n]*(?:\\n" + "(?!\\1" + LIST_BULLET + " )[^\\n]*)*(\n|$)", "gm" ); var BLOCK_END_R = /\n{2,}$/; var INLINE_CODE_ESCAPE_BACKTICKS_R = /^ (?= *`)|(` *) $/g; // recognize the end of a paragraph block inside a list item: // two or more newlines at end end of the item var LIST_BLOCK_END_R = BLOCK_END_R; var LIST_ITEM_END_R = / *\n+$/; // check whether a list item has paragraphs: if it does, // we leave the newlines at the end var LIST_R = new RegExp( "^( *)(" + LIST_BULLET + ") " + "[\\s\\S]+?(?:\n{2,}(?! )" + "(?!\\1" + LIST_BULLET + " )\\n*" + // the \\s*$ here is so that we can parse the inside of nested // lists, where our content might end before we receive two `\n`s "|\\s*\n*$)" ); var LIST_LOOKBEHIND_R = /(?:^|\n)( *)$/; var TABLES = (function() { var TABLE_ROW_SEPARATOR_TRIM = /^ *\| *| *\| *$/g; var TABLE_CELL_END_TRIM = / *$/; var TABLE_RIGHT_ALIGN = /^ *-+: *$/; var TABLE_CENTER_ALIGN = /^ *:-+: *$/; var TABLE_LEFT_ALIGN = /^ *:-+ *$/; /** * @param {string} alignCapture * @returns {SimpleMarkdown.TableAlignment} */ var parseTableAlignCapture = function(alignCapture) { if (TABLE_RIGHT_ALIGN.test(alignCapture)) { return "right"; } else if (TABLE_CENTER_ALIGN.test(alignCapture)) { return "center"; } else if (TABLE_LEFT_ALIGN.test(alignCapture)) { return "left"; } else { return null; } }; /** * @param {string} source * @param {SimpleMarkdown.Parser} parse * @param {SimpleMarkdown.State} state * @param {boolean} trimEndSeparators * @returns {Array<SimpleMarkdown.TableAlignment>} */ var parseTableAlign = function(source, parse, state, trimEndSeparators) { if (trimEndSeparators) { source = source.replace(TABLE_ROW_SEPARATOR_TRIM, ""); } var alignText = source.trim().split("|"); return alignText.map(parseTableAlignCapture); }; /** * @param {string} source * @param {SimpleMarkdown.Parser} parse * @param {SimpleMarkdown.State} state * @param {boolean} trimEndSeparators * @returns {SimpleMarkdown.SingleASTNode[][]} */ var parseTableRow = function(source, parse, state, trimEndSeparators) { var prevInTable = state.inTable; state.inTable = true; var tableRow = parse(source.trim(), state); state.inTable = prevInTable; /** @type {SimpleMarkdown.SingleASTNode[][]} */ var cells = [[]]; tableRow.forEach(function(node, i) { if (node.type === 'tableSeparator') { // Filter out empty table separators at the start/end: if (!trimEndSeparators || i !== 0 && i !== tableRow.length - 1) { // Split the current row: cells.push([]); } } else { if (node.type === 'text' && ( tableRow[i + 1] == null || tableRow[i + 1].type === 'tableSeparator' )) { node.content = node.content.replace(TABLE_CELL_END_TRIM, ""); } cells[cells.length - 1].push(node); } }); return cells; }; /** * @param {string} source * @param {SimpleMarkdown.Parser} parse * @param {SimpleMarkdown.State} state * @param {boolean} trimEndSeparators * @returns {SimpleMarkdown.ASTNode[][]} */ var parseTableCells = function(source, parse, state, trimEndSeparators) { var rowsText = source.trim().split("\n"); return rowsText.map(function(rowText) { return parseTableRow(rowText, parse, state, trimEndSeparators); }); }; /** * @param {boolean} trimEndSeparators * @returns {SimpleMarkdown.SingleNodeParseFunction} */ var parseTable = function(trimEndSeparators) { /** @type {SimpleMarkdown.SingleNodeParseFunction} */ return function(capture, parse, state) { state.inline = true; var header = parseTableRow(capture[1], parse, state, trimEndSeparators); var align = parseTableAlign(capture[2], parse, state, trimEndSeparators); var cells = parseTableCells(capture[3], parse, state, trimEndSeparators); state.inline = false; return { type: "table", header: header, align: align, cells: cells }; }; }; return { parseTable: parseTable(true), parseNpTable: parseTable(false), TABLE_REGEX: /^ *(\|.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/, NPTABLE_REGEX: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/ }; })(); var LINK_INSIDE = "(?:\\[[^\\]]*\\]|[^\\[\\]]|\\](?=[^\\[]*\\]))*"; var LINK_HREF_AND_TITLE = "\\s*<?((?:\\([^)]*\\)|[^\\s\\\\]|\\\\.)*?)>?(?:\\s+['\"]([\\s\\S]*?)['\"])?\\s*"; var AUTOLINK_MAILTO_CHECK_R = /mailto:/i; /** * @param {SimpleMarkdown.Capture} capture * @param {SimpleMarkdown.State} state * @param {SimpleMarkdown.RefNode} refNode * @returns {SimpleMarkdown.RefNode} */ var parseRef = function(capture, state, refNode /* : RefNode */) { var ref = (capture[2] || capture[1]) .replace(/\s+/g, ' ') .toLowerCase(); // We store information about previously seen defs on // state._defs (_ to deconflict with client-defined // state). If the def for this reflink/refimage has // already been seen, we can use its target/source // and title here: if (state._defs && state._defs[ref]) { var def = state._defs[ref]; // `refNode` can be a link or an image. Both use // target and title properties. refNode.target = def.target; refNode.title = def.title; } // In case we haven't seen our def yet (or if someone // overwrites that def later on), we add this node // to the list of ref nodes for that def. Then, when // we find the def, we can modify this link/image AST // node :). // I'm sorry. state._refs = state._refs || {}; state._refs[ref] = state._refs[ref] || []; state._refs[ref].push(refNode); return refNode; }; var currOrder = 0; /** @type {SimpleMarkdown.DefaultRules} */ var defaultRules /* : DefaultRules */ = { Array: { react: function(arr, output, state) { var oldKey = state.key; var result /* : Array<ReactElements> */ = []; // map output over the ast, except group any text // nodes together into a single string output. for (var i = 0, key = 0; i < arr.length; i++, key++) { // `key` is our numerical `state.key`, which we increment for // every output node, but don't change for joined text nodes. // (i, however, must change for joined text nodes) state.key = '' + i; var node = arr[i]; if (node.type === 'text') { node = { type: 'text', content: node.content }; for (; i + 1 < arr.length && arr[i + 1].type === 'text'; i++) { node.content += arr[i + 1].content; } } result.push(output(node, state)); } state.key = oldKey; return result; }, html: function(arr, output, state) { var result = ""; // map output over the ast, except group any text // nodes together into a single string output. for (var i = 0; i < arr.length; i++) { var node = arr[i]; if (node.type === 'text') { node = { type: 'text', content: node.content }; for (; i + 1 < arr.length && arr[i + 1].type === 'text'; i++) { node.content += arr[i + 1].content; } } result += output(node, state); } return result; } }, heading: { order: currOrder++, match: blockRegex(/^ *(#{1,6})([^\n]+?)#* *(?:\n *)+\n/), parse: function(capture, parse, state) { return { level: capture[1].length, content: parseInline(parse, capture[2].trim(), state) }; }, react: function(node, output, state) { return reactElement( 'h' + node.level, state.key, { children: output(node.content, state) } ); }, html: function(node, output, state) { return htmlTag("h" + node.level, output(node.content, state)); } }, nptable: { order: currOrder++, match: blockRegex(TABLES.NPTABLE_REGEX), parse: TABLES.parseNpTable, react: null, html: null }, lheading: { order: currOrder++, match: blockRegex(/^([^\n]+)\n *(=|-){3,} *(?:\n *)+\n/), parse: function(capture, parse, state) { return { type: "heading", level: capture[2] === '=' ? 1 : 2, content: parseInline(parse, capture[1], state) }; }, react: null, html: null }, hr: { order: currOrder++, match: blockRegex(/^( *[-*_]){3,} *(?:\n *)+\n/), parse: ignoreCapture, react: function(node, output, state) { return reactElement( 'hr', state.key, EMPTY_PROPS ); }, html: function(node, output, state) { return "<hr>"; } }, codeBlock: { order: currOrder++, match: blockRegex(/^(?: [^\n]+\n*)+(?:\n *)+\n/), parse: function(capture, parse, state) { var content = capture[0] .replace(/^ /gm, '') .replace(/\n+$/, ''); return { lang: undefined, content: content }; }, react: function(node, output, state) { var className = node.lang ? "markdown-code-" + node.lang : undefined; return reactElement( 'pre', state.key, { children: reactElement( 'code', null, { className: className, children: node.content } ) } ); }, html: function(node, output, state) { var className = node.lang ? "markdown-code-" + node.lang : undefined; var codeBlock = htmlTag("code", sanitizeText(node.content), { class: className }); return htmlTag("pre", codeBlock); } }, fence: { order: currOrder++, match: blockRegex(/^ *(`{3,}|~{3,}) *(?:(\S+) *)?\n([\s\S]+?)\n?\1 *(?:\n *)+\n/), parse: function(capture, parse, state) { return { type: "codeBlock", lang: capture[2] || undefined, content: capture[3] }; }, react: null, html: null }, blockQuote: { order: currOrder++, match: blockRegex(/^( *>[^\n]+(\n[^\n]+)*\n*)+\n{2,}/), parse: function(capture, parse, state) { var content = capture[0].replace(/^ *> ?/gm, ''); return { content: parse(content, state) }; }, react: function(node, output, state) { return reactElement( 'blockquote', state.key, { children: output(node.content, state) } ); }, html: function(node, output, state) { return htmlTag("blockquote", output(node.content, state)); } }, list: { order: currOrder++, match: function(source, state) { // We only want to break into a list if we are at the start of a // line. This is to avoid parsing "hi * there" with "* there" // becoming a part of a list. // You might wonder, "but that's inline, so of course it wouldn't // start a list?". You would be correct! Except that some of our // lists can be inline, because they might be inside another list, // in which case we can parse with inline scope, but need to allow // nested lists inside this inline scope. var prevCaptureStr = state.prevCapture == null ? "" : state.prevCapture[0]; var isStartOfLineCapture = LIST_LOOKBEHIND_R.exec(prevCaptureStr); var isListBlock = state._list || !state.inline; if (isStartOfLineCapture && isListBlock) { source = isStartOfLineCapture[1] + source; return LIST_R.exec(source); } else { return null; } }, parse: function(capture, parse, state) { var bullet = capture[2]; var ordered = bullet.length > 1; var start = ordered ? +bullet : undefined; var items = /** @type {string[]} */ ( capture[0] .replace(LIST_BLOCK_END_R, "\n") .match(LIST_ITEM_R) ); // We know this will match here, because of how the regexes are // defined /*:: items = ((items : any) : Array<string>) */ var lastItemWasAParagraph = false; var itemContent = items.map(function(/** @type {string} */ item, /** @type {number} */ i) { // We need to see how far indented this item is: var prefixCapture = LIST_ITEM_PREFIX_R.exec(item); var space = prefixCapture ? prefixCapture[0].length : 0; // And then we construct a regex to "unindent" the subsequent // lines of the items by that amount: var spaceRegex = new RegExp("^ {1," + space + "}", "gm"); // Before processing the item, we need a couple things var content = item // remove indents on trailing lines: .replace(spaceRegex, '') // remove the bullet: .replace(LIST_ITEM_PREFIX_R, ''); // I'm not sur4 why this is necessary again? /*:: items = ((items : any) : Array<string>) */ // Handling "loose" lists, like: // // * this is wrapped in a paragraph // // * as is this // // * as is this var isLastItem = (i === items.length - 1); var containsBlocks = content.indexOf("\n\n") !== -1; // Any element in a list is a block if it contains multiple // newlines. The last element in the list can also be a block // if the previous item in the list was a block (this is // because non-last items in the list can end with \n\n, but // the last item can't, so we just "inherit" this property // from our previous element). var thisItemIsAParagraph = containsBlocks || (isLastItem && lastItemWasAParagraph); lastItemWasAParagraph = thisItemIsAParagraph; // backup our state for restoration afterwards. We're going to // want to set state._list to true, and state.inline depending // on our list's looseness. var oldStateInline = state.inline; var oldStateList = state._list; state._list = true; // Parse inline if we're in a tight list, or block if we're in // a loose list. var adjustedContent; if (thisItemIsAParagraph) { state.inline = false; adjustedContent = content.replace(LIST_ITEM_END_R, "\n\n"); } else { state.inline = true; adjustedContent = content.replace(LIST_ITEM_END_R, ""); } var result = parse(adjustedContent, state); // Restore our state before returning state.inline = oldStateInline; state._list = oldStateList; return result; }); return { ordered: ordered, start: start, items: itemContent }; }, react: function(node, output, state) { var ListWrapper = node.ordered ? "ol" : "ul"; return reactElement( ListWrapper, state.key, { start: node.start, children: node.items.map(function( /** @type {SimpleMarkdown.ASTNode} */ item, /** @type {number} */ i ) { return reactElement( 'li', '' + i, { children: output(item, state) } ); }) } ); }, html: function(node, output, state) { var listItems = node.items.map(function(/** @type {SimpleMarkdown.ASTNode} */ item) { return htmlTag("li", output(item, state)); }).join(""); var listTag = node.ordered ? "ol" : "ul"; var attributes = { start: node.start }; return htmlTag(listTag, listItems, attributes); } }, def: { order: currOrder++, // TODO(aria): This will match without a blank line before the next // block element, which is inconsistent with most of the rest of // simple-markdown. match: blockRegex( /^ *\[([^\]]+)\]: *<?([^\s>]*)>?(?: +["(]([^\n]+)[")])? *\n(?: *\n)*/ ), parse: function(capture, parse, state) { var def = capture[1] .replace(/\s+/g, ' ') .toLowerCase(); var target = capture[2]; var title = capture[3]; // Look for previous links/images using this def // If any links/images using this def have already been declared, // they will have added themselves to the state._refs[def] list // (_ to deconflict with client-defined state). We look through // that list of reflinks for this def, and modify those AST nodes // with our newly found information now. // Sorry :(. if (state._refs && state._refs[def]) { // `refNode` can be a link or an image state._refs[def].forEach(function(/** @type {SimpleMarkdown.RefNode} */ refNode) { refNode.target = target; refNode.title = title; }); } // Add this def to our map of defs for any future links/images // In case we haven't found any or all of the refs referring to // this def yet, we add our def to the table of known defs, so // that future reflinks can modify themselves appropriately with // this information. state._defs = state._defs || {}; state._defs[def] = { target: target, title: title, }; // return the relevant parsed information // for debugging only. return { def: def, target: target, title: title, }; }, react: function() { return null; }, html: function() { return ""; } }, table: { order: currOrder++, match: blockRegex(TABLES.TABLE_REGEX), parse: TABLES.parseTable, react: function(node, output, state) { /** * @param {number} colIndex * @returns {{ [attr: string]: SimpleMarkdown.Attr }} */ var getStyle = function(colIndex) { return node.align[colIndex] == null ? {} : { textAlign: node.align[colIndex] }; }; var headers = node.header.map(function( /** @type {SimpleMarkdown.ASTNode} */ content, /** @type {number} */ i ) { return reactElement( 'th', '' + i, { style: getStyle(i), scope: 'col', children: output(content, state) } ); }); var rows = node.cells.map(function( /** @type {SimpleMarkdown.ASTNode[]} */ row, /** @type {number} */ r ) { return reactElement( 'tr', '' + r, { children: row.map(function( /** @type {SimpleMarkdown.ASTNode} */ content, /** @type {number} */ c ) { return reactElement( 'td', '' + c, { style: getStyle(c), children: output(content, state) } ); }) } ); }); return reactElement( 'table', state.key, { children: [reactElement( 'thead', 'thead', { children: reactElement( 'tr', null, { children: headers } ) } ), reactElement( 'tbody', 'tbody', { children: rows } )] } ); }, html: function(node, output, state) { /** * @param {number} colIndex * @returns {string} */ var getStyle = function(colIndex) { return node.align[colIndex] == null ? "" : "text-align:" + node.align[colIndex] + ";"; }; var headers = node.header.map(function( /** @type {SimpleMarkdown.ASTNode} */ content, /** @type {number} */ i ) { return htmlTag("th", output(content, state), { style: getStyle(i), scope: "col" }); }).join(""); var rows = node.cells.map(function(/** @type {SimpleMarkdown.ASTNode[]} */ row) { var cols = row.map(function( /** @type {SimpleMarkdown.ASTNode} */ content, /** @type {number} */ c ) { return htmlTag("td", output(content, state), { style: getStyle(c) }); }).join(""); return htmlTag("tr", cols); }).join(""); var thead = htmlTag("thead", htmlTag("tr", headers)); var tbody = htmlTag("tbody", rows); return htmlTag("table", thead + tbody); } }, newline: { order: currOrder++, match: blockRegex(/^(?:\n *)*\n/), parse: ignoreCapture, react: function(node, output, state) { return "\n"; }, html: function(node, output, state) { return "\n"; } }, paragraph: { order: currOrder++, match: blockRegex(/^((?:[^\n]|\n(?! *\n))+)(?:\n *)+\n/), parse: parseCaptureInline, react: function(node, output, state) { return reactElement( 'div', state.key, { className: 'paragraph', children: output(node.content, state) } ); }, html: function(node, output, state) { var attributes = { class: 'paragraph' }; return htmlTag("div", output(node.content, state),