mirror of
https://github.com/gurnec/removeddit.git
synced 2026-03-11 08:54:27 +00:00
3950 lines
104 KiB
JavaScript
3950 lines
104 KiB
JavaScript
/**
|
|
@preserve snuownd.js - javascript port of reddit's "snudown" markdown parser
|
|
https://github.com/gamefreak/snuownd
|
|
*/
|
|
/**
|
|
* @license Copyright (c) 2009, Natacha Porté
|
|
* Copyright (c) 2011, Vicent Marti
|
|
* Copyright (c) 2012, Scott McClaugherty
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
// up to date with commit e518f5077b072599dea87596f4ce3c1d03e443c0
|
|
|
|
/**
|
|
@module SnuOwnd
|
|
*/
|
|
(function(exports){
|
|
function _isspace(c) {return c == ' ' || c == '\n';}
|
|
function isspace(c) {return /[\x09-\x0d ]/.test(c);}
|
|
function isalnum(c) { return /[A-Za-z0-9]/.test(c); }
|
|
function isalpha(c) { return /[A-Za-z]/.test(c); }
|
|
function isdigit(c) { return /[0-9]/.test(c); }
|
|
function isxdigit(c) { return /[0-9a-fA-F]/.test(c); }
|
|
function ispunct(c) {return /[\x20-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]/.test(c); }
|
|
|
|
function urlHexCode(number) {
|
|
var hex_str = '0123456789ABCDEF';
|
|
return '%'+hex_str[(number&0xf0)>>4]+hex_str[(number&0x0f)>>0];
|
|
}
|
|
|
|
function escapeUTF8Char(char) {
|
|
var code = char.charCodeAt(0);
|
|
if (code < 0x80) {
|
|
return urlHexCode(code);
|
|
} else if((code > 0x7f) && (code < 0x0800)) {
|
|
var seq = urlHexCode(code >> 6 & 0xff | 0xc0);
|
|
seq += urlHexCode(code >> 0 & 0x3f | 0x80);
|
|
return seq;
|
|
} else {
|
|
var seq = urlHexCode(code >> 12 & 0xff | 0xe0);
|
|
seq += urlHexCode(code >> 6 & 0x3f | 0x80);
|
|
seq += urlHexCode(code >> 0 & 0x3f | 0x80);
|
|
return seq;
|
|
}
|
|
}
|
|
|
|
function find_block_tag (str) {
|
|
var wordList = [
|
|
'p', 'dl', 'div', 'math',
|
|
'table', 'ul', 'del', 'form',
|
|
'blockquote', 'figure', 'ol', 'fieldset',
|
|
'h1', 'h6', 'pre', 'script',
|
|
'h5', 'noscript', 'style', 'iframe',
|
|
'h4', 'ins', 'h3', 'h2'
|
|
];
|
|
if (wordList.indexOf(str.toLowerCase()) != -1) {
|
|
return str.toLowerCase();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function sdhtml_is_tag(tag_data, tagname) {
|
|
var i;
|
|
var closed = 0;
|
|
var tag_size = tag_data.length;
|
|
|
|
if (tag_size < 3 || tag_data[0] != '<') return HTML_TAG_NONE;
|
|
|
|
i = 1;
|
|
|
|
if (tag_data[i] == '/') {
|
|
closed = 1;
|
|
i++;
|
|
}
|
|
|
|
var tagname_c = 0;
|
|
for (; i < tag_size; ++i, ++tagname_c) {
|
|
if (tagname_c >= tagname.length) break;
|
|
|
|
if (tag_data[i] != tagname[tagname_c]) return HTML_TAG_NONE;
|
|
}
|
|
|
|
if (i == tag_size) return HTML_TAG_NONE;
|
|
|
|
if (isspace(tag_data[i]) || tag_data[i] == '>')
|
|
return closed ? HTML_TAG_CLOSE : HTML_TAG_OPEN;
|
|
|
|
return HTML_TAG_NONE;
|
|
}
|
|
|
|
function unscape_text(out, src) {
|
|
var i = 0, org;
|
|
while (i < src.s.length) {
|
|
org = i;
|
|
while (i < src.s.length && src.s[i] != '\\') i++;
|
|
|
|
if (i > org) out.s += src.s.slice(org, i);
|
|
|
|
if (i + 1 >= src.s.length) break;
|
|
|
|
out.s += src.s[i + 1];
|
|
i += 2;
|
|
}
|
|
}
|
|
|
|
|
|
// Parsers tend to choke on entities with values greater than this
|
|
var MAX_NUM_ENTITY_VAL = 0x10ffff;
|
|
|
|
function is_valid_numeric_entity(entity_val) {
|
|
/* Some XML parsers will choke on entities with certain
|
|
* values (mostly control characters.)
|
|
*
|
|
* According to lxml these are all problematic:
|
|
*
|
|
* [xrange(0, 8),
|
|
* xrange(11, 12),
|
|
* xrange(14, 31),
|
|
* xrange(55296, 57343),
|
|
* xrange(65534, 65535)]
|
|
*/
|
|
return (entity_val > 8
|
|
&& (entity_val !== 11 && entity_val > 12)
|
|
&& (entity_val < 14 || entity_val > 31)
|
|
&& (entity_val < 55296 || entity_val > 57343)
|
|
&& (entity_val !== 65534 && entity_val !== 65535)
|
|
&& entity_val <= MAX_NUM_ENTITY_VAL);
|
|
}
|
|
// Any numeric entity longer than this is obviously above MAX_ENTITY_CHAR
|
|
// used to avoid dealing with overflows.
|
|
var MAX_NUM_ENTITY_LEN = 7;
|
|
var VALID_ENTITIES = [
|
|
'Æ', 'Á', 'Â', 'À', 'Α', 'Å',
|
|
'Ã', 'Ä', 'Β', 'Ç', 'Χ', '‡', 'Δ',
|
|
'Ð', 'É', 'Ê', 'È', 'Ε', 'Η', 'Ë',
|
|
'Γ', 'Í', 'Î', 'Ì', 'Ι', 'Ï', 'Κ',
|
|
'Λ', 'Μ', 'Ñ', 'Ν', 'Œ', 'Ó', 'Ô',
|
|
'Ò', 'Ω', 'Ο', 'Ø', 'Õ', 'Ö', 'Φ',
|
|
'Π', '″', 'Ψ', 'Ρ', 'Š', 'Σ', 'Þ', 'Τ',
|
|
'Θ', 'Ú', 'Û', 'Ù', 'Υ', 'Ü', 'Ξ',
|
|
'Ý', 'Ÿ', 'Ζ', 'á', 'â', '´', 'æ',
|
|
'à', 'ℵ', 'α', '&', '∧', '∠', ''',
|
|
'å', '≈', 'ã', 'ä', '„', 'β', '¦',
|
|
'•', '∩', 'ç', '¸', '¢', 'χ', 'ˆ',
|
|
'♣', '≅', '©', '↵', '∪', '¤', '⇓',
|
|
'†', '↓', '°', 'δ', '♦', '÷',
|
|
|
|
'é', 'ê', 'è', '∅', ' ', ' ',
|
|
'ε', '≡', 'η', 'ð', 'ë', '€', '∃',
|
|
'ƒ', '∀', '½', '¼', '¾', '⁄',
|
|
'γ','≥', '>', '⇔', '↔', '♥', '…',
|
|
'í', 'î', '¡', 'ì', 'ℑ',
|
|
|
|
'∞',
|
|
'∫', 'ι', '¿', '∈', 'ï', 'κ', '⇐',
|
|
'λ', '⟨', '«', '←', '⌈', '“', '≤',
|
|
'⌊', '∗', '◊', '‎', '‹', '‘', '<',
|
|
'¯', '—', 'µ', '·', '−', 'μ', '∇',
|
|
' ', '–', '≠', '∋', '¬', '∉', '⊄', 'ñ',
|
|
'ν', 'ó', 'ô', 'œ', 'ò', '‾', 'ω',
|
|
'ο', '⊕', '∨', 'ª', 'º', 'ø', 'õ',
|
|
'⊗', 'ö', '¶', '∂', '‰', '⊥', 'φ',
|
|
'π', 'ϖ', '±', '£', '′', '∏', '∝',
|
|
'ψ', '"', '⇒', '√', '⟩', '»', '→',
|
|
'⌉', '”', 'ℜ', '®', '⌋', 'ρ', '‏',
|
|
'›', '’', '‚', 'š', '⋅', '§', '­',
|
|
'σ', 'ς', '∼', '♠', '⊂', '⊆', '∑',
|
|
'¹', '²', '³', '⊃', '⊇', 'ß', 'τ',
|
|
'∴', 'θ', 'ϑ', ' ', 'þ', '˜',
|
|
'×', '™', '⇑', 'ú', '↑', 'û', 'ù',
|
|
'¨', 'ϒ', 'υ', 'ü', '℘', 'ξ', 'ý',
|
|
'¥', 'ÿ', 'ζ', '‍', '‌',
|
|
];
|
|
|
|
/**
|
|
* According to the OWASP rules:
|
|
*
|
|
* & --> &
|
|
* < --> <
|
|
* > --> >
|
|
* " --> "
|
|
* ' --> ' ' is not recommended
|
|
* / --> / forward slash is included as it helps end an HTML entity
|
|
*
|
|
*/
|
|
var HTML_ESCAPE_TABLE = [
|
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 7, 7, 0, 7, 7,
|
|
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|
0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
|
];
|
|
|
|
var HTML_ESCAPES = ["", """, "&", "'", "/", "<", ">", "" /* throw out control characters */ ];
|
|
|
|
function escape_html(out, src, secure) {
|
|
var i = 0, org, esc = 0;
|
|
while (i < src.length) {
|
|
org = i;
|
|
while (i < src.length && !(esc = HTML_ESCAPE_TABLE[src.charCodeAt(i)]))
|
|
i++;
|
|
|
|
if (i > org) out.s += src.slice(org, i);
|
|
|
|
/* escaping */
|
|
if (i >= src.length) break;
|
|
|
|
/* The forward slash is only escaped in secure mode */
|
|
if (src[i] == '/' && !secure) {
|
|
out.s += '/';
|
|
} else if (HTML_ESCAPE_TABLE[src.charCodeAt(i)] == 7) {
|
|
/* skip control characters */
|
|
} else {
|
|
out.s += HTML_ESCAPES[esc];
|
|
}
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* The following characters will not be escaped:
|
|
*
|
|
* -_.+!*'(),%#@?=;:/,+&$ alphanum
|
|
*
|
|
* Note that this character set is the addition of:
|
|
*
|
|
* - The characters which are safe to be in an URL
|
|
* - The characters which are *not* safe to be in
|
|
* an URL because they are RESERVED characters.
|
|
*
|
|
* We asume (lazily) that any RESERVED char that
|
|
* appears inside an URL is actually meant to
|
|
* have its native function (i.e. as an URL
|
|
* component/separator) and hence needs no escaping.
|
|
*
|
|
* There are two exceptions: the chacters & (amp)
|
|
* and ' (single quote) do not appear in the table.
|
|
* They are meant to appear in the URL as components,
|
|
* yet they require special HTML-entity escaping
|
|
* to generate valid HTML markup.
|
|
*
|
|
* All other characters will be escaped to %XX.
|
|
*
|
|
*/
|
|
var HREF_SAFE = [
|
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 0, 2, 2,
|
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
|
0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
|
|
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
|
];
|
|
|
|
function escape_href(out, src) {
|
|
var i = 0, org;
|
|
|
|
|
|
while (i < src.length) {
|
|
org = i;
|
|
/* Skip by characters that don't need special processing */
|
|
while (i < src.length && HREF_SAFE[src.charCodeAt(i)] === 1) i++;
|
|
|
|
if (i > org) out.s += src.slice(org, i);
|
|
|
|
/* escaping */
|
|
if (i >= src.length) break;
|
|
|
|
/* throw out control characters */
|
|
if (HREF_SAFE[src.charCodeAt(i)] == 2) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
switch (src[i]) {
|
|
/* amp appears all the time in URLs, but needs
|
|
* HTML-entity escaping to be inside an href */
|
|
case '&':
|
|
out.s += '&';
|
|
break;
|
|
|
|
/* the single quote is a valid URL character
|
|
* according to the standard; it needs HTML
|
|
* entity escaping too */
|
|
case '\'':
|
|
out.s += ''';
|
|
break;
|
|
|
|
/* the space can be escaped to %20 or a plus
|
|
* sign. we're going with the generic escape
|
|
* for now. the plus thing is more commonly seen
|
|
* when building GET strings */
|
|
/*
|
|
//This was disabled
|
|
case ' ':
|
|
out.s += '+'
|
|
break;
|
|
//*/
|
|
|
|
/* every other character goes with a %XX escaping */
|
|
default:
|
|
out.s += escapeUTF8Char(src[i]);
|
|
/*
|
|
var cc = src.charCodeAt(i);
|
|
hex_str[1] = hex_chars[(cc >> 4) & 0xF];
|
|
hex_str[2] = hex_chars[cc & 0xF];
|
|
out.s += hex_str.join('');
|
|
*/
|
|
}
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
|
|
// function autolink_delim(uint8_t *data, size_t link_end, size_t offset, size_t size)
|
|
function autolink_delim(data_, link_end, offset, size) {
|
|
var data = data_.slice(offset)
|
|
var cclose, copen = 0;
|
|
var i;
|
|
|
|
for (i = 0; i < link_end; ++i)
|
|
if (data[i] == '<') {
|
|
link_end = i;
|
|
break;
|
|
}
|
|
|
|
while (link_end > 0) {
|
|
var c = data[link_end - 1];
|
|
if (c === "\0") break;
|
|
|
|
if ("?!.,".indexOf(c) !== -1) {
|
|
link_end--;
|
|
} else if (c === ';') {
|
|
var new_end = link_end - 2;
|
|
|
|
while (new_end > 0 && isalpha(data[new_end])) new_end--;
|
|
|
|
if (new_end < link_end - 2 && data[new_end] == '&')
|
|
link_end = new_end;
|
|
else link_end--;
|
|
}
|
|
else break;
|
|
}
|
|
|
|
if (link_end == 0) return 0;
|
|
|
|
cclose = data[link_end - 1];
|
|
|
|
switch (cclose) {
|
|
case '"': copen = '"'; break;
|
|
case '\'': copen = '\''; break;
|
|
case ')': copen = '('; break;
|
|
case ']': copen = '['; break;
|
|
case '}': copen = '{'; break;
|
|
}
|
|
|
|
if (copen != 0) {
|
|
var closing = 0;
|
|
var opening = 0;
|
|
var j = 0;
|
|
|
|
/* Try to close the final punctuation sign in this same line;
|
|
* if we managed to close it outside of the URL, that means that it's
|
|
* not part of the URL. If it closes inside the URL, that means it
|
|
* is part of the URL.
|
|
*
|
|
* Examples:
|
|
*
|
|
* foo http://www.pokemon.com/Pikachu_(Electric) bar
|
|
* => http://www.pokemon.com/Pikachu_(Electric)
|
|
*
|
|
* foo (http://www.pokemon.com/Pikachu_(Electric)) bar
|
|
* => http://www.pokemon.com/Pikachu_(Electric)
|
|
*
|
|
* foo http://www.pokemon.com/Pikachu_(Electric)) bar
|
|
* => http://www.pokemon.com/Pikachu_(Electric))
|
|
*
|
|
* (foo http://www.pokemon.com/Pikachu_(Electric)) bar
|
|
* => foo http://www.pokemon.com/Pikachu_(Electric)
|
|
*/
|
|
|
|
while (j < link_end) {
|
|
if (data[j] == copen) opening++;
|
|
else if (data[j] == cclose) closing++;
|
|
|
|
j++;
|
|
}
|
|
|
|
if (closing != opening) link_end--;
|
|
}
|
|
|
|
return link_end;
|
|
}
|
|
|
|
function check_domain(data, allow_short) {
|
|
var i, np = 0;
|
|
|
|
if (!isalnum(data[0])) return 0;
|
|
|
|
for (i = 1; i < data.length - 1; ++i) {
|
|
if (data[i] == '.') np++;
|
|
else if (!isalnum(data[i]) && data[i] != '-') break;
|
|
}
|
|
|
|
/* a valid domain needs to have at least a dot.
|
|
* that's as far as we get */
|
|
if (allow_short) {
|
|
/* We don't need a valid domain in the strict sence (with
|
|
* at least one dot; so just make sure it's composed of valid
|
|
* domain characters and return the length of the valid
|
|
* sequence. */
|
|
return i;
|
|
} else {
|
|
return np ? i : 0;
|
|
}
|
|
}
|
|
|
|
function sd_autolink_issafe(link) {
|
|
var valid_uris = [
|
|
"http://", "https://", "ftp://", "mailto://",
|
|
"/", "git://", "steam://", "irc://", "news://", "mumble://",
|
|
"ssh://", "ircs://", "ts3server://", "#"];
|
|
|
|
var i;
|
|
|
|
for (i = 0; i < valid_uris.length; ++i) {
|
|
var len = valid_uris[i].length;
|
|
|
|
if (link.length > len &&
|
|
link.toLowerCase().indexOf(valid_uris[i]) == 0 &&
|
|
/[A-Za-z0-9#\/?]/.test(link[len]))
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
function sd_autolink__url(rewind_p, link, data_, offset, size, flags) {
|
|
var data = data_.slice(offset);
|
|
var link_end, rewind = 0, domain_len;
|
|
|
|
if (size < 4 || data_[offset+1] != '/' || data_[offset+2] != '/') return 0;
|
|
|
|
while (rewind < offset && isalpha(data_[offset-rewind - 1])) rewind++;
|
|
|
|
if (!sd_autolink_issafe(data_.substr(offset-rewind, size+rewind))) return 0;
|
|
link_end = "://".length;
|
|
|
|
domain_len = check_domain(data.slice(link_end), flags & SD_AUTOLINK_SHORT_DOMAINS);
|
|
if (domain_len == 0) return 0;
|
|
|
|
link_end += domain_len;
|
|
while (link_end < size && !isspace(data_[offset+link_end])) link_end++;
|
|
|
|
link_end = autolink_delim(data_, link_end, offset, size);
|
|
|
|
if (link_end == 0) return 0;
|
|
|
|
//TODO
|
|
link.s += data_.substr(offset-rewind, link_end+rewind);
|
|
rewind_p.p = rewind;
|
|
|
|
return link_end;
|
|
}
|
|
|
|
function sd_autolink__subreddit(rewind_p, link, data_, offset, size) {
|
|
var data = data_.slice(offset);
|
|
var link_end;
|
|
var allMinus = false;
|
|
|
|
if (size < 3) return 0;
|
|
|
|
/* make sure this / is part of /r/ */
|
|
if (data.indexOf('/r/') != 0) return 0;
|
|
|
|
link_end = "/r/".length;
|
|
if (data.substr(link_end-1, 4).toLowerCase() == "all-") {
|
|
allMinus = true;
|
|
}
|
|
do {
|
|
var start = link_end;
|
|
var max_length = 24;
|
|
/* special case: /r/reddit.com (the only subreddit with a '.') */
|
|
if ( size >= link_end+10 && data.substr(link_end, 10).toLowerCase() == 'reddit.com') {
|
|
link_end += 10;
|
|
max_length = 10;
|
|
} else {
|
|
/* If not the special case make sure it starts with (t:)?[A-Za-z0-9] */
|
|
/* support autolinking to timereddits, /r/t:when (1 April 2012) */
|
|
if ( size > link_end+2 && data.substr(link_end, 2) == 't:')
|
|
link_end += 2; /* Jump over the 't:' */
|
|
|
|
/* the first character of a subreddit name must be a letter or digit */
|
|
if (!isalnum(data[link_end]))
|
|
return 0;
|
|
link_end += 1;
|
|
}
|
|
|
|
/* consume valid characters ([A-Za-z0-9_]) until we run out */
|
|
while (link_end < size && (isalnum(data[link_end]) ||
|
|
data[link_end] == '_'))
|
|
link_end++;
|
|
|
|
/* valid subreddit names are between 3 and 21 characters, with
|
|
* some subreddits having 2-character names. Don't bother with
|
|
* autolinking for anything outside this length range.
|
|
* (chksrname function in reddit/.../validator.py) */
|
|
if ( link_end-start < 2 || link_end-start > max_length )
|
|
return 0;
|
|
|
|
/* If we are linking to a multireddit, continue */
|
|
} while ( link_end < size && (data[link_end] == '+' || (allMinus && data[link_end] == '-')) && link_end++ );
|
|
if (link_end < size && data[link_end] == '/') {
|
|
while (link_end < size && (isalnum(data[link_end]) ||
|
|
data[link_end] == '_' ||
|
|
data[link_end] == '/' ||
|
|
data[link_end] == '-')) {
|
|
link_end++;
|
|
}
|
|
}
|
|
/* make the link */
|
|
link.s += data.slice(0, link_end);
|
|
rewind_p.p = 0;
|
|
|
|
return link_end;
|
|
}
|
|
|
|
function sd_autolink__username(rewind_p, link, data_, offset, size) {
|
|
var data = data_.slice(offset);
|
|
var link_end;
|
|
|
|
if (size < 6) return 0;
|
|
|
|
/* make sure this / is part of /u/ */
|
|
if (data.indexOf('/u/') != 0) return 0;
|
|
|
|
/* the first letter of a username must... well, be valid, we don't care otherwise */
|
|
link_end = "/u/".length;
|
|
if (!isalnum(data[link_end]) && data[link_end] != '_' && data[link_end] != '-')
|
|
return 0;
|
|
link_end += 1;
|
|
|
|
/* consume valid characters ([A-Za-z0-9_-/]) until we run out */
|
|
while (link_end < size && (isalnum(data[link_end]) ||
|
|
data[link_end] == '_' ||
|
|
data[link_end] == '/' ||
|
|
data[link_end] == '-'))
|
|
link_end++;
|
|
|
|
/* make the link */
|
|
link.s += data.slice(0, link_end);
|
|
rewind_p.p = 0;
|
|
|
|
return link_end;
|
|
}
|
|
|
|
function sd_autolink__email(rewind_p, link, data_, offset, size, flags) {
|
|
var data = data_.slice(offset);
|
|
var link_end, rewind;
|
|
var nb = 0, np = 0;
|
|
|
|
for (rewind = 0; rewind < offset; ++rewind) {
|
|
var c = data_[offset-rewind - 1];
|
|
|
|
if (c == '\0') break;
|
|
|
|
if (isalnum(c)) continue;
|
|
if (".+-_".indexOf(c) != -1) continue;
|
|
break;
|
|
}
|
|
|
|
if (rewind == 0) return 0;
|
|
|
|
for (link_end = 0; link_end < size; ++link_end) {
|
|
var c = data_[offset+link_end];
|
|
|
|
if (isalnum(c)) continue;
|
|
|
|
if (c == '@') nb++;
|
|
else if (c == '.' && link_end < size - 1) np++;
|
|
else if (c != '-' && c != '_') break;
|
|
}
|
|
|
|
if (link_end < 2 || nb != 1 || np == 0) return 0;
|
|
|
|
//TODO
|
|
link_end = autolink_delim(data_, link_end, offset, size);
|
|
|
|
if (link_end == 0) return 0;
|
|
|
|
// link.s += data_.slice(offset - rewind, link_end + rewind
|
|
link.s += data_.substr(offset - rewind, link_end + rewind);
|
|
rewind_p.p = rewind;
|
|
|
|
return link_end;
|
|
}
|
|
|
|
function sd_autolink__www(rewind_p, link, data_, offset, size, flags) {
|
|
var data = data_.slice(offset);
|
|
var link_end;
|
|
|
|
if (offset > 0 && !ispunct(data_[offset-1]) && !isspace(data_[offset-1]))
|
|
return 0;
|
|
|
|
// if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0)
|
|
if (size < 4 || (data.slice(0,4) != 'www.')) return 0;
|
|
|
|
link_end = check_domain(data, 0);
|
|
|
|
if (link_end == 0)
|
|
return 0;
|
|
|
|
while (link_end < size && !isspace(data[link_end])) link_end++;
|
|
|
|
link_end = autolink_delim(data_, link_end, offset, size);
|
|
|
|
if (link_end == 0) return 0;
|
|
|
|
link.s += data.slice(0, link_end);
|
|
rewind_p.p = 0;
|
|
|
|
return link_end;
|
|
}
|
|
|
|
/**
|
|
Initialize a Callbacks object.
|
|
|
|
@constructor
|
|
@param {Object.<string, ?function>} callbacks A set of callbacks to use as the methods on this object.
|
|
*/
|
|
function Callbacks(callbacks) {
|
|
if (callbacks) {
|
|
for (var name in callbacks) {
|
|
if (name in this) this[name] = callbacks[name];
|
|
}
|
|
}
|
|
}
|
|
|
|
Callbacks.prototype = {
|
|
/**
|
|
Renders a code block.
|
|
|
|
Syntax highlighting specific to lanugage may be performed here.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {Buffer} language The name of the code langage.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
blockcode: null,
|
|
/**
|
|
Renders a blockquote.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
blockquote: null,
|
|
/**
|
|
Renders a block of HTML code.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
blockhtml: null,
|
|
/**
|
|
Renders a header.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {Number} level The header level.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
header: null,
|
|
/**
|
|
Renders a horizontal rule.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
hrule: null,
|
|
/**
|
|
Renders a list.
|
|
<p>
|
|
This method handles the list wrapper, which in terms of HTML would be <ol> or <ul>.
|
|
This method is not responsible for handling list elements, all such processing should
|
|
already have occured on text pased to the method . All that it is intended
|
|
to do is to wrap the text parameter in anything needed.
|
|
</p>
|
|
|
|
@example
|
|
out.s += "<ul>" + text.s + "</ul>"
|
|
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input that goes inside the list.
|
|
@param {Number} flags A bitfield holding a portion of the render state. The only bit that this should be concerned with is MKD_LIST_ORDERED
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
list: null,
|
|
/**
|
|
Renders a list.
|
|
<p>
|
|
Wraps the text in a list element.
|
|
</p>
|
|
|
|
@example
|
|
out.s += "<li>" + text.s + "</li>"
|
|
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The contents of the list element.
|
|
@param {Number} flags A bitfield holding a portion of the render state. The only bit that this should be concerned with is MKD_LI_BLOCK.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
listitem: null,
|
|
/**
|
|
Renders a paragraph.
|
|
|
|
@example
|
|
|
|
out.s += "<p>" + text.s + "</p>";
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
paragraph: null,
|
|
/**
|
|
Renders a table.
|
|
|
|
@example
|
|
|
|
out.s += "<table><thead>";
|
|
out.s += header.s;
|
|
out.s += "</thead><tbody>";
|
|
out.s += body.s;
|
|
out.s += "</tbody></table>";
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} head The table header.
|
|
@param {Buffer} body The table body.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
table: null,
|
|
/**
|
|
Renders a table row.
|
|
|
|
@example
|
|
|
|
out.s += "<tr>" + text.s + "</tr>";
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
table_row: null,
|
|
/**
|
|
Renders a table cell.
|
|
|
|
@example
|
|
|
|
out.s += "<td>" + text.s + "</td>";
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {Number} flags A bit filed indicating a portion of the output state. Relevant bits are: MKD_TABLE_HEADER, MKD_TABLE_ALIGN_CENTER. MKD_TABLE_ALIGN_L, and MKD_TABLE_ALIGN_R.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
table_cell: null,
|
|
/**
|
|
Renders a link that was autodetected.
|
|
|
|
@example
|
|
|
|
out.s += "<a href=\""+ text.s + "\">" + text.s + "</a>";
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The address being linked to.
|
|
@param {Number} type Equal to MKDA_NORMAL or MKDA_EMAIL
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
autolink: null,
|
|
/**
|
|
Renders inline code.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being wrapped.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
codespan: null,
|
|
/**
|
|
Renders text with double emphasis. Default is equivalent to the HTML <strong> tag.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being wrapped.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
double_emphasis: null,
|
|
/**
|
|
Renders text with single emphasis. Default is equivalent to the HTML <em> tag.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being wrapped.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
emphasis: null,
|
|
/**
|
|
Renders an image.
|
|
|
|
@example
|
|
|
|
out.s = "<img src=\"" + link.s + "\" title=\"" + title.s + "\" alt=\"" + alt.s + "\"/>";"
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} link The address of the image.
|
|
@param {Buffer} title Title text for the image
|
|
@param {Buffer} alt Alt text for the image
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
image: null,
|
|
/**
|
|
Renders line break.
|
|
|
|
@example
|
|
|
|
out.s += "<br/>";
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
linebreak: null,
|
|
/**
|
|
Renders a link.
|
|
|
|
@example
|
|
|
|
out.s = "<a href=\"" + link.s + "\" title=\"" + title.s + "\">" + content.s + "</a>";
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} link The link address.
|
|
@param {Buffer} title Title text for the link.
|
|
@param {Buffer} content Link text.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
link: null,
|
|
/**
|
|
Copies and potentially escapes some HTML.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The input text.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
raw_html_tag: null,
|
|
/**
|
|
Renders text with triple emphasis. Default is equivalent to both the <em> and <strong> HTML tags.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being wrapped.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
triple_emphasis: null,
|
|
/**
|
|
Renders text crossd out.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being wrapped.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
strikethrough: null,
|
|
/**
|
|
Renders text as superscript.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being wrapped.
|
|
@param {?Object} context A renderer specific context object.
|
|
@returns {Boolean} Whether or not the tag was rendered.
|
|
*/
|
|
superscript: null,
|
|
/**
|
|
Escapes an HTML entity.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being wrapped.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
entity: null,
|
|
/**
|
|
Renders plain text.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {Buffer} text The text being rendered.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
normal_text: null,
|
|
/**
|
|
Creates opening boilerplate for a table of contents.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
doc_header: null,
|
|
/**
|
|
Creates closing boilerplate for a table of contents.
|
|
|
|
@method
|
|
@param {Buffer} out The output string buffer to append to.
|
|
@param {?Object} context A renderer specific context object.
|
|
*/
|
|
doc_footer: null
|
|
};
|
|
|
|
|
|
/**
|
|
A renderer object
|
|
|
|
@constructor
|
|
@param {Callbacks} callbacks The callbacks object to use for the renderer.
|
|
@param {?Callbacks} context Renderer specific context information.
|
|
*/
|
|
function Renderer(callbacks, context) {
|
|
this.callbacks = callbacks;
|
|
this.context = context;
|
|
}
|
|
|
|
/**
|
|
Instantiates a custom Renderer object.
|
|
|
|
@param {Callbacks} callbacks The callbacks object to use for the renderer.
|
|
@param {?Callbacks} context Renderer specific context information.
|
|
@returns {Renderer}
|
|
*/
|
|
function createCustomRenderer(callbacks, context) {
|
|
return new Renderer(callbacks, context)
|
|
}
|
|
exports.createCustomRenderer = createCustomRenderer;
|
|
|
|
function defaultRenderState() {
|
|
return {
|
|
nofollow: 0,
|
|
target: null,
|
|
tocData: {
|
|
headerCount: 0,
|
|
currentLevel: 0,
|
|
levelOffset: 0
|
|
},
|
|
toc_id_prefix: null,
|
|
html_element_whitelist: DEFAULT_HTML_ELEMENT_WHITELIST,
|
|
html_attr_whitelist: DEFAULT_HTML_ATTR_WHITELIST,
|
|
flags: 0,
|
|
//(flags != undefined?flags:HTML_SKIP_HTML | HTML_SKIP_IMAGES | HTML_SAFELINK | HTML_ESCAPE | HTML_USE_XHTML),
|
|
/* extra callbacks */
|
|
// void (*link_attributes)(struct buf *ob, const struct buf *url, void *self);
|
|
link_attributes: function link_attributes(out, url, options) {
|
|
|
|
if (options.nofollow) out.s += ' rel="nofollow"';
|
|
|
|
if (options.target != null) {
|
|
out.s += ' target="' + options.target + '"';
|
|
}
|
|
}
|
|
};
|
|
}
|
|
exports.defaultRenderState = defaultRenderState;
|
|
/**
|
|
Produces a renderer object that will match Reddit's output.
|
|
@param {?Number=} flags A bitfield containing flags specific to the reddit HTML renderer. Passing undefined, null, or null value will produce reddit exact output.
|
|
@returns {Renderer} A renderer object that will match Reddit's output.
|
|
*/
|
|
function getRedditRenderer(flags) {
|
|
var state =defaultRenderState();
|
|
if (flags == null) {
|
|
state.flags = DEFAULT_BODY_FLAGS;
|
|
} else {
|
|
state.flags = flags;
|
|
}
|
|
|
|
var renderer = new Renderer(getRedditCallbacks() , state);
|
|
if (renderer.context.flags & HTML_SKIP_IMAGES)
|
|
renderer.callbacks.image = null;
|
|
|
|
if (renderer.context.flags & HTML_SKIP_LINKS) {
|
|
renderer.callbacks.link = null;
|
|
renderer.callbacks.autolink = null;
|
|
}
|
|
|
|
if (renderer.context.flags & HTML_SKIP_HTML || renderer.context.flags & HTML_ESCAPE)
|
|
renderer.callbacks.blockhtml = null;
|
|
return renderer;
|
|
}
|
|
exports.getRedditRenderer = getRedditRenderer;
|
|
|
|
/**
|
|
Produces a renderer object that will match Reddit's for a table of contents.
|
|
@returns {Renderer} A renderer object that will match Reddit's output.
|
|
*/
|
|
function getTocRenderer() {
|
|
var state = defaultRenderState();
|
|
state.flags = HTML_TOC | HTML_SKIP_HTML;
|
|
var renderer = new Renderer(getTocCallbacks(), state);
|
|
return renderer;
|
|
}
|
|
exports.getTocRenderer = getTocRenderer;
|
|
|
|
/**
|
|
Create a Callbacks object with the given callback table.
|
|
|
|
@param {Object.<string, function>} callbacks A table of callbacks to place int a callbacks object.
|
|
@returns {Callbacks} A callbacks object holding the provided callbacks.
|
|
*/
|
|
function createCustomCallbacks(callbacks) {
|
|
return new Callbacks(callbacks);
|
|
}
|
|
exports.createCustomCallbacks = createCustomCallbacks;
|
|
|
|
/**
|
|
Produce a callbacks object that matches Reddit's output.
|
|
@returns {Callbacks} A callbacks object that matches Reddit's output.
|
|
*/
|
|
function getRedditCallbacks(){
|
|
return new Callbacks({
|
|
blockcode: cb_blockcode,
|
|
blockquote: cb_blockquote,
|
|
blockhtml: cb_blockhtml,
|
|
header: cb_header,
|
|
hrule: cb_hrule,
|
|
list: cb_list,
|
|
listitem: cb_listitem,
|
|
paragraph: cb_paragraph,
|
|
table: cb_table,
|
|
table_row: cb_table_row,
|
|
table_cell: cb_table_cell,
|
|
autolink: cb_autolink,
|
|
codespan: cb_codespan,
|
|
double_emphasis: cb_double_emphasis,
|
|
emphasis: cb_emphasis,
|
|
image: cb_image,
|
|
linebreak: cb_linebreak,
|
|
link: cb_link,
|
|
raw_html_tag: cb_raw_html_tag,
|
|
triple_emphasis: cb_triple_emphasis,
|
|
strikethrough: cb_strikethrough,
|
|
superscript: cb_superscript,
|
|
entity: null,
|
|
normal_text: cb_normal_text,
|
|
doc_header: null,
|
|
doc_footer: cb_reset_toc
|
|
});
|
|
}
|
|
exports.getRedditCallbacks = getRedditCallbacks;
|
|
|
|
/**
|
|
Produce a callbacks object for rendering a table of contents.
|
|
@returns {Callbacks} A callbacks object for rendering a table of contents.
|
|
*/
|
|
function getTocCallbacks() {
|
|
return new Callbacks({
|
|
blockcode: null,
|
|
blockquote: null,
|
|
blockhtml: null,
|
|
header: cb_toc_header,
|
|
hrule: null,
|
|
list: null,
|
|
listitem: null,
|
|
paragraph: null,
|
|
table: null,
|
|
table_row: null,
|
|
table_cell: null,
|
|
autolink: null,
|
|
codespan: cb_codespan,
|
|
double_emphasis: cb_double_emphasis,
|
|
emphasis: cb_emphasis,
|
|
image: null,
|
|
linebreak: null,
|
|
link: cb_toc_link,
|
|
raw_html_tag: null,
|
|
triple_emphasis: cb_triple_emphasis,
|
|
strikethrough: cb_strikethrough,
|
|
superscript: cb_superscript,
|
|
entity: null,
|
|
normal_text: null,
|
|
doc_header: null,
|
|
doc_footer: cb_toc_finalize
|
|
});
|
|
}
|
|
exports.getTocCallbacks = getTocCallbacks;
|
|
|
|
/* block level callbacks - NULL skips the block */
|
|
// void (*blockcode)(struct buf *ob, const struct buf *text, const struct buf *lang, void *opaque);
|
|
function cb_blockcode(out, text, lang, options) {
|
|
if (out.s.length) out.s += '\n';
|
|
|
|
if (lang && lang.s.length) {
|
|
var i, cls;
|
|
out.s += '<pre><code class="';
|
|
|
|
for (i = 0, cls = 0; i < lang.s.length; ++i, ++cls) {
|
|
while (i < lang.s.length && isspace(lang.s[i]))
|
|
i++;
|
|
|
|
if (i < lang.s.length) {
|
|
var org = i;
|
|
while (i < lang.s.length && !isspace(lang.s[i])) i++;
|
|
|
|
if (lang.s[org] == '.') org++;
|
|
|
|
if (cls) out.s += ' ';
|
|
escape_html(out, lang.s.slice(org, i), false);
|
|
}
|
|
}
|
|
|
|
out.s += '">';
|
|
} else
|
|
out.s += '<pre><code>';
|
|
|
|
if (text) escape_html(out, text.s, false);
|
|
|
|
out.s += '</code></pre>\n';
|
|
}
|
|
|
|
// void (*blockquote)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_blockquote(out, text, options) {
|
|
if (out.s.length) out.s += '\n';
|
|
out.s += '<blockquote>\n';
|
|
if (text) out.s += text.s;
|
|
out.s += '</blockquote>\n';
|
|
}
|
|
|
|
// void (*blockhtml)(struct buf *ob,const struct buf *text, void *opaque);
|
|
function cb_blockhtml(out, text, options) {
|
|
var org, sz;
|
|
if (!text) return;
|
|
sz = text.s.length;
|
|
while (sz > 0 && text.s[sz - 1] == '\n') sz--;
|
|
org = 0;
|
|
while (org < sz && text.s[org] == '\n') org++;
|
|
if (org >= sz) return;
|
|
if (out.s.length) out.s += '\n';
|
|
out.s += text.s.slice(org, sz);
|
|
out.s += '\n';
|
|
}
|
|
|
|
// header(Buffer out, Buffer text, int level, void *opaque);
|
|
function cb_header(out, text, level, options) {
|
|
if (out.s.length) out.s += '\n';
|
|
|
|
if (options.flags & HTML_TOC) {
|
|
out.s += '<h' + (+level) + ' id="';
|
|
if (options.toc_id_prefix) out.s += options.toc_id_prefix;
|
|
out.s += 'toc_' + (options.tocData.headerCount++) + '">';
|
|
} else {
|
|
out.s += '<h' + (+level) + '>';
|
|
}
|
|
|
|
if (text) out.s += text.s;
|
|
out.s += '</h' + (+level) + '>\n';
|
|
}
|
|
|
|
// void (*hrule)(struct buf *ob, void *opaque);
|
|
function cb_hrule(out, options) {
|
|
if (out.s.length) out.s += '\n';
|
|
out.s += (options.flags & HTML_USE_XHTML) ? '<hr/>\n' : '<hr>\n';
|
|
}
|
|
|
|
// void (*list)(struct buf *ob, const struct buf *text, int flags, void *opaque);
|
|
function cb_list(out, text, flags, options) {
|
|
if (out.s.length) out.s += '\n';
|
|
out.s += (flags&MKD_LIST_ORDERED?'<ol>\n':'<ul>\n');
|
|
if (text) out.s += text.s;
|
|
out.s += (flags&MKD_LIST_ORDERED?'</ol>\n':'</ul>\n');
|
|
}
|
|
|
|
// void (*listitem)(struct buf *ob, const struct buf *text, int flags, void *opaque);
|
|
function cb_listitem(out, text, flags, options) {
|
|
out.s += '<li>';
|
|
if (text) {
|
|
var size = text.s.length;
|
|
while (size && text.s[size - 1] == '\n') size--;
|
|
out.s += text.s.slice(0, size);
|
|
}
|
|
out.s += '</li>\n';
|
|
}
|
|
|
|
// void (*paragraph)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_paragraph(out, text, options) {
|
|
var i = 0;
|
|
|
|
if (out.s.length) out.s += '\n';
|
|
|
|
if (!text || !text.s.length) return;
|
|
|
|
while (i < text.s.length && isspace(text.s[i])) i++;
|
|
|
|
if (i == text.s.length) return;
|
|
|
|
out.s += '<p>';
|
|
if (options.flags & HTML_HARD_WRAP) {
|
|
var org;
|
|
while (i < text.s.length) {
|
|
org = i;
|
|
while (i < text.s.length && text.data[i] != '\n')
|
|
i++;
|
|
|
|
if (i > org) out.s += text.s.slice(org, i);
|
|
|
|
/*
|
|
* do not insert a line break if this newline
|
|
* is the last character on the paragraph
|
|
*/
|
|
if (i >= text.s.length - 1) break;
|
|
|
|
cb_linebreak(out, options);
|
|
i++;
|
|
}
|
|
} else {
|
|
out.s += text.s.slice(i);
|
|
}
|
|
out.s += '</p>\n';
|
|
}
|
|
|
|
// void (*table)(struct buf *ob, const struct buf *header, const struct buf *body, void *opaque);
|
|
function cb_table(out, header, body, options) {
|
|
if (out.s.length) out.s += '\n';
|
|
out.s += '<table><thead>\n';
|
|
if (header) out.s += header.s;
|
|
out.s += '</thead><tbody>\n';
|
|
if (body) out.s += body.s;
|
|
out.s += '</tbody></table>\n';
|
|
}
|
|
|
|
// void (*table_row)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_table_row(out, text, options) {
|
|
out.s += '<tr>\n';
|
|
if (text) out.s += text.s;
|
|
out.s += '</tr>\n';
|
|
}
|
|
|
|
// void (*table_cell)(struct buf *ob, const struct buf *text, int flags, void *opaque);
|
|
function cb_table_cell(out, text, flags, options, col_span) {
|
|
if (flags & MKD_TABLE_HEADER) {
|
|
out.s += '<th';
|
|
} else {
|
|
out.s += '<td';
|
|
}
|
|
|
|
if (col_span > 1) {
|
|
out.s += " colspan=\"" + col_span + "\" ";
|
|
}
|
|
switch (flags & MKD_TABLE_ALIGNMASK) {
|
|
case MKD_TABLE_ALIGN_CENTER:
|
|
out.s += ' align="center">';
|
|
break;
|
|
|
|
case MKD_TABLE_ALIGN_L:
|
|
out.s += ' align="left">';
|
|
break;
|
|
|
|
case MKD_TABLE_ALIGN_R:
|
|
out.s += ' align="right">';
|
|
break;
|
|
default:
|
|
out.s += '>';
|
|
}
|
|
|
|
if (text) out.s += text.s;
|
|
|
|
if (flags & MKD_TABLE_HEADER) {
|
|
out.s += '</th>\n';
|
|
} else {
|
|
out.s += '</td>\n';
|
|
}
|
|
}
|
|
|
|
/* span level callbacks - NULL or return 0 prints the span verbatim */
|
|
// int (*autolink)(struct buf *ob, const struct buf *link, enum mkd_autolink type, void *opaque);
|
|
function cb_autolink(out, link, type, options) {
|
|
var offset = 0;
|
|
|
|
if (!link || !link.s.length) return 0;
|
|
|
|
if ((options.flags & HTML_SAFELINK) != 0 &&
|
|
!sd_autolink_issafe(link.s) && type != MKDA_EMAIL)
|
|
return 0;
|
|
|
|
out.s += '<a href="';
|
|
if (type == MKDA_EMAIL) out.s += 'mailto:';
|
|
escape_href(out, link.s.slice(offset));
|
|
|
|
if (options.link_attributes) {
|
|
out.s += '"';
|
|
options.link_attributes(out, link, options);
|
|
out.s += '>';
|
|
} else {
|
|
out.s += '">';
|
|
}
|
|
|
|
/*
|
|
* Pretty printing: if we get an email address as
|
|
* an actual URI, e.g. `mailto:foo@bar.com`, we don't
|
|
* want to print the `mailto:` prefix
|
|
*/
|
|
if (link.s.indexOf('mailto:')==0) {
|
|
escape_html(out, link.s.slice(7), false);
|
|
} else {
|
|
escape_html(out, link.s, false);
|
|
}
|
|
|
|
out.s += '</a>';
|
|
|
|
return 1;
|
|
}
|
|
|
|
// int (*codespan)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_codespan(out, text, options) {
|
|
out.s += '<code>';
|
|
if (text) escape_html(out, text.s, false);
|
|
out.s += '</code>';
|
|
return 1;
|
|
}
|
|
|
|
// int (*double_emphasis)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_double_emphasis(out, text, options) {
|
|
if (!text || !text.s.length) return 0;
|
|
out.s += '<strong>' + text.s + '</strong>';
|
|
return 1;
|
|
}
|
|
|
|
// int (*emphasis)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_emphasis(out, text, options) {
|
|
if (!text || !text.s.length) return 0;
|
|
out.s += '<em>' + text.s + '</em>';
|
|
return 1;
|
|
}
|
|
|
|
// int (*image)(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *alt, void *opaque);
|
|
function cb_image(out, link, title, alt, options) {
|
|
if (!link || !link.s.length) return 0;
|
|
|
|
out.s += '<img src="';
|
|
escape_href(out, link.s);
|
|
out.s += '" alt="';
|
|
|
|
if (alt && alt.s.length) escape_html(out, alt.s, false);
|
|
|
|
if (title && title.s.length) {
|
|
out.s += '" title="';
|
|
escape_html(out, title.s, false);
|
|
}
|
|
|
|
out.s += (options.flags&HTML_USE_XHTML?'"/>':'">');
|
|
return 1;
|
|
}
|
|
|
|
|
|
// int (*linebreak)(struct buf *ob, void *opaque);
|
|
function cb_linebreak(out, options) {
|
|
out.s += (options.flags&HTML_USE_XHTML?'<br/>\n':'<br>\n');
|
|
return 1;
|
|
}
|
|
|
|
// int (*link)(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *content, void *opaque);
|
|
function cb_link(out, link, title, content, options) {
|
|
if (link != null && (options.flags & HTML_SAFELINK) != 0 && !sd_autolink_issafe(link.s)) return 0;
|
|
|
|
out.s += '<a href="';
|
|
|
|
if (link && link.s.length) escape_href(out, link.s);
|
|
|
|
if (title && title.s.length) {
|
|
out.s += '" title="';
|
|
escape_html(out, title.s, false);
|
|
}
|
|
|
|
if (options.link_attributes) {
|
|
out.s += '"';
|
|
options.link_attributes(out, link, options);
|
|
out.s += '>';
|
|
} else {
|
|
out.s += '">';
|
|
}
|
|
|
|
if (content && content.s.length) out.s += content.s;
|
|
out.s += '</a>';
|
|
return 1;
|
|
}
|
|
|
|
// rndr_html_tag(struct buf *ob, const struct buf *text, void *opaque, char* tagname, char** whitelist, int tagtype)
|
|
//NOT A CALLBACK!
|
|
function rndr_html_tag(out, text, options, tagname, whitelist, tagtype) {
|
|
var i, x, z, in_str = 0, seen_equals = 0, done = 0, done_attr = 0, reset = 0;
|
|
var attr, value;
|
|
var c;
|
|
|
|
out.s += '<';
|
|
|
|
if(tagtype == HTML_TAG_CLOSE) {
|
|
out.s += "/" + tagname + ">";
|
|
return;
|
|
}
|
|
|
|
out.s += tagname;
|
|
var i = 1 + tagname.length;
|
|
|
|
attr = new Buffer;
|
|
value = new Buffer;
|
|
|
|
for(;i < text.s.length && !done; i++) {
|
|
c = text.s[i];
|
|
done = 0;
|
|
reset = 0;
|
|
done_attr = 0;
|
|
|
|
switch(c) {
|
|
case '>':
|
|
done = 1;
|
|
break;
|
|
case '\'':
|
|
case '"':
|
|
if (!seen_equals) {
|
|
reset = 1;
|
|
} else if(!in_str) {
|
|
in_str = c;
|
|
} else if(in_str == c) {
|
|
in_str = 0;
|
|
done_attr = 1;
|
|
} else {
|
|
value.s += c;
|
|
}
|
|
break;
|
|
case ' ':
|
|
if (in_str) {
|
|
value.s += ' ';
|
|
} else {
|
|
reset = 1;
|
|
}
|
|
break;
|
|
case '=':
|
|
if (seen_equals) {
|
|
reset = 1;
|
|
break;
|
|
}
|
|
seen_equals = 1;
|
|
break;
|
|
default:
|
|
if (seen_equals && in_str || !seen_equals) {
|
|
if (seen_equals) {
|
|
value.s += c;
|
|
} else {
|
|
attr.s += c;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (done_attr) {
|
|
var valid = 0;
|
|
for (z = 0; whitelist[z]; z++) {
|
|
if (whitelist[z].length != attr.s.length) {
|
|
continue;
|
|
}
|
|
for (x = 0; x < attr.s.length; x++) {
|
|
if (whitelist[z][x].toLowerCase() != attr.s[x].toLowerCase()) {
|
|
break;
|
|
}
|
|
}
|
|
if (x == attr.s.length) {
|
|
valid = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (valid && value.s.length && attr.s.length) {
|
|
// console.log("VALD", value.s.length, attr.s.length);
|
|
out.s += ' ';
|
|
escape_html(out, attr.s, false);
|
|
out.s += '="';
|
|
escape_html(out, value.s, false)
|
|
out.s += '"';
|
|
}
|
|
reset = 1;
|
|
}
|
|
if (reset) {
|
|
seen_equals = 0;
|
|
in_str = 0;
|
|
attr = new Buffer;
|
|
value = new Buffer;
|
|
}
|
|
}
|
|
out.s += '>';
|
|
}
|
|
|
|
// int (*raw_html_tag)(struct buf *ob, const struct buf *tag, void *opaque);
|
|
function cb_raw_html_tag(out, text, options) {
|
|
var whitelist = options.html_element_whitelist;
|
|
|
|
/* Items on the whitelist ignore all other flags and just output */
|
|
if (((options.flags & HTML_ALLOW_ELEMENT_WHITELIST) != 0) && whitelist) {
|
|
for(var i = 0; whitelist[i]; i++) {
|
|
var tagtype = sdhtml_is_tag(text.s, whitelist[i]);
|
|
if(tagtype != HTML_TAG_NONE) {
|
|
rndr_html_tag(out, text, options, whitelist[i], options.html_attr_whitelist, tagtype);
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* HTML_ESCAPE overrides SKIP_HTML, SKIP_STYLE, SKIP_LINKS and SKIP_IMAGES
|
|
* It doens't see if there are any valid tags, just escape all of them. */
|
|
if((options.flags & HTML_ESCAPE) != 0) {
|
|
escape_html(out, text.s, false);
|
|
return 1;
|
|
}
|
|
|
|
if ((options.flags & HTML_SKIP_HTML) != 0) return 1;
|
|
|
|
if ((options.flags & HTML_SKIP_STYLE) != 0 &&
|
|
sdhtml_is_tag(text.s, "style"))
|
|
return 1;
|
|
|
|
if ((options.flags & HTML_SKIP_LINKS) != 0 &&
|
|
sdhtml_is_tag(text.s, "a"))
|
|
return 1;
|
|
|
|
if ((options.flags & HTML_SKIP_IMAGES) != 0 &&
|
|
sdhtml_is_tag(text.s, "img"))
|
|
return 1;
|
|
|
|
out.s += text.s;
|
|
return 1;
|
|
}
|
|
|
|
// int (*triple_emphasis)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_triple_emphasis(out, text, options) {
|
|
if (!text || !text.s.length) return 0;
|
|
out.s += '<strong><em>' + text.s + '</em></strong>';
|
|
return 1;
|
|
}
|
|
|
|
// int (*strikethrough)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_strikethrough(out, text, options) {
|
|
if (!text || !text.s.length) return 0;
|
|
out.s += '<del>' + text.s + '</del>';
|
|
return 1;
|
|
}
|
|
|
|
// int (*superscript)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_superscript(out, text, options) {
|
|
if (!text || !text.s.length) return 0;
|
|
out.s += '<sup>' + text.s + '</sup>';
|
|
return 1;
|
|
}
|
|
|
|
/* low level callbacks - NULL copies input directly into the output */
|
|
//do not use
|
|
// void (*entity)(struct buf *ob, const struct buf *entity, void *opaque);
|
|
// function cb_entity(out, entity, options) {}
|
|
|
|
// void (*normal_text)(struct buf *ob, const struct buf *text, void *opaque);
|
|
function cb_normal_text(out, text, options) {
|
|
if (text) escape_html(out, text.s, false);
|
|
}
|
|
|
|
// toc_header(struct buf *ob, const struct buf *text, int level, void *opaque)
|
|
function cb_toc_header(out, text, level, options) {
|
|
/* set the level offset if this is the first header
|
|
* we're parsing for the document */
|
|
if (options.tocData.currentLevel== 0) {
|
|
out.s += '<div class="toc">\n';
|
|
options.tocData.levelOffset = level - 1;
|
|
}
|
|
level -= options.tocData.levelOffset;
|
|
|
|
if (level > options.tocData.currentLevel) {
|
|
while (level > options.tocData.currentLevel) {
|
|
out.s += '<ul>\n<li>\n';
|
|
options.tocData.currentLevel++;
|
|
}
|
|
} else if (level < options.tocData.currentLevel) {
|
|
out.s += '</li>\n';
|
|
while (level < options.tocData.currentLevel) {
|
|
out.s += '</ul>\n</li>\n';
|
|
options.tocData.currentLevel--;
|
|
}
|
|
out.s += '<li>\n';
|
|
} else {
|
|
out.s += '</li>\n<li>\n';
|
|
}
|
|
|
|
out.s += '<a href="#';
|
|
if (options.toc_id_prefix) out.s += options.toc_id_prefix;
|
|
out.s += 'toc_' + (options.tocData.headerCount++) + '">';
|
|
if (text) escape_html(out, text.s, false);
|
|
out.s += '</a>\n';
|
|
}
|
|
|
|
//toc_link(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *content, void *opaque)
|
|
function cb_toc_link(out, link, title, content, options) {
|
|
if (content && content.s)
|
|
out.s += content.s;
|
|
return 1;
|
|
}
|
|
|
|
|
|
// reset_toc(struct buf *ob, void *opaque)
|
|
function cb_reset_toc(out, options) {
|
|
options.tocData = {
|
|
headerCount: 0,
|
|
currentLevel: 0,
|
|
levelOffset: 0
|
|
};
|
|
}
|
|
|
|
//toc_finalize(struct buf *ob, void *opaque)
|
|
function cb_toc_finalize(out, options) {
|
|
var hasToc = false;
|
|
while (options.tocData.currentLevel > 0) {
|
|
out.s += '</li>\n</ul>\n';
|
|
options.tocData.currentLevel--;
|
|
hasToc = true;
|
|
}
|
|
if (hasToc) {
|
|
out.s += '</div>\n';
|
|
}
|
|
cb_reset_toc(out, options);
|
|
}
|
|
|
|
/* header and footer */
|
|
// doc_header(Buffer out}, context);
|
|
// doc_header: null,
|
|
// doc_footer(Buffer out, context);
|
|
// doc_footer: null
|
|
|
|
|
|
/* char_emphasis • single and double emphasis parsing */
|
|
//Buffer, md, str, int
|
|
function char_emphasis(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var size = data.length;
|
|
var c = data[0];
|
|
var ret;
|
|
|
|
if (size > 2 && data[1] != c) {
|
|
/* whitespace cannot follow an opening emphasis;
|
|
* strikethrough only takes two characters '~~' */
|
|
if (c == '~' || _isspace(data[1]) || (ret = parse_emph1(out, md, data, c)) == 0)
|
|
return 0;
|
|
|
|
return ret + 1;
|
|
}
|
|
|
|
if (data.length > 3 && data[1] == c && data[2] != c) {
|
|
if (_isspace(data[2]) || (ret = parse_emph2(out, md, data, c)) == 0)
|
|
return 0;
|
|
|
|
return ret + 2;
|
|
}
|
|
|
|
if (data.length > 4 && data[1] == c && data[2] == c && data[3] != c) {
|
|
if (c == '~' || _isspace(data[3]) || (ret = parse_emph3(out, md, data, c)) == 0)
|
|
return 0;
|
|
|
|
return ret + 3;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* char_codespan - '`' parsing a code span (assuming codespan != 0) */
|
|
function char_codespan(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var end, nb = 0, i, f_begin, f_end;
|
|
|
|
/* counting the number of backticks in the delimiter */
|
|
while (nb < data.length && data[nb] == '`')
|
|
nb++;
|
|
|
|
/* finding the next delimiter */
|
|
i = 0;
|
|
for (end = nb; end < data.length && i < nb; end++) {
|
|
if (data[end] == '`') i++;
|
|
else i = 0;
|
|
}
|
|
|
|
if (i < nb && end >= data.length)
|
|
return 0; /* no matching delimiter */
|
|
|
|
/* trimming outside whitespaces */
|
|
f_begin = nb;
|
|
while (f_begin < end && data[f_begin] == ' ') f_begin++;
|
|
|
|
f_end = end - nb;
|
|
while (f_end > nb && data[f_end-1] == ' ') f_end--;
|
|
|
|
/* real code span */
|
|
if (f_begin < f_end) {
|
|
var work = new Buffer(data.slice(f_begin, f_end));
|
|
if (!md.callbacks.codespan(out, work, md.context))
|
|
end = 0;
|
|
} else {
|
|
if (!md.callbacks.codespan(out, null, md.context))
|
|
end = 0;
|
|
}
|
|
|
|
return end;
|
|
}
|
|
|
|
/* char_linebreak - '\n' preceded by two spaces (assuming linebreak != 0) */
|
|
function char_linebreak(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
if (offset < 2 || data_[offset-1] != ' ' || data_[offset-2] != ' ')
|
|
return 0;
|
|
|
|
/* removing the last space from ob and rendering */
|
|
var len = out.s.length;
|
|
while (len && out.s[len - 1] == ' ') len--;
|
|
out.s = out.s.slice(0, len);
|
|
|
|
return md.callbacks.linebreak(out, md.context) ? 1 : 0;
|
|
}
|
|
|
|
/* char_link - '[': parsing a link or an image */
|
|
function char_link(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var is_img = (offset && data_[offset - 1] == '!'), level;
|
|
var i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0;
|
|
//4 bufs
|
|
var content = null;
|
|
var link = null;
|
|
var title = null;
|
|
var u_link = null;
|
|
var org_work_size = md.spanStack.length;
|
|
var text_has_nl = 0, ret = 0;
|
|
var in_title = 0, qtype = 0;
|
|
|
|
function cleanup() {
|
|
md.spanStack.length = org_work_size;
|
|
return ret ? i : 0;
|
|
}
|
|
|
|
/* checking whether the correct renderer exists */
|
|
if ((is_img && !md.callbacks.image) || (!is_img && !md.callbacks.link))
|
|
return cleanup();
|
|
/* looking for the matching closing bracket */
|
|
for (level = 1; i < data.length; i++) {
|
|
if (data[i] == '\n') text_has_nl = 1;
|
|
else if (data[i - 1] == '\\') continue;
|
|
else if (data[i] == '[') level++;
|
|
else if (data[i] == ']') {
|
|
level--;
|
|
if (level <= 0) break;
|
|
}
|
|
}
|
|
|
|
if (i >= data.length) return cleanup();
|
|
|
|
txt_e = i;
|
|
i++;
|
|
|
|
/* skip any amount of whitespace or newline */
|
|
/* (this is much more laxist than original markdown syntax) */
|
|
while (i < data.length && _isspace(data[i])) i++;
|
|
|
|
/* inline style link */
|
|
if (i < data.length && data[i] == '(') {
|
|
/* skipping initial whitespace */
|
|
i++;
|
|
|
|
while (i < data.length && _isspace(data[i])) i++;
|
|
|
|
link_b = i;
|
|
|
|
/* looking for link end: ' " ) */
|
|
while (i < data.length) {
|
|
if (data[i] == '\\') i += 2;
|
|
else if (data[i] == ')') break;
|
|
else if (i >= 1 && _isspace(data[i-1]) && (data[i] == '\'' || data[i] == '"')) break;
|
|
else i++;
|
|
}
|
|
|
|
if (i >= data.length) return cleanup();
|
|
link_e = i;
|
|
|
|
/* looking for title end if present */
|
|
if (data[i] == '\'' || data[i] == '"') {
|
|
qtype = data[i];
|
|
in_title = 1;
|
|
i++;
|
|
title_b = i;
|
|
|
|
while (i < data.length) {
|
|
if (data[i] == '\\') i += 2;
|
|
else if (data[i] == qtype) {in_title = 0; i++;}
|
|
else if ((data[i] == ')') && !in_title) break;
|
|
else i++;
|
|
}
|
|
|
|
if (i >= data.length) return cleanup();
|
|
|
|
/* skipping whitespaces after title */
|
|
title_e = i - 1;
|
|
while (title_e > title_b && _isspace(data[title_e])) title_e--;
|
|
|
|
/* checking for closing quote presence */
|
|
if (data[title_e] != '\'' && data[title_e] != '"') {
|
|
title_b = title_e = 0;
|
|
link_e = i;
|
|
}
|
|
}
|
|
|
|
/* remove whitespace at the end of the link */
|
|
while (link_e > link_b && _isspace(data[link_e - 1])) link_e--;
|
|
|
|
/* remove optional angle brackets around the link */
|
|
if (data[link_b] == '<') link_b++;
|
|
if (data[link_e - 1] == '>') link_e--;
|
|
|
|
/* building escaped link and title */
|
|
if (link_e > link_b) {
|
|
link = new Buffer();
|
|
md.spanStack.push(link);
|
|
link.s += data.slice(link_b, link_e);
|
|
}
|
|
|
|
if (title_e > title_b) {
|
|
title = new Buffer();
|
|
md.spanStack.push(title);
|
|
title.s += data.slice(title_b, title_e);
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
/* reference style link */
|
|
else if (i < data.length && data[i] == '[') {
|
|
var id = new Buffer();
|
|
var lr = null;
|
|
|
|
/* looking for the id */
|
|
i++;
|
|
link_b = i;
|
|
while (i < data.length && data[i] != ']') i++;
|
|
if (i >= data.length) return cleanup();
|
|
link_e = i;
|
|
|
|
/* finding the link_ref */
|
|
if (link_b == link_e) {
|
|
if (text_has_nl) {
|
|
var b = new Buffer();
|
|
md.spanStack.push(b);
|
|
var j;
|
|
|
|
for (j = 1; j < txt_e; j++) {
|
|
if (data[j] != '\n')
|
|
b.s += data[j];
|
|
else if (data[j - 1] != ' ')
|
|
b.s += ' ';
|
|
}
|
|
|
|
id.s = b.s;
|
|
} else {
|
|
id.s = data.slice(1);
|
|
}
|
|
} else {
|
|
id.s = data.slice(link_b, link_e);
|
|
}
|
|
|
|
//TODO
|
|
lr = md.refs[id.s];
|
|
if (!lr) return cleanup();
|
|
|
|
/* keeping link and title from link_ref */
|
|
link = lr.link;
|
|
title = lr.title;
|
|
i++;
|
|
}
|
|
|
|
/* shortcut reference style link */
|
|
else {
|
|
var id = new Buffer();
|
|
var lr = null;
|
|
|
|
/* crafting the id */
|
|
if (text_has_nl) {
|
|
var b = new Buffer();
|
|
md.spanStack.push(b);
|
|
|
|
var j;
|
|
for (j = 1; j < txt_e; j++) {
|
|
if (data[j] != '\n') b.s += data[j];
|
|
else if (data[j - 1] != ' ') b.s += ' ';
|
|
}
|
|
|
|
id.s = b.s;
|
|
} else {
|
|
id.s = data.slice(1, txt_e);
|
|
}
|
|
|
|
/* finding the link_ref */
|
|
lr = md.refs[id.s];
|
|
if (!lr) return cleanup();
|
|
|
|
/* keeping link and title from link_ref */
|
|
link = lr.link;
|
|
title = lr.title;
|
|
|
|
/* rewinding the whitespace */
|
|
i = txt_e + 1;
|
|
}
|
|
|
|
/* building content: img alt is escaped, link content is parsed */
|
|
if (txt_e > 1) {
|
|
content = new Buffer();
|
|
md.spanStack.push(content);
|
|
if (is_img) {
|
|
content.s += data.slice(1, txt_e);
|
|
} else {
|
|
/* disable autolinking when parsing inline the
|
|
* content of a link */
|
|
md.inLinkBody = 1;
|
|
parse_inline(content, md, data.slice(1, txt_e));
|
|
md.inLinkBody = 0;
|
|
}
|
|
}
|
|
|
|
if (link) {
|
|
u_link = new Buffer();
|
|
md.spanStack.push(u_link);
|
|
unscape_text(u_link, link);
|
|
} else {
|
|
return cleanup();
|
|
}
|
|
|
|
/* calling the relevant rendering function */
|
|
if (is_img) {
|
|
if (out.s.length && out.s[out.s.length - 1] == '!')
|
|
out.s = out.s.slice(0, -1);
|
|
|
|
ret = md.callbacks.image(out, u_link, title, content, md.context);
|
|
} else {
|
|
ret = md.callbacks.link(out, u_link, title, content, md.context);
|
|
}
|
|
|
|
/* cleanup */
|
|
// cleanup:
|
|
// rndr->work_bufs[BUFFER_SPAN].size = (int)org_work_size;
|
|
// return ret ? i : 0;
|
|
return cleanup();
|
|
}
|
|
|
|
|
|
/* char_langle_tag - '<' when tags or autolinks are allowed */
|
|
function char_langle_tag(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var altype = {p:MKDA_NOT_AUTOLINK};
|
|
var end = tag_length(data, altype);
|
|
var work = new Buffer(data.slice(0, end));
|
|
var ret = 0;
|
|
|
|
if (end > 2) {
|
|
if (md.callbacks.autolink && altype.p != MKDA_NOT_AUTOLINK) {
|
|
var u_link = new Buffer();
|
|
md.spanStack.push(u_link);
|
|
work.s = data.substr(1 , end - 2);
|
|
unscape_text(u_link, work);
|
|
ret = md.callbacks.autolink(out, u_link, altype.p, md.context);
|
|
md.spanStack.pop();
|
|
}
|
|
else if (md.callbacks.raw_html_tag)
|
|
ret = md.callbacks.raw_html_tag(out, work, md.context);
|
|
}
|
|
|
|
if (!ret) return 0;
|
|
else return end;
|
|
}
|
|
|
|
|
|
/* char_escape - '\\' backslash escape */
|
|
function char_escape(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var escape_chars = "\\`*_{}[]()#+-.!:|&<>/^~";
|
|
var work = new Buffer();
|
|
|
|
if (data.length > 1) {
|
|
if (escape_chars.indexOf(data[1]) == -1) return 0;
|
|
|
|
if (md.callbacks.normal_text) {
|
|
work.s = data[1];
|
|
md.callbacks.normal_text(out, work, md.context);
|
|
}
|
|
else out.s += data[1];
|
|
} else if (data.length == 1) {
|
|
out.s += data[0];
|
|
}
|
|
|
|
return 2;
|
|
}
|
|
|
|
|
|
|
|
/* char_entity - '&' escaped when it doesn't belong to an entity */
|
|
//-/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */
|
|
function char_entity(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var end = 1, content_start, content_end;
|
|
var numeric = false, hex = false, entity_base, entity_val;
|
|
var work = new Buffer();
|
|
|
|
if (end < data.length && data[end] === '#') {
|
|
numeric = true;
|
|
end++;
|
|
}
|
|
|
|
if (end < data.length && numeric && data[end].toLowerCase() === 'x') {
|
|
hex = true;
|
|
end++;
|
|
}
|
|
|
|
content_start = end;
|
|
while (end < data.length) {
|
|
var c = data[end];
|
|
if (hex) {
|
|
if (!isxdigit(c)) break;
|
|
} else if (numeric) {
|
|
if (!isdigit(c)) break;
|
|
} else if (!isalnum(c)) {
|
|
break;
|
|
}
|
|
end++;
|
|
}
|
|
|
|
|
|
if (end > content_start && end < data.length && data[end] === ';') {
|
|
/* well formed entity */
|
|
end++;
|
|
} else {
|
|
return 0; /* not an entity */
|
|
}
|
|
|
|
/* way too long to be a valid numeric entity */
|
|
if (numeric && content_end - content_start > MAX_NUM_ENTITY_LEN) {
|
|
return 0;
|
|
}
|
|
|
|
/* Validate the entity's contents */
|
|
if (numeric) {
|
|
if (hex) entity_base = 16;
|
|
else entity_base = 10;
|
|
entity_val = parseInt(data.slice(content_start), entity_base);
|
|
if (!is_valid_numeric_entity(entity_val)) return 0;
|
|
} else {
|
|
if (VALID_ENTITIES.indexOf(data.slice(0, end)) === -1) return 0;
|
|
}
|
|
|
|
if (md.callbacks.entity) {
|
|
work.s = data.slice(0, end);
|
|
md.callbacks.entity(out, work, md.context);
|
|
}
|
|
else out.s += data.slice(0, end);
|
|
|
|
return end;
|
|
}
|
|
|
|
function char_autolink_url(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var link = null;
|
|
var link_len, rewind = {p: null};
|
|
|
|
if (!md.callbacks.autolink || md.inLinkBody) return 0;
|
|
|
|
link = new Buffer();
|
|
md.spanStack.push(link);
|
|
|
|
if ((link_len = sd_autolink__url(rewind, link, data_, offset, data.length, 0)) > 0) {
|
|
if (rewind.p > 0) out.truncate(out.s.length - rewind.p);
|
|
md.callbacks.autolink(out, link, MKDA_NORMAL, md.context);
|
|
}
|
|
|
|
md.spanStack.pop();
|
|
return link_len;
|
|
}
|
|
|
|
|
|
function char_autolink_email(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var link = null;
|
|
var link_len, rewind = {p: null};
|
|
|
|
if (!md.callbacks.autolink || md.inLinkBody) return 0;
|
|
|
|
link = new Buffer();
|
|
md.spanStack.push(link);
|
|
|
|
if ((link_len = sd_autolink__email(rewind, link, data_, offset, data.length, 0)) > 0) {
|
|
if (rewind.p > 0) out.truncate(out.s.length - rewind.p);
|
|
md.callbacks.autolink(out, link, MKDA_EMAIL, md.context);
|
|
}
|
|
|
|
md.spanStack.pop();
|
|
return link_len;
|
|
}
|
|
|
|
|
|
function char_autolink_www(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var link = null, link_url = null, link_text = null;
|
|
var link_len, rewind = {p: null};
|
|
|
|
if (!md.callbacks.link || md.inLinkBody) return 0;
|
|
|
|
link = new Buffer();
|
|
md.spanStack.push(link);
|
|
|
|
if ((link_len = sd_autolink__www(rewind, link, data_, offset, data.length, 0)) > 0) {
|
|
link_url = new Buffer();
|
|
md.spanStack.push(link_url);
|
|
link_url.s += 'http://';
|
|
link_url.s += link.s;
|
|
|
|
if (rewind.p > 0) out.truncate(out.s.length - rewind.p);
|
|
|
|
if (md.callbacks.normal_text) {
|
|
link_text = new Buffer();
|
|
md.spanStack.push(link_text);
|
|
md.callbacks.normal_text(link_text, link, md.context);
|
|
md.callbacks.link(out, link_url, null, link_text, md.context);
|
|
md.spanStack.pop();
|
|
} else {
|
|
md.callbacks.link(out, link_url, null, link, md.context);
|
|
}
|
|
md.spanStack.pop();
|
|
}
|
|
|
|
md.spanStack.pop();
|
|
return link_len;
|
|
}
|
|
|
|
function char_autolink_subreddit_or_username(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var link = null;
|
|
var link_len, rewind = {p: null};
|
|
|
|
if (!md.callbacks.autolink || md.inLinkBody) return 0;
|
|
|
|
link = new Buffer();
|
|
md.spanStack.push(link);
|
|
if ((link_len = sd_autolink__subreddit(rewind, link, data_, offset, data.length)) > 0) {
|
|
//don't slice because the rewind pointer will always be 0
|
|
if (rewind.p > 0) out.truncate(out.s.length - rewind.p);
|
|
md.callbacks.autolink(out, link, MKDA_NORMAL, md.context);
|
|
} else if ((link_len = sd_autolink__username(rewind, link, data_, offset, data.length)) > 0) {
|
|
//don't slice because the rewind pointer will always be 0
|
|
if (rewind.p > 0) out.truncate(out.s.length - rewind.p);
|
|
md.callbacks.autolink(out, link, MKDA_NORMAL, md.context);
|
|
}
|
|
md.spanStack.pop();
|
|
|
|
return link_len;
|
|
}
|
|
|
|
function char_superscript(out, md, data_, offset) {
|
|
var data = data_.slice(offset);
|
|
var size = data.length;
|
|
var sup_start, sup_len;
|
|
var sup = null;
|
|
|
|
if (!md.callbacks.superscript) return 0;
|
|
|
|
if (size < 2) return 0;
|
|
|
|
if (data[1] == '(') {
|
|
sup_start = sup_len = 2;
|
|
|
|
while (sup_len < size && data[sup_len] != ')' && data[sup_len - 1] != '\\') sup_len++;
|
|
|
|
if (sup_len == size) return 0;
|
|
} else {
|
|
sup_start = sup_len = 1;
|
|
|
|
while (sup_len < size && !_isspace(data[sup_len])) sup_len++;
|
|
}
|
|
|
|
if (sup_len - sup_start == 0) return (sup_start == 2) ? 3 : 0;
|
|
|
|
sup = new Buffer();
|
|
md.spanStack.push(sup);
|
|
parse_inline(sup, md, data.slice(sup_start, sup_len));
|
|
md.callbacks.superscript(out, sup, md.context);
|
|
md.spanStack.pop();
|
|
|
|
return (sup_start == 2) ? sup_len + 1 : sup_len;
|
|
}
|
|
|
|
|
|
var markdown_char_ptrs = [
|
|
null,
|
|
char_emphasis,
|
|
char_codespan,
|
|
char_linebreak,
|
|
char_link,
|
|
char_langle_tag,
|
|
char_escape,
|
|
char_entity,
|
|
char_autolink_url,
|
|
char_autolink_email,
|
|
char_autolink_www,
|
|
char_autolink_subreddit_or_username,
|
|
char_superscript
|
|
];
|
|
|
|
var MKD_LIST_ORDERED = 1;
|
|
var MKD_LI_BLOCK = 2; /* <li> containing block data */
|
|
var MKD_LI_END = 8; /* internal list flag */
|
|
|
|
var enumCounter = 0;
|
|
var MD_CHAR_NONE = enumCounter++;
|
|
var MD_CHAR_EMPHASIS = enumCounter++;
|
|
var MD_CHAR_CODESPAN = enumCounter++;
|
|
var MD_CHAR_LINEBREAK = enumCounter++;
|
|
var MD_CHAR_LINK = enumCounter++;
|
|
var MD_CHAR_LANGLE = enumCounter++;
|
|
var MD_CHAR_ESCAPE = enumCounter++;
|
|
var MD_CHAR_ENTITITY = enumCounter++;
|
|
var MD_CHAR_AUTOLINK_URL = enumCounter++;
|
|
var MD_CHAR_AUTOLINK_EMAIL = enumCounter++;
|
|
var MD_CHAR_AUTOLINK_WWW = enumCounter++;
|
|
var MD_CHAR_AUTOLINK_SUBREDDIT_OR_USERNAME = enumCounter++;
|
|
var MD_CHAR_SUPERSCRIPT = enumCounter++;
|
|
|
|
var SD_AUTOLINK_SHORT_DOMAINS = (1 << 0);
|
|
|
|
enumCounter = 0;
|
|
var MKDA_NOT_AUTOLINK = enumCounter++; /* used internally when it is not an autolink*/
|
|
var MKDA_NORMAL = enumCounter++; /* normal http/http/ftp/mailto/etc link */
|
|
var MKDA_EMAIL = enumCounter++; /* e-mail link without explit mailto: */
|
|
|
|
var MKDEXT_NO_INTRA_EMPHASIS = (1 << 0);
|
|
var MKDEXT_TABLES = (1 << 1);
|
|
var MKDEXT_FENCED_CODE = (1 << 2);
|
|
var MKDEXT_AUTOLINK = (1 << 3);
|
|
var MKDEXT_STRIKETHROUGH = (1 << 4);
|
|
// var MKDEXT_LAX_HTML_BLOCKS = (1 << 5);
|
|
var MKDEXT_SPACE_HEADERS = (1 << 6);
|
|
var MKDEXT_SUPERSCRIPT = (1 << 7);
|
|
var MKDEXT_LAX_SPACING = (1 << 8)
|
|
var MKDEXT_NO_EMAIL_AUTOLINK = (1 << 9)
|
|
|
|
var HTML_SKIP_HTML = (1 << 0);
|
|
var HTML_SKIP_STYLE = (1 << 1);
|
|
var HTML_SKIP_IMAGES = (1 << 2);
|
|
var HTML_SKIP_LINKS = (1 << 3);
|
|
var HTML_EXPAND_TABS = (1 << 4);
|
|
var HTML_SAFELINK = (1 << 5);
|
|
var HTML_TOC = (1 << 6);
|
|
var HTML_HARD_WRAP = (1 << 7);
|
|
var HTML_USE_XHTML = (1 << 8);
|
|
var HTML_ESCAPE = (1 << 9);
|
|
var HTML_ALLOW_ELEMENT_WHITELIST = (1 << 10);
|
|
|
|
var MKD_TABLE_ALIGN_L = 1;
|
|
var MKD_TABLE_ALIGN_R = 2;
|
|
var MKD_TABLE_ALIGN_CENTER = 3;
|
|
var MKD_TABLE_ALIGNMASK = 3;
|
|
var MKD_TABLE_HEADER = 4
|
|
|
|
var HTML_TAG_NONE = 0;
|
|
var HTML_TAG_OPEN = 1;
|
|
var HTML_TAG_CLOSE = 2;
|
|
|
|
|
|
/**
|
|
* A string buffer wrapper because JavaScript doesn't have mutable strings.
|
|
* @constructor
|
|
* @param {string=} str Optional string to initialize the Buffer with.
|
|
*/
|
|
function Buffer(str) {
|
|
this.s = str || "";
|
|
};
|
|
|
|
/**
|
|
*/
|
|
Buffer.prototype.truncate = function(size) {
|
|
if (this.s.length < size) throw new RangeError("Buffer smaller than desired size");
|
|
if (size < 0) throw new RangeError("Size argument is negative");
|
|
this.s = this.s.slice(0, size);
|
|
}
|
|
|
|
|
|
/**
|
|
* A Markdown parser object.
|
|
* @constructor
|
|
*/
|
|
function Markdown() {
|
|
|
|
//Becase javascript strings are immutable they must be wrapped with Buffer()
|
|
this.spanStack = [];
|
|
this.blockStack = [];
|
|
this.extensions = MKDEXT_NO_INTRA_EMPHASIS | MKDEXT_SUPERSCRIPT | MKDEXT_AUTOLINK | MKDEXT_STRIKETHROUGH | MKDEXT_TABLES | MKDEXT_TABLES;
|
|
var renderer = getRedditRenderer();
|
|
this.context = renderer.context;
|
|
this.callbacks = renderer.callbacks;
|
|
this.inLinkBody = 0;
|
|
this.activeChars = {};
|
|
this.refs = {};
|
|
this.nestingLimit = 16;
|
|
this.maxTableCols = 64;
|
|
};
|
|
|
|
|
|
/* is_empty - returns the line length when it is empty, 0 otherwise */
|
|
function is_empty(data) {
|
|
var i;
|
|
for (i = 0; i < data.length && data[i] != '\n'; i++)
|
|
if (data[i] != ' ') return 0;
|
|
|
|
return i + 1;
|
|
}
|
|
|
|
|
|
/* is_hrule - returns whether a line is a horizontal rule */
|
|
function is_hrule(data) {
|
|
var i = 0, n = 0;
|
|
var c;
|
|
|
|
/* skipping initial spaces */
|
|
if (data.length < 3) return 0;
|
|
if (data[0] == ' ') { i++;
|
|
if (data[1] == ' ') { i++;
|
|
if (data[2] == ' ') { i++; } } }
|
|
|
|
/* looking at the hrule uint8_t */
|
|
if (i + 2 >= data.length
|
|
|| (data[i] != '*' && data[i] != '-' && data[i] != '_'))
|
|
return 0;
|
|
c = data[i];
|
|
|
|
/* the whole line must be the char or whitespace */
|
|
while (i < data.length && data[i] != '\n') {
|
|
if (data[i] == c) n++;
|
|
else if (data[i] != ' ')
|
|
return 0;
|
|
|
|
i++;
|
|
}
|
|
|
|
return n >= 3;
|
|
}
|
|
|
|
|
|
/* check if a line begins with a code fence; return the
|
|
* width if it is */
|
|
function prefix_codefence(data) {
|
|
var i = 0, n = 0;
|
|
var c;
|
|
|
|
/* skipping initial spaces */
|
|
if (data.length < 3) return 0;
|
|
if (data[0] == ' ') { i++;
|
|
if (data[1] == ' ') { i++;
|
|
if (data[2] == ' ') { i++; } } }
|
|
|
|
/* looking at the hrule uint8_t */
|
|
if (i + 2 >= data.length || !(data[i] == '~' || data[i] == '`'))
|
|
return 0;
|
|
|
|
c = data[i];
|
|
|
|
/* the whole line must be the uint8_t or whitespace */
|
|
while (i < data.length && data[i] == c) {
|
|
n++; i++;
|
|
}
|
|
|
|
if (n < 3) return 0;
|
|
|
|
return i;
|
|
}
|
|
|
|
/* check if a line is a code fence; return its size if it */
|
|
function is_codefence(data, syntax) {
|
|
var i = 0, syn_len = 0;
|
|
i = prefix_codefence(data);
|
|
if (i == 0) return 0;
|
|
|
|
|
|
while (i < data.length && data[i] == ' ') i++;
|
|
|
|
var syn_start;
|
|
//syn_start = data + i;
|
|
syn_start = i;
|
|
|
|
if (i < data.length && data[i] == '{') {
|
|
i++; syn_start++;
|
|
|
|
while (i < data.length && data[i] != '}' && data[i] != '\n') {
|
|
syn_len++; i++;
|
|
}
|
|
|
|
if (i == data.length || data[i] != '}')
|
|
return 0;
|
|
|
|
/* strip all whitespace at the beginning and the end
|
|
* of the {} block */
|
|
/*remember not to remove the +0, it helps me keep syncronised with snudown*/
|
|
while (syn_len > 0 && _isspace(data[syn_start+0])) {
|
|
syn_start++; syn_len--;
|
|
}
|
|
|
|
// while (syn_len > 0 && _isspace(syn_start[syn_len - 1]))
|
|
while (syn_len > 0 && _isspace(data[syn_start+syn_len - 1]))
|
|
syn_len--;
|
|
|
|
i++;
|
|
} else {
|
|
while (i < data.length && !_isspace(data[i])) {
|
|
syn_len++; i++;
|
|
}
|
|
}
|
|
|
|
if (syntax) syntax.s = data.substr(syn_start, syn_len);
|
|
// syntax->size = syn;
|
|
|
|
while (i < data.length && data[i] != '\n') {
|
|
if (!_isspace(data[i])) return 0;
|
|
|
|
i++;
|
|
}
|
|
|
|
return i + 1;
|
|
}
|
|
|
|
/* find_emph_char - looks for the next emph uint8_t, skipping other constructs */
|
|
function find_emph_char(data, c) {
|
|
var i = 1;
|
|
while (i < data.length) {
|
|
while (i < data.length && data[i] != c && data[i] != '`' && data[i] != '[')
|
|
i++;
|
|
|
|
if (i == data.length) return 0;
|
|
|
|
if (data[i] == c)
|
|
return i;
|
|
|
|
/* not counting escaped chars */
|
|
if (i && data[i - 1] == '\\') {
|
|
i++; continue;
|
|
}
|
|
|
|
if (data[i] == '`') {
|
|
var span_nb = 0, bt;
|
|
var tmp_i = 0;
|
|
|
|
/* counting the number of opening backticks */
|
|
while (i < data.length && data[i] == '`') {
|
|
i++; span_nb++;
|
|
}
|
|
|
|
if (i >= data.length) return 0;
|
|
|
|
/* finding the matching closing sequence */
|
|
bt = 0;
|
|
while (i < data.length && bt < span_nb) {
|
|
if (!tmp_i && data[i] == c) tmp_i = i;
|
|
if (data[i] == '`') bt++;
|
|
else bt = 0;
|
|
i++;
|
|
}
|
|
|
|
if (i >= data.length) return tmp_i;
|
|
}
|
|
/* skipping a link */
|
|
else if (data[i] == '[') {
|
|
var tmp_i = 0;
|
|
var cc;
|
|
|
|
i++;
|
|
while (i < data.length && data[i] != ']') {
|
|
if (!tmp_i && data[i] == c) tmp_i = i;
|
|
i++;
|
|
}
|
|
|
|
i++;
|
|
while (i < data.length && (data[i] == ' ' || data[i] == '\n'))
|
|
i++;
|
|
|
|
if (i >= data.length) return tmp_i;
|
|
|
|
switch (data[i]) {
|
|
case '[':
|
|
cc = ']'; break;
|
|
|
|
case '(':
|
|
cc = ')'; break;
|
|
|
|
default:
|
|
if (tmp_i)
|
|
return tmp_i;
|
|
else
|
|
continue;
|
|
}
|
|
|
|
i++;
|
|
while (i < data.length && data[i] != cc) {
|
|
if (!tmp_i && data[i] == c) tmp_i = i;
|
|
i++;
|
|
}
|
|
|
|
if (i >= data.length) return tmp_i;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* parse_emph1 - parsing single emphase */
|
|
/* closed by a symbol not preceded by whitespace and not followed by symbol */
|
|
function parse_emph1(out, md, data_, c) {
|
|
var data = data_.slice(1);
|
|
var i = 0, len;
|
|
var r;
|
|
|
|
if (!md.callbacks.emphasis) return 0;
|
|
|
|
/* skipping one symbol if coming from emph3 */
|
|
if (data.length > 1 && data[0] == c && data[1] == c) i = 1;
|
|
|
|
while (i < data.length) {
|
|
len = find_emph_char(data.slice(i), c);
|
|
if (!len) return 0;
|
|
i += len;
|
|
if (i >= data.length) return 0;
|
|
|
|
if (data[i] == c && !_isspace(data[i - 1])) {
|
|
if ((md.extensions & MKDEXT_NO_INTRA_EMPHASIS) && (c == '_')) {
|
|
if (!(i + 1 == data.length || _isspace(data[i + 1]) || ispunct(data[i + 1])))
|
|
continue;
|
|
}
|
|
|
|
var work = new Buffer();
|
|
md.spanStack.push(work);
|
|
parse_inline(work, md, data.slice(0, i));
|
|
r = md.callbacks.emphasis(out, work, md.context);
|
|
md.spanStack.pop();
|
|
return r ? i + 1 : 0;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* parse_emph2 - parsing single emphase */
|
|
function parse_emph2(out, md, data_, c) {
|
|
var data = data_.slice(2);
|
|
var i = 0, len;
|
|
var r;
|
|
|
|
var render_method = (c == '~') ? md.callbacks.strikethrough : md.callbacks.double_emphasis;
|
|
|
|
if (!render_method) return 0;
|
|
|
|
while (i < data.length) {
|
|
len = find_emph_char(data.slice(i), c);
|
|
if (!len) return 0;
|
|
i += len;
|
|
|
|
if (i + 1 < data.length && data[i] == c && data[i + 1] == c && i && !_isspace(data[i - 1])) {
|
|
var work = new Buffer();
|
|
md.spanStack.push(work);
|
|
parse_inline(work, md, data.slice(0, i));
|
|
r = render_method(out, work, md.context);
|
|
md.spanStack.pop();
|
|
return r ? i + 2 : 0;
|
|
}
|
|
i++;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* parse_emph3 • parsing single emphase */
|
|
/* finds the first closing tag, and delegates to the other emph */
|
|
function parse_emph3(out, md, data_, c) {
|
|
var data = data_.slice(3);
|
|
var i = 0, len;
|
|
var r;
|
|
|
|
while (i < data.length) {
|
|
len = find_emph_char(data.slice(i), c);
|
|
if (!len) return 0;
|
|
i += len;
|
|
|
|
/* skip whitespace preceded symbols */
|
|
if (data[i] != c || _isspace(data[i - 1])) continue;
|
|
|
|
if (i + 2 < data.length && data[i + 1] == c && data[i + 2] == c && md.callbacks.triple_emphasis) {
|
|
/* triple symbol found */
|
|
var work = new Buffer();
|
|
md.spanStack.push(work);
|
|
parse_inline(work, md, data.slice(0, i));
|
|
r = md.callbacks.triple_emphasis(out, work, md.context);
|
|
md.spanStack.pop();
|
|
return r ? i + 3 : 0;
|
|
|
|
} else if (i + 1 < data.length && data[i + 1] == c) {
|
|
/* double symbol found, handing over to emph1 */
|
|
len = parse_emph1(out, md, data_, c);
|
|
if (!len) return 0;
|
|
else return len - 2;
|
|
|
|
} else {
|
|
/* single symbol found, handing over to emph2 */
|
|
len = parse_emph2(out, md, data_, c);
|
|
if (!len) return 0;
|
|
else return len - 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function is_atxheader(md, data) {
|
|
if (data[0] != '#') return false;
|
|
|
|
if (md.extensions & MKDEXT_SPACE_HEADERS) {
|
|
var level = 0;
|
|
|
|
while (level < data.length && level < 6 && data[level] == '#')
|
|
level++;
|
|
|
|
if (level < data.length && data[level] != ' ')
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/* is_headerline . returns whether the line is a setext-style hdr underline */
|
|
function is_headerline(data) {
|
|
var i = 0;
|
|
var size = data.length;
|
|
|
|
/* test of level 1 header */
|
|
if (data[i] == '=') {
|
|
for (i = 1; i < size && data[i] == '='; i++) {}
|
|
while (i < size && data[i] == ' ') i++;
|
|
return (i >= size || data[i] == '\n') ? 1 : 0; }
|
|
|
|
/* test of level 2 header */
|
|
if (data[i] == '-') {
|
|
for (i = 1; i < size && data[i] == '-'; i++) {}
|
|
while (i < size && data[i] == ' ') i++;
|
|
return (i >= size || data[i] == '\n') ? 2 : 0; }
|
|
|
|
return 0;
|
|
}
|
|
|
|
function is_next_headerline(data) {
|
|
var size = data.length;
|
|
var i = 0;
|
|
|
|
while (i < size && data[i] != '\n') i++;
|
|
|
|
if (++i >= size) return 0;
|
|
|
|
return is_headerline(data.slice(i));
|
|
}
|
|
|
|
/* prefix_quote - returns blockquote prefix length */
|
|
function prefix_quote(data) {
|
|
var i = 0;
|
|
var size = data.length;
|
|
if (i < size && data[i] == ' ') i++;
|
|
if (i < size && data[i] == ' ') i++;
|
|
if (i < size && data[i] == ' ') i++;
|
|
|
|
if (i < size && data[i] == '>') {
|
|
if (i + 1 < size && data[i + 1] == ' ')
|
|
return i + 2;
|
|
|
|
return i + 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* prefix_code • returns prefix length for block code*/
|
|
function prefix_code(data) {
|
|
if (data.length > 3 && data[0] == ' ' && data[1] == ' '
|
|
&& data[2] == ' ' && data[3] == ' ') return 4;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* prefix_oli - returns ordered list item prefix */
|
|
function prefix_oli(data) {
|
|
var size = data.length;
|
|
var i = 0;
|
|
|
|
if (i < size && data[i] == ' ') i++;
|
|
if (i < size && data[i] == ' ') i++;
|
|
if (i < size && data[i] == ' ') i++;
|
|
|
|
if (i >= size || data[i] < '0' || data[i] > '9') return 0;
|
|
|
|
while (i < size && data[i] >= '0' && data[i] <= '9') i++;
|
|
|
|
if (i + 1 >= size || data[i] != '.' || data[i + 1] != ' ') return 0;
|
|
|
|
if (is_next_headerline(data.slice(i))) return 0;
|
|
|
|
return i + 2;
|
|
}
|
|
|
|
/* prefix_uli - returns ordered list item prefix */
|
|
function prefix_uli(data) {
|
|
var size = data.length;
|
|
var i = 0;
|
|
|
|
if (i < size && data[i] == ' ') i++;
|
|
if (i < size && data[i] == ' ') i++;
|
|
if (i < size && data[i] == ' ') i++;
|
|
|
|
if (i + 1 >= size ||
|
|
(data[i] != '*' && data[i] != '+' && data[i] != '-') ||
|
|
data[i + 1] != ' ')
|
|
return 0;
|
|
|
|
if (is_next_headerline(data.slice(i))) return 0;
|
|
|
|
return i + 2;
|
|
}
|
|
|
|
/* is_mail_autolink - looks for the address part of a mail autolink and '>' */
|
|
/* this is less strict than the original markdown e-mail address matching */
|
|
function is_mail_autolink(data) {
|
|
var i = 0, nb = 0;
|
|
|
|
/* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */
|
|
for (i = 0; i < data.length; ++i) {
|
|
if (isalnum(data[i]))
|
|
continue;
|
|
|
|
switch (data[i]) {
|
|
case '@':
|
|
nb++;
|
|
|
|
case '-':
|
|
case '.':
|
|
case '_':
|
|
break;
|
|
|
|
case '>':
|
|
return (nb == 1) ? i + 1 : 0;
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* tag_length - returns the length of the given tag, or 0 is it's not valid */
|
|
function tag_length(data, autolink) {
|
|
var i, j;
|
|
|
|
/* a valid tag can't be shorter than 3 chars */
|
|
if (data.length < 3) return 0;
|
|
|
|
/* begins with a '<' optionally followed by '/', followed by letter or number */
|
|
if (data[0] != '<') return 0;
|
|
i = (data[1] == '/') ? 2 : 1;
|
|
|
|
if (!isalnum(data[i])) return 0;
|
|
|
|
/* scheme test */
|
|
autolink.p = MKDA_NOT_AUTOLINK;
|
|
|
|
/* try to find the beginning of an URI */
|
|
while (i < data.length && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-')) i++;
|
|
|
|
if (i > 1 && data[i] == '@') {
|
|
if ((j = is_mail_autolink(data.slice(i))) != 0) {
|
|
autolink.p = MKDA_EMAIL;
|
|
return i + j;
|
|
}
|
|
}
|
|
|
|
if (i > 2 && data[i] == ':') {
|
|
autolink.p = MKDA_NORMAL;
|
|
i++;
|
|
}
|
|
|
|
/* completing autolink test: no whitespace or ' or " */
|
|
if (i >= data.length) autolink.p = MKDA_NOT_AUTOLINK;
|
|
else if (autolink.p) {
|
|
j = i;
|
|
|
|
while (i < data.length) {
|
|
if (data[i] == '\\') i += 2;
|
|
else if (data[i] == '>' || data[i] == '\'' ||
|
|
data[i] == '"' || data[i] == ' ' || data[i] == '\n')
|
|
break;
|
|
else i++;
|
|
}
|
|
|
|
if (i >= data.length) return 0;
|
|
if (i > j && data[i] == '>') return i + 1;
|
|
/* one of the forbidden chars has been found */
|
|
autolink.p = MKDA_NOT_AUTOLINK;
|
|
}
|
|
|
|
/* looking for sometinhg looking like a tag end */
|
|
while (i < data.length && data[i] != '>') i++;
|
|
if (i >= data.length) return 0;
|
|
return i + 1;
|
|
}
|
|
|
|
// parse_inline - parses inline markdown elements
|
|
//Buffer, md, String
|
|
function parse_inline(out, md, data) {
|
|
var i = 0, end = 0;
|
|
var action = 0;
|
|
var work = new Buffer();
|
|
|
|
if (md.spanStack.length + md.blockStack.length > md.nestingLimit)
|
|
return;
|
|
|
|
while (i < data.length) {
|
|
/* copying inactive chars into the output */
|
|
while (end < data.length && !(action = md.activeChars[data[end]])) {
|
|
end++;
|
|
}
|
|
|
|
if (md.callbacks.normal_text) {
|
|
work.s = data.slice(i, end);
|
|
md.callbacks.normal_text(out, work, md.context);
|
|
}
|
|
else
|
|
out.s += data.slice(i, end);
|
|
|
|
if (end >= data.length) break;
|
|
i = end;
|
|
|
|
end = markdown_char_ptrs[action](out, md, data, i);
|
|
if (!end) /* no action from the callback */
|
|
end = i + 1;
|
|
else {
|
|
i += end;
|
|
end = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* parse_atxheader - parsing of atx-style headers */
|
|
function parse_atxheader(out, md, data) {
|
|
var level = 0;
|
|
var i, end, skip;
|
|
|
|
while (level < data.length && level < 6 && data[level] == '#') level++;
|
|
|
|
for (i = level; i < data.length && data[i] == ' '; i++) {}
|
|
|
|
for (end = i; end < data.length && data[end] != '\n'; end++) {}
|
|
skip = end;
|
|
|
|
while (end && data[end - 1] == '#') end--;
|
|
|
|
while (end && data[end - 1] == ' ') end--;
|
|
|
|
if (end > i) {
|
|
var work = new Buffer();
|
|
md.spanStack.push(work);
|
|
|
|
parse_inline(work, md, data.slice(i, end));
|
|
|
|
if (md.callbacks.header)
|
|
md.callbacks.header(out, work, level, md.context);
|
|
|
|
md.spanStack.pop();
|
|
}
|
|
|
|
return skip;
|
|
}
|
|
|
|
/* htmlblock_end - checking end of HTML block : </tag>[ \t]*\n[ \t*]\n */
|
|
/* returns the length on match, 0 otherwise */
|
|
// htmlblock_end(const char *tag, size_t tag_len, struct sd_markdown *rndr, uint8_t *data, size_t size)
|
|
function htmlblock_end(tag, md, data) {
|
|
var i, w;
|
|
|
|
/* checking if tag is a match */
|
|
//tag should already be lowercase
|
|
if (tag.length + 3 >= data.length ||
|
|
data.slice(2).toLowerCase() != tag ||
|
|
data[tag.length + 2] != '>')
|
|
return 0;
|
|
|
|
/* checking white lines */
|
|
i = tag.length + 3;
|
|
w = 0;
|
|
if (i < data.length && (w = is_empty(data.slice(i))) == 0)
|
|
return 0; /* non-blank after tag */
|
|
i += w;
|
|
w = 0;
|
|
|
|
if (i < data.length) w = is_empty(data.slice(i));
|
|
|
|
return i + w;
|
|
}
|
|
|
|
/* parse_htmlblock - parsing of inline HTML block */
|
|
//TODO
|
|
function parse_htmlblock(out, md, data, do_render) {
|
|
var i, j = 0;
|
|
var curtag = null;
|
|
var found;
|
|
var work = new Buffer(data);
|
|
|
|
/* identification of the opening tag */
|
|
if (data.length < 2 || data[0] != '<') return 0;
|
|
|
|
i = 1;
|
|
while (i < data.length && data[i] != '>' && data[i] != ' ') i++;
|
|
|
|
if (i < data.length) curtag = find_block_tag(data.slice(1));
|
|
|
|
/* handling of special cases */
|
|
if (!curtag) {
|
|
|
|
/* HTML comment, laxist form */
|
|
if (data.length > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') {
|
|
i = 5;
|
|
|
|
while (i < data.length && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>')) i++;
|
|
|
|
i++;
|
|
|
|
if (i < size)
|
|
j = is_empty(data.slice(i));
|
|
|
|
if (j) {
|
|
//TODO: HANDLE WORK!!!
|
|
// work.size = i + j;
|
|
work.s = data.slice(0, i + j);
|
|
if (do_render && md.callbacks.blockhtml)
|
|
md.callbacks.blockhtml(out, work, md.context);
|
|
return work.s.length;
|
|
}
|
|
}
|
|
|
|
/* HR, which is the only self-closing block tag considered */
|
|
if (data.length > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R')) {
|
|
i = 3;
|
|
while (i < data.length && data[i] != '>') i++;
|
|
|
|
if (i + 1 < data.length) {
|
|
i++;
|
|
j = is_empty(data.slice(i));
|
|
if (j) {
|
|
work.s = data.slice(0, i + j);
|
|
if (do_render && md.callbacks.blockhtml)
|
|
md.callbacks.blockhtml(out, work, md.context);
|
|
return work.s.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* no special case recognised */
|
|
return 0;
|
|
}
|
|
|
|
/* looking for an unindented matching closing tag */
|
|
/* followed by a blank line */
|
|
i = 1;
|
|
found = 0;
|
|
|
|
/* if not found, trying a second pass looking for indented match */
|
|
/* but not if tag is "ins" or "del" (following original Markdown.pl) */
|
|
if (curtag != 'ins' && curtag != 'del') {
|
|
var tag_size = curtag.length;
|
|
i = 1;
|
|
while (i < data.length) {
|
|
i++;
|
|
while (i < data.length && !(data[i - 1] == '<' && data[i] == '/'))
|
|
i++;
|
|
|
|
if (i + 2 + tag_size >= data.length)
|
|
break;
|
|
|
|
// j = htmlblock_end(tag, md, data + i - 1, size - i + 1);
|
|
//TODO
|
|
j = htmlblock_end(tag, md, data.slice(i - 1));
|
|
|
|
if (j) {
|
|
i += j - 1;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found) return 0;
|
|
|
|
/* the end of the block has been found */
|
|
//TODO:
|
|
work.s = work.s.slice(0, i);
|
|
if (do_render && md.callbacks.blockhtml)
|
|
md.callbacks.blockhtml(out, work, md.context);
|
|
|
|
return i;
|
|
}
|
|
|
|
/* parse_blockquote - handles parsing of a blockquote fragment */
|
|
function parse_blockquote(out, md, data) {
|
|
var size = data.length;
|
|
var beg, end = 0, pre, work_size = 0;
|
|
// uint8_t *work_data = 0;
|
|
var work_data = "";
|
|
var work_data_cursor = 0;
|
|
|
|
var out_ = new Buffer();
|
|
md.blockStack.push(out_);
|
|
|
|
beg = 0;
|
|
while (beg < size) {
|
|
for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {}
|
|
|
|
pre = prefix_quote(data.slice(beg, end));
|
|
|
|
if (pre) beg += pre; /* skipping prefix */
|
|
|
|
/* empty line followed by non-quote line */
|
|
else if (is_empty(data.slice(beg, end)) &&
|
|
(end >= size || (prefix_quote(data.slice(end)) == 0 &&
|
|
!is_empty(data.slice(end)))))
|
|
break;
|
|
|
|
if (beg < end) { /* copy into the in-place working buffer */
|
|
/* bufput(work, data + beg, end - beg); */
|
|
//TODO:!!! FIX THIS!!!
|
|
// if (!work_data) work_data = data.slice(beg, end);
|
|
// work_data = data + beg;
|
|
work_data += data.slice(beg, end);
|
|
/*
|
|
if (!work_data) work_data_cursor = beg;
|
|
else if (beg != work_data_cursor + work_size)
|
|
work_data += data.slice(beg, end);
|
|
*/
|
|
// memmove(work_data + work_size, data + beg, end - beg);
|
|
work_size += end - beg;
|
|
}
|
|
beg = end;
|
|
}
|
|
|
|
parse_block(out_, md, work_data);
|
|
if (md.callbacks.blockquote)
|
|
md.callbacks.blockquote(out, out_, md.context);
|
|
md.blockStack.pop();
|
|
return end;
|
|
}
|
|
|
|
/* parse_paragraph - handles parsing of a regular paragraph */
|
|
function parse_paragraph(out, md, data) {
|
|
var i = 0, end = 0;
|
|
var level = 0;
|
|
var size = data.length;
|
|
var work = new Buffer(data);
|
|
|
|
while (i < size) {
|
|
for (end = i + 1; end < size && data[end - 1] != '\n'; end++) {/* empty */}
|
|
|
|
if (prefix_quote(data.slice(i, end)) != 0) {
|
|
end = i;
|
|
break;
|
|
}
|
|
|
|
var tempdata = data.slice(i);
|
|
if (is_empty(tempdata) || (level = is_headerline(tempdata)) != 0) break;
|
|
if (is_empty(tempdata)) break;
|
|
if ((level = is_headerline(tempdata)) != 0) break;
|
|
|
|
if (is_atxheader(md, tempdata)
|
|
|| is_hrule(tempdata)
|
|
|| prefix_quote(tempdata)) {
|
|
end = i;
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Early termination of a paragraph with the same logic
|
|
* as markdown 1.0.0. If this logic is applied, the
|
|
* Markdown 1.0.3 test suite wont pass cleanly.
|
|
*
|
|
* :: If the first character in a new line is not a letter
|
|
* lets check to see if there's some kind of block starting here
|
|
*/
|
|
if ((md.extensions & MKDEXT_LAX_SPACING) && !isalnum(data[i])) {
|
|
if (prefix_oli(tempdata)
|
|
|| prefix_uli(tempdata)) {
|
|
end = i;
|
|
break;
|
|
}
|
|
/* see if an html block starts here */
|
|
if (data[i] == '<' && md.callbacks.blockhtml
|
|
&& parse_htmlblock(out, md, tempdata, 0)) {
|
|
end = i;
|
|
break
|
|
}
|
|
|
|
/* see if a code fence starts here */
|
|
if ((md.extensions && MKDEXT_FENCED_CODE) != 0
|
|
&& is_codefence(tempdata, null) != 0) {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
i = end;
|
|
}
|
|
|
|
var work_size = i;
|
|
while (work_size && data[work_size - 1] == '\n') work_size--;
|
|
work.s = work.s.slice(0, work_size);
|
|
|
|
if (!level) {
|
|
var tmp = new Buffer();
|
|
md.blockStack.push(tmp);
|
|
parse_inline(tmp, md, work.s);
|
|
if (md.callbacks.paragraph)
|
|
md.callbacks.paragraph(out, tmp, md.context);
|
|
md.blockStack.pop();
|
|
} else {
|
|
var header_work = null;
|
|
|
|
if (work.size) {
|
|
var beg;
|
|
i = work.s.length;
|
|
// var work_size = work.s.length - 1;
|
|
// work.size -= 1;
|
|
|
|
while (work_size && data[work_size] != '\n')
|
|
work_size -= 1;
|
|
|
|
beg = work_size + 1;
|
|
while (work_size && data[work_size - 1] == '\n')
|
|
work_size -= 1;
|
|
|
|
work.s = work.s.slice(0, work_size);
|
|
if (work_size > 0) {
|
|
var tmp = new Buffer();
|
|
md.blockStack.push(tmp);
|
|
parse_inline(tmp, md, work.s);
|
|
|
|
if (md.callbacks.paragraph)
|
|
md.callbacks.paragraph(out, tmp, md.context);
|
|
|
|
md.blockStack.pop();
|
|
work.s = work.s.slice(beg, i);
|
|
}
|
|
else work.s = work.s.slice(0, i);
|
|
}
|
|
|
|
header_work = new Buffer();
|
|
md.spanStack.push(header_work);
|
|
parse_inline(header_work, md, work.s);
|
|
|
|
if (md.callbacks.header)
|
|
md.callbacks.header(out, header_work, level, md.context);
|
|
|
|
md.spanStack.pop();
|
|
}
|
|
|
|
return end;
|
|
}
|
|
|
|
/* parse_fencedcode - handles parsing of a block-level code fragment */
|
|
function parse_fencedcode(out, md, data) {
|
|
var beg, end;
|
|
var work = null;
|
|
var lang = new Buffer();
|
|
|
|
beg = is_codefence(data, lang);
|
|
if (beg == 0) return 0;
|
|
|
|
work = new Buffer();
|
|
md.blockStack.push(work);
|
|
|
|
while (beg < data.length) {
|
|
var fence_end;
|
|
var fence_trail = new Buffer();
|
|
|
|
fence_end = is_codefence(data.slice(beg), fence_trail);
|
|
if (fence_end != 0 && fence_trail.s.length == 0) {
|
|
beg += fence_end;
|
|
break;
|
|
}
|
|
|
|
for (end = beg + 1; end < data.length && data[end - 1] != '\n'; end++) {}
|
|
|
|
if (beg < end) {
|
|
/* verbatim copy to the working buffer,
|
|
escaping entities */
|
|
var tempData = data.slice(beg, end);
|
|
if (is_empty(tempData)) work.s += '\n';
|
|
else work.s += tempData;
|
|
}
|
|
beg = end;
|
|
}
|
|
|
|
if (work.s.length && work.s[work.s.length - 1] != '\n')
|
|
work.s += '\n';
|
|
|
|
if (md.callbacks.blockcode)
|
|
md.callbacks.blockcode(out, work, lang.s.length ? lang : null, md.context);
|
|
|
|
md.blockStack.pop();
|
|
return beg;
|
|
}
|
|
|
|
function parse_blockcode(out, md, data) {
|
|
var size = data.length;
|
|
var beg, end, pre;
|
|
|
|
var work = null;
|
|
md.blockStack.push(work = new Buffer());
|
|
|
|
beg = 0;
|
|
while (beg < size) {
|
|
for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {};
|
|
pre = prefix_code(data.slice(beg, end));
|
|
|
|
if (pre) beg += pre; /* skipping prefix */
|
|
else if (!is_empty(data.slice(beg, end)))
|
|
/* non-empty non-prefixed line breaks the pre */
|
|
break;
|
|
|
|
if (beg < end) {
|
|
/* verbatim copy to the working buffer,
|
|
escaping entities */
|
|
if (is_empty(data.slice(beg, end))) work.s += '\n';
|
|
else work.s += data.slice(beg, end);
|
|
}
|
|
beg = end;
|
|
}
|
|
|
|
var work_size = work.s.length;
|
|
while (work_size && work.s[work_size - 1] == '\n') work_size -= 1;
|
|
work.s = work.s.slice(0, work_size);
|
|
|
|
work.s += '\n';
|
|
|
|
if (md.callbacks.blockcode)
|
|
md.callbacks.blockcode(out, work, null, md.context);
|
|
|
|
md.blockStack.pop();
|
|
return beg;
|
|
}
|
|
|
|
/* parse_listitem - parsing of a single list item */
|
|
/* assuming initial prefix is already removed */
|
|
//FLAGS is pointer
|
|
function parse_listitem(out, md, data, flags) {
|
|
var size = data.length;
|
|
var work = null, inter = null;
|
|
var beg = 0, end, pre, sublist = 0, orgpre = 0, i;
|
|
var in_empty = 0, has_inside_empty = 0, in_fence = 0;
|
|
|
|
/* keeping track of the first indentation prefix */
|
|
while (orgpre < 3 && orgpre < size && data[orgpre] == ' ')
|
|
orgpre++;
|
|
|
|
//TODO
|
|
beg = prefix_uli(data);
|
|
if (!beg) beg = prefix_oli(data);
|
|
|
|
if (!beg) return 0;
|
|
|
|
/* skipping to the beginning of the following line */
|
|
end = beg;
|
|
while (end < size && data[end - 1] != '\n') end++;
|
|
|
|
/* getting working buffers */
|
|
md.spanStack.push(work = new Buffer());
|
|
md.spanStack.push(inter = new Buffer());
|
|
|
|
/* putting the first line into the working buffer */
|
|
work.s += data.slice(beg, end);
|
|
beg = end;
|
|
|
|
/* process the following lines */
|
|
while (beg < size) {
|
|
var has_next_uli, has_next_oli;
|
|
end++;
|
|
|
|
while (end < size && data[end - 1] != '\n') end++;
|
|
|
|
/* process an empty line */
|
|
if (is_empty(data.slice(beg, end))) {
|
|
in_empty = 1;
|
|
beg = end;
|
|
continue;
|
|
}
|
|
|
|
/* calculating the indentation */
|
|
i = 0;
|
|
while (i < 4 && beg + i < end && data[beg + i] == ' ') i++;
|
|
|
|
pre = i;
|
|
|
|
//TODO: Cache this slice?
|
|
if (md.flags & MKDEXT_FENCED_CODE) {
|
|
if (is_codefence(data.slice(beg+i, end), null) != 0) {
|
|
in_fence = !in_fence;
|
|
}
|
|
}
|
|
|
|
/* only check for new list items if we are **not** in a fenced code block */
|
|
if (!in_fence) {
|
|
has_next_uli = prefix_uli(data.slice(beg+i, end));
|
|
has_next_oli = prefix_oli(data.slice(beg+i, end));
|
|
}
|
|
|
|
/* checking for ul/ol switch */
|
|
if (in_empty && (
|
|
((flags.p & MKD_LIST_ORDERED) && has_next_uli) ||
|
|
(!(flags.p & MKD_LIST_ORDERED) && has_next_oli))){
|
|
flags.p |= MKD_LI_END;
|
|
break; /* the following item must have same list type */
|
|
}
|
|
|
|
/* checking for a new item */
|
|
if ((has_next_uli && !is_hrule(data.slice(beg+i, end))) || has_next_oli) {
|
|
if (in_empty) has_inside_empty = 1;
|
|
|
|
if (pre == orgpre) /* the following item must have */
|
|
break; /* the same indentation */
|
|
|
|
if (!sublist) sublist = work.s.length;
|
|
}
|
|
/* joining only indented stuff after empty lines;
|
|
* note that now we only require 1 space of indentation
|
|
* to continue list */
|
|
else if (in_empty && pre == 0) {
|
|
flags.p |= MKD_LI_END;
|
|
break;
|
|
}
|
|
else if (in_empty) {
|
|
work.s += '\n';
|
|
has_inside_empty = 1;
|
|
}
|
|
|
|
in_empty = 0;
|
|
|
|
/* adding the line without prefix into the working buffer */
|
|
work.s += data.slice(beg + i, end);
|
|
beg = end;
|
|
}
|
|
|
|
/* render of li contents */
|
|
if (has_inside_empty) flags.p |= MKD_LI_BLOCK;
|
|
|
|
if (flags.p & MKD_LI_BLOCK) {
|
|
/* intermediate render of block li */
|
|
if (sublist && sublist < work.s.length) {
|
|
parse_block(inter, md, work.s.slice(0, sublist));
|
|
parse_block(inter, md, work.s.slice(sublist));
|
|
}
|
|
else
|
|
parse_block(inter, md, work.s);
|
|
} else {
|
|
//TODO:
|
|
/* intermediate render of inline li */
|
|
if (sublist && sublist < work.s.length) {
|
|
parse_inline(inter, md, work.s.slice(0, sublist));
|
|
parse_block(inter, md, work.s.slice(sublist));
|
|
}
|
|
else
|
|
parse_inline(inter, md, work.s);
|
|
}
|
|
|
|
/* render of li itself */
|
|
if (md.callbacks.listitem)
|
|
md.callbacks.listitem(out, inter, flags.p, md.context);
|
|
|
|
md.spanStack.pop();
|
|
md.spanStack.pop();
|
|
return beg;
|
|
}
|
|
|
|
|
|
/* parse_list - parsing ordered or unordered list block */
|
|
function parse_list(out, md, data, flags) {
|
|
var size = data.length;
|
|
var i = 0, j;
|
|
|
|
var work = null;
|
|
md.blockStack.push(work = new Buffer());
|
|
|
|
while (i < size) {
|
|
var flag_p = {p: flags};
|
|
j = parse_listitem(work, md, data.slice(i), flag_p);
|
|
flags = flag_p.p;
|
|
i += j;
|
|
|
|
if (!j || (flags & MKD_LI_END)) break;
|
|
}
|
|
|
|
if (md.callbacks.list)
|
|
md.callbacks.list(out, work, flags, md.context);
|
|
md.blockStack.pop();
|
|
return i;
|
|
}
|
|
|
|
function parse_table_row(out, md, data, columns, header_flag) {
|
|
var i = 0, col, cols_left;
|
|
var row_work = null;
|
|
|
|
if (!md.callbacks.table_cell || !md.callbacks.table_row) return;
|
|
|
|
md.spanStack.push(row_work = new Buffer());
|
|
|
|
if (i < data.length && data[i] == '|') i++;
|
|
|
|
for (col = 0; col < columns.length && i < data.length; ++col) {
|
|
var cell_start, cell_end;
|
|
var cell_work;
|
|
|
|
md.spanStack.push(cell_work = new Buffer());
|
|
|
|
while (i < data.length && _isspace(data[i])) i++;
|
|
|
|
cell_start = i;
|
|
|
|
while (i < data.length && data[i] != '|') i++;
|
|
|
|
cell_end = i - 1;
|
|
|
|
while (cell_end > cell_start && _isspace(data[cell_end])) cell_end--;
|
|
|
|
// parse_inline(cell_work, rndr, data + cell_start, 1 + cell_end - cell_start);
|
|
parse_inline(cell_work, md, data.slice(cell_start, 1 + cell_end));
|
|
md.callbacks.table_cell(row_work, cell_work, columns[col] | header_flag, md.context, 0);
|
|
|
|
md.spanStack.pop();
|
|
i++;
|
|
}
|
|
|
|
cols_left = columns.length - col;
|
|
// for (; col < columns.length; ++col) {
|
|
if (cols_left > 0) {
|
|
var empty_cell = null;
|
|
md.callbacks.table_cell(row_work, empty_cell, columns[col] | header_flag, md.context, cols_left);
|
|
}
|
|
|
|
md.callbacks.table_row(out, row_work, md.context);
|
|
|
|
md.spanStack.pop();
|
|
}
|
|
|
|
function parse_table_header(out, md, data, columns) {
|
|
var i = 0, col, header_end, under_end;
|
|
|
|
var pipes = 0;
|
|
while (i < data.length && data[i] != '\n')
|
|
if (data[i++] == '|') pipes++;
|
|
|
|
if (i == data.length || pipes == 0)
|
|
return 0;
|
|
|
|
header_end = i;
|
|
|
|
while (header_end > 0 && _isspace(data[header_end - 1])) header_end--;
|
|
|
|
if (data[0] == '|') pipes--;
|
|
|
|
if (header_end && data[header_end - 1] == '|') pipes--;
|
|
|
|
if (pipes + 1 > md.maxTableCols) return 0;
|
|
// columns.p = pipes + 1;
|
|
// column_data.p = new Array(columns.p);
|
|
columns.p = new Array(pipes + 1);
|
|
for (var k = 0; k < columns.p.length; k++) columns.p[k] = 0;
|
|
|
|
/* Parse the header underline */
|
|
i++;
|
|
if (i < data.length && data[i] == '|') i++;
|
|
|
|
under_end = i;
|
|
while (under_end < data.length && data[under_end] != '\n') under_end++;
|
|
|
|
for (col = 0; col < columns.p.length && i < under_end; ++col) {
|
|
var dashes = 0;
|
|
|
|
while (i < under_end && data[i] == ' ') i++;
|
|
|
|
if (data[i] == ':') {
|
|
i++;
|
|
columns.p[col] |= MKD_TABLE_ALIGN_L;
|
|
dashes++;
|
|
}
|
|
|
|
while (i < under_end && data[i] == '-') {
|
|
i++; dashes++;
|
|
}
|
|
|
|
if (i < under_end && data[i] == ':') {
|
|
i++; columns.p[col] |= MKD_TABLE_ALIGN_R;
|
|
dashes++;
|
|
}
|
|
|
|
while (i < under_end && data[i] == ' ') i++;
|
|
|
|
if (i < under_end && data[i] != '|') break;
|
|
|
|
if (dashes < 1) break;
|
|
|
|
i++;
|
|
}
|
|
|
|
if (col < columns.p.length) return 0;
|
|
|
|
parse_table_row(out, md, data.slice(0, header_end), columns.p, MKD_TABLE_HEADER);
|
|
|
|
return under_end + 1;
|
|
}
|
|
|
|
function parse_table(out, md, data) {
|
|
var i;
|
|
var header_work, body_work;
|
|
|
|
var columns = {p: null};
|
|
|
|
md.spanStack.push(header_work = new Buffer());
|
|
md.blockStack.push(body_work = new Buffer());
|
|
|
|
i = parse_table_header(header_work, md, data, columns);
|
|
if (i > 0) {
|
|
|
|
while (i < data.length) {
|
|
var row_start;
|
|
var pipes = 0;
|
|
|
|
row_start = i;
|
|
|
|
while (i < data.length && data[i] != '\n')
|
|
if (data[i++] == '|')
|
|
pipes++;
|
|
|
|
if (pipes == 0 || i == data.length) {
|
|
i = row_start;
|
|
break;
|
|
}
|
|
|
|
parse_table_row(body_work, md, data.slice(row_start, i), columns.p, 0);
|
|
|
|
i++;
|
|
}
|
|
|
|
if (md.callbacks.table)
|
|
md.callbacks.table(out, header_work, body_work, md.context);
|
|
}
|
|
|
|
md.spanStack.pop();
|
|
md.blockStack.pop();
|
|
return i;
|
|
}
|
|
|
|
function parse_block(out, md, data) {
|
|
var beg = 0, end, i;
|
|
var textData;
|
|
|
|
if (md.spanStack.length +
|
|
md.blockStack.length > md.nestingLimit)
|
|
return;
|
|
|
|
while (beg < data.length) {
|
|
textData = data.slice(beg);
|
|
end = data.length - beg;
|
|
|
|
if (is_atxheader(md, textData))
|
|
beg += parse_atxheader(out, md, textData);
|
|
|
|
else if (data[beg] == '<' && md.callbacks.blockhtml &&
|
|
(i = parse_htmlblock(out, md, textData, 1)) != 0)
|
|
beg += i;
|
|
else if ((i = is_empty(textData)) != 0)
|
|
beg += i;
|
|
else if (is_hrule(textData)) {
|
|
if (md.callbacks.hrule)
|
|
md.callbacks.hrule(out, md.context);
|
|
|
|
while (beg < data.length && data[beg] != '\n') beg++;
|
|
|
|
beg++;
|
|
}
|
|
|
|
else if ((md.extensions & MKDEXT_FENCED_CODE) != 0 &&
|
|
(i = parse_fencedcode(out, md, textData)) != 0)
|
|
beg += i;
|
|
|
|
else if ((md.extensions & MKDEXT_TABLES) != 0 &&
|
|
(i = parse_table(out, md, textData)) != 0)
|
|
beg += i;
|
|
|
|
else if (prefix_quote(textData))
|
|
beg += parse_blockquote(out, md, textData);
|
|
|
|
else if (prefix_code(textData))
|
|
beg += parse_blockcode(out, md, textData);
|
|
|
|
else if (prefix_uli(textData))
|
|
beg += parse_list(out, md, textData, 0);
|
|
|
|
else if (prefix_oli(textData))
|
|
beg += parse_list(out, md, textData, MKD_LIST_ORDERED);
|
|
|
|
else {
|
|
beg += parse_paragraph(out, md, textData);
|
|
}
|
|
}
|
|
}
|
|
|
|
function is_ref(data, beg, end, md) {
|
|
/* int n; */
|
|
var i = 0;
|
|
var idOffset, idEnd;
|
|
var linkOffset, linkEnd;
|
|
var titleOffset, titleEnd;
|
|
var lineEnd;
|
|
|
|
/* up to 3 optional leading spaces */
|
|
if (beg + 3 >= end) return 0;
|
|
if (data[beg] == ' ') { i = 1;
|
|
if (data[beg + 1] == ' ') { i = 2;
|
|
if (data[beg + 2] == ' ') { i = 3;
|
|
if (data[beg + 3] == ' ') return 0; } } }
|
|
i += beg;
|
|
|
|
/* id part: anything but a newline between brackets */
|
|
if (data[i] != '[') return 0;
|
|
i++;
|
|
idOffset = i;
|
|
while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') i++;
|
|
if (i >= end || data[i] != ']') return 0;
|
|
idEnd = i;
|
|
|
|
/* spacer: colon (space | tab)* newline? (space | tab)* */
|
|
i++;
|
|
if (i >= end || data[i] != ':') return 0;
|
|
i++;
|
|
while (i < end && data[i] == ' ') i++;
|
|
if (i < end && (data[i] == '\n' || data[i] == '\r')) {
|
|
i++;
|
|
if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; }
|
|
while (i < end && data[i] == ' ') i++;
|
|
if (i >= end) return 0;
|
|
|
|
/* link: whitespace-free sequence, optionally between angle brackets */
|
|
if (data[i] == '<') i++;
|
|
|
|
linkOffset = i;
|
|
while (i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r') i++;
|
|
|
|
if (data[i - 1] == '>') linkEnd = i - 1; else linkEnd = i;
|
|
|
|
/* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */
|
|
while (i < end && data[i] == ' ') i++;
|
|
if (i < end && data[i] != '\n' && data[i] != '\r'
|
|
&& data[i] != '\'' && data[i] != '"' && data[i] != '(')
|
|
return 0;
|
|
lineEnd = 0;
|
|
/* computing end-of-line */
|
|
if (i >= end || data[i] == '\r' || data[i] == '\n') lineEnd = i;
|
|
if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
|
|
lineEnd = i + 1;
|
|
|
|
/* optional (space|tab)* spacer after a newline */
|
|
if (lineEnd) {
|
|
i = lineEnd + 1;
|
|
while (i < end && data[i] == ' ') i++;
|
|
}
|
|
|
|
/* optional title: any non-newline sequence enclosed in '"()
|
|
alone on its line */
|
|
titleOffset = titleEnd = 0;
|
|
if (i + 1 < end && (data[i] == '\'' || data[i] == '"' || data[i] == '(')) {
|
|
i++;
|
|
titleOffset = i;
|
|
/* looking for EOL */
|
|
while (i < end && data[i] != '\n' && data[i] != '\r') i++;
|
|
if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
|
|
titleEnd = i + 1;
|
|
else titleEnd = i;
|
|
/* stepping back */
|
|
i -= 1;
|
|
while (i > titleOffset && data[i] == ' ')
|
|
i -= 1;
|
|
if (i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')')) {
|
|
lineEnd = titleEnd;
|
|
titleEnd = i;
|
|
}
|
|
}
|
|
|
|
if (!lineEnd || linkEnd == linkOffset)
|
|
return 0; /* garbage after the link empty link */
|
|
|
|
var id = data.slice(idOffset, idEnd);
|
|
var link = data.slice(linkOffset, linkEnd);
|
|
var title = null;
|
|
if (titleEnd > titleOffset) title = data.slice(titleOffset, titleEnd);
|
|
md.refs[id] = {
|
|
id: id,
|
|
link: new Buffer(link),
|
|
title: new Buffer(title)
|
|
};
|
|
return lineEnd;
|
|
}
|
|
|
|
function expand_tabs(out, line) {
|
|
var i = 0, tab = 0;
|
|
|
|
while (i < line.length) {
|
|
var org = i;
|
|
|
|
while (i < line.length && line[i] != '\t') {
|
|
i++; tab++;
|
|
}
|
|
|
|
if (i > org) out.s += line.slice(org, i);
|
|
|
|
if (i >= line.length) break;
|
|
|
|
do {
|
|
out.s += ' ';
|
|
tab++;
|
|
} while (tab % 4);
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
Render markdown code to HTML.
|
|
|
|
@param {string} source Markdown code.
|
|
@returns {string} HTML code.
|
|
*/
|
|
function render(source) {
|
|
var text = new Buffer();
|
|
var beg = 0, end;
|
|
this.refs = {};
|
|
|
|
while (beg < source.length) { /* iterating over lines */
|
|
if (end = is_ref(source, beg, source.length, this))
|
|
beg = end;
|
|
else { /* skipping to the next line */
|
|
end = beg;
|
|
while (end < source.length && source[end] != '\n' && source[end] != '\r') end++;
|
|
|
|
/* adding the line body if present */
|
|
if (end > beg) expand_tabs(text, source.slice(beg, end));
|
|
|
|
while (end < source.length && (source[end] == '\n' || source[end] == '\r')) {
|
|
/* add one \n per newline */
|
|
if (source[end] == '\n' || (end + 1 < source.length && source[end + 1] != '\n'))
|
|
text.s += '\n';
|
|
end++;
|
|
}
|
|
|
|
beg = end;
|
|
}
|
|
}
|
|
|
|
var out = new Buffer();
|
|
|
|
/* second pass: actual rendering */
|
|
if (this.callbacks.doc_header)
|
|
this.callbacks.doc_header(out, this.context);
|
|
|
|
if (text.s.length) {
|
|
/* adding a final newline if not already present */
|
|
if (text.s[text.s.length - 1] != '\n' && text.s[text.s.length - 1] != '\r')
|
|
text.s += '\n';
|
|
parse_block(out, this, text.s);
|
|
}
|
|
|
|
if (this.callbacks.doc_footer)
|
|
this.callbacks.doc_footer(out, this.context);
|
|
|
|
return out.s;
|
|
}
|
|
Markdown.prototype['render'] = render;
|
|
|
|
/**
|
|
Create a parser object using the given configuration parameters.
|
|
|
|
To get a Reddit equivelent configuration, pass no arguments.
|
|
|
|
@param {?Renderer=} renderer A renderer object.
|
|
@param {?Number=} extensions A series of OR'd extension flags. (Extension flags start with MKDEXT_)
|
|
@param {?Number=} nestingLimit The maximum depth to which inline elements can be nested.
|
|
@return {Markdown} A configured markdown object.
|
|
*/
|
|
exports.getParser = function getParser(renderer, extensions, nestingLimit, columnLimit) {
|
|
var md = new Markdown();
|
|
if (renderer) md.callbacks = renderer.callbacks;
|
|
if (nestingLimit) md.nestingLimit = nestingLimit;
|
|
if (nestingLimit) md.maxTableCols = columnLimit;
|
|
if (renderer) md.context = renderer.context;
|
|
if (extensions != undefined && extensions != null) md.extensions = extensions;
|
|
|
|
// if (!(nestingLimit > 0 && columnLimit > 0 && callbacks)) throw new Error;
|
|
var cb = md.callbacks;
|
|
if (cb.emphasis || cb.double_emphasis || cb.triple_emphasis) {
|
|
md.activeChars['*'] = MD_CHAR_EMPHASIS;
|
|
md.activeChars['_'] = MD_CHAR_EMPHASIS;
|
|
if (md.extensions & MKDEXT_STRIKETHROUGH) md.activeChars['~'] = MD_CHAR_EMPHASIS;
|
|
}
|
|
|
|
if (cb.codespan) md.activeChars['`'] = MD_CHAR_CODESPAN;
|
|
if (cb.linebreak) md.activeChars['\n'] = MD_CHAR_LINEBREAK;
|
|
if (cb.image || cb.link) md.activeChars['['] = MD_CHAR_LINK;
|
|
|
|
md.activeChars['<'] = MD_CHAR_LANGLE;
|
|
md.activeChars['\\'] = MD_CHAR_ESCAPE;
|
|
md.activeChars['&'] = MD_CHAR_ENTITITY;
|
|
|
|
if (md.extensions & MKDEXT_AUTOLINK) {
|
|
if (!(md.extensions & MKDEXT_NO_EMAIL_AUTOLINK)) {
|
|
md.activeChars['@'] = MD_CHAR_AUTOLINK_EMAIL;
|
|
}
|
|
md.activeChars[':'] = MD_CHAR_AUTOLINK_URL;
|
|
md.activeChars['w'] = MD_CHAR_AUTOLINK_WWW;
|
|
md.activeChars['/'] = MD_CHAR_AUTOLINK_SUBREDDIT_OR_USERNAME;
|
|
}
|
|
if (md.extensions & MKDEXT_SUPERSCRIPT) md.activeChars['^'] = MD_CHAR_SUPERSCRIPT;
|
|
|
|
return md;
|
|
}
|
|
|
|
var DEFAULT_BODY_FLAGS = HTML_SKIP_HTML | HTML_SKIP_IMAGES | HTML_SAFELINK | HTML_ESCAPE | HTML_USE_XHTML;
|
|
var DEFAULT_WIKI_FLAGS = HTML_SKIP_HTML | HTML_SAFELINK | HTML_ALLOW_ELEMENT_WHITELIST | HTML_ESCAPE | HTML_USE_XHTML;
|
|
var DEFAULT_HTML_ATTR_WHITELIST = ['colspan', 'rowspan', 'cellspacing', 'cellpadding', 'scope'];
|
|
var DEFAULT_HTML_ELEMENT_WHITELIST = ['tr', 'th', 'td', 'table', 'tbody', 'thead', 'tfoot', 'caption'];
|
|
exports.DEFAULT_HTML_ELEMENT_WHITELIST = DEFAULT_HTML_ELEMENT_WHITELIST;
|
|
exports.DEFAULT_HTML_ATTR_WHITELIST = DEFAULT_HTML_ATTR_WHITELIST;
|
|
exports.DEFAULT_BODY_FLAGS = DEFAULT_BODY_FLAGS;
|
|
exports.DEFAULT_WIKI_FLAGS = DEFAULT_WIKI_FLAGS;
|
|
|
|
exports.HTML_SKIP_HTML = HTML_SKIP_HTML;
|
|
exports.HTML_SKIP_STYLE = HTML_SKIP_STYLE;
|
|
exports.HTML_SKIP_IMAGES = HTML_SKIP_IMAGES;
|
|
exports.HTML_SKIP_LINKS = HTML_SKIP_LINKS;
|
|
exports.HTML_EXPAND_TABS = HTML_EXPAND_TABS;
|
|
exports.HTML_SAFELINK = HTML_SAFELINK;
|
|
exports.HTML_TOC = HTML_TOC;
|
|
exports.HTML_HARD_WRAP = HTML_HARD_WRAP;
|
|
exports.HTML_USE_XHTML = HTML_USE_XHTML;
|
|
exports.HTML_ESCAPE = HTML_ESCAPE;
|
|
exports.HTML_ALLOW_ELEMENT_WHITELIST = HTML_ALLOW_ELEMENT_WHITELIST;
|
|
|
|
exports.MKDEXT_NO_INTRA_EMPHASIS = MKDEXT_NO_INTRA_EMPHASIS;
|
|
exports.MKDEXT_TABLES = MKDEXT_TABLES;
|
|
exports.MKDEXT_FENCED_CODE = MKDEXT_FENCED_CODE;
|
|
exports.MKDEXT_AUTOLINK = MKDEXT_AUTOLINK;
|
|
exports.MKDEXT_STRIKETHROUGH = MKDEXT_STRIKETHROUGH;
|
|
exports.MKDEXT_SPACE_HEADERS = MKDEXT_SPACE_HEADERS;
|
|
exports.MKDEXT_SUPERSCRIPT = MKDEXT_SUPERSCRIPT;
|
|
exports.MKDEXT_LAX_SPACING = MKDEXT_LAX_SPACING;
|
|
exports.MKDEXT_NO_EMAIL_AUTOLINK = MKDEXT_NO_EMAIL_AUTOLINK;
|
|
|
|
exports['SD_AUTOLINK_SHORT_DOMAINS'] = SD_AUTOLINK_SHORT_DOMAINS;
|
|
|
|
exports.MKDA_NOT_AUTOLINK = MKDA_NOT_AUTOLINK;
|
|
exports.MKDA_NORMAL = MKDA_NORMAL;
|
|
exports.MKDA_EMAIL = MKDA_EMAIL;
|
|
|
|
if (typeof define === 'function') {
|
|
define('snuownd', [], exports);
|
|
}
|
|
})(typeof(exports)!=='undefined'?exports:typeof(window)!=='undefined'?window.SnuOwnd={}:{});
|
|
|