@skybloxsystems/ticket-bot
Version:
1,415 lines (1,296 loc) • 72.4 kB
JavaScript
(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 = {
'<': '<',
'>': '>',
'&': '&',
'"': '"',
"'": ''',
'/': '/',
"`": '`'
};
/**
* @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),