aboutsummaryrefslogtreecommitdiff
path: root/js/lib/inlines.js
blob: 34f15604229c4029cd292e7b0cd111cedb4b0bc4 (plain)
  1. var fromCodePoint = require('./from-code-point.js');
  2. var entityToChar = require('./html5-entities.js').entityToChar;
  3. // Constants for character codes:
  4. var C_NEWLINE = 10;
  5. var C_SPACE = 32;
  6. var C_ASTERISK = 42;
  7. var C_UNDERSCORE = 95;
  8. var C_BACKTICK = 96;
  9. var C_OPEN_BRACKET = 91;
  10. var C_CLOSE_BRACKET = 93;
  11. var C_LESSTHAN = 60;
  12. var C_GREATERTHAN = 62;
  13. var C_BANG = 33;
  14. var C_BACKSLASH = 92;
  15. var C_AMPERSAND = 38;
  16. var C_OPEN_PAREN = 40;
  17. var C_COLON = 58;
  18. // Some regexps used in inline parser:
  19. var ESCAPABLE = '[!"#$%&\'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]';
  20. var ESCAPED_CHAR = '\\\\' + ESCAPABLE;
  21. var IN_DOUBLE_QUOTES = '"(' + ESCAPED_CHAR + '|[^"\\x00])*"';
  22. var IN_SINGLE_QUOTES = '\'(' + ESCAPED_CHAR + '|[^\'\\x00])*\'';
  23. var IN_PARENS = '\\((' + ESCAPED_CHAR + '|[^)\\x00])*\\)';
  24. var REG_CHAR = '[^\\\\()\\x00-\\x20]';
  25. var IN_PARENS_NOSP = '\\((' + REG_CHAR + '|' + ESCAPED_CHAR + ')*\\)';
  26. var TAGNAME = '[A-Za-z][A-Za-z0-9]*';
  27. var ATTRIBUTENAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*';
  28. var UNQUOTEDVALUE = "[^\"'=<>`\\x00-\\x20]+";
  29. var SINGLEQUOTEDVALUE = "'[^']*'";
  30. var DOUBLEQUOTEDVALUE = '"[^"]*"';
  31. var ATTRIBUTEVALUE = "(?:" + UNQUOTEDVALUE + "|" + SINGLEQUOTEDVALUE + "|" + DOUBLEQUOTEDVALUE + ")";
  32. var ATTRIBUTEVALUESPEC = "(?:" + "\\s*=" + "\\s*" + ATTRIBUTEVALUE + ")";
  33. var ATTRIBUTE = "(?:" + "\\s+" + ATTRIBUTENAME + ATTRIBUTEVALUESPEC + "?)";
  34. var OPENTAG = "<" + TAGNAME + ATTRIBUTE + "*" + "\\s*/?>";
  35. var CLOSETAG = "</" + TAGNAME + "\\s*[>]";
  36. var HTMLCOMMENT = "<!--([^-]+|[-][^-]+)*-->";
  37. var PROCESSINGINSTRUCTION = "[<][?].*?[?][>]";
  38. var DECLARATION = "<![A-Z]+" + "\\s+[^>]*>";
  39. var CDATA = "<!\\[CDATA\\[([^\\]]+|\\][^\\]]|\\]\\][^>])*\\]\\]>";
  40. var HTMLTAG = "(?:" + OPENTAG + "|" + CLOSETAG + "|" + HTMLCOMMENT + "|" +
  41. PROCESSINGINSTRUCTION + "|" + DECLARATION + "|" + CDATA + ")";
  42. var ENTITY = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});";
  43. var reHtmlTag = new RegExp('^' + HTMLTAG, 'i');
  44. var reLinkTitle = new RegExp(
  45. '^(?:"(' + ESCAPED_CHAR + '|[^"\\x00])*"' +
  46. '|' +
  47. '\'(' + ESCAPED_CHAR + '|[^\'\\x00])*\'' +
  48. '|' +
  49. '\\((' + ESCAPED_CHAR + '|[^)\\x00])*\\))');
  50. var reLinkDestinationBraces = new RegExp(
  51. '^(?:[<](?:[^<>\\n\\\\\\x00]' + '|' + ESCAPED_CHAR + '|' + '\\\\)*[>])');
  52. var reLinkDestination = new RegExp(
  53. '^(?:' + REG_CHAR + '+|' + ESCAPED_CHAR + '|' + IN_PARENS_NOSP + ')*');
  54. var reEscapable = new RegExp(ESCAPABLE);
  55. var reAllEscapedChar = new RegExp('\\\\(' + ESCAPABLE + ')', 'g');
  56. var reEscapedChar = new RegExp('^\\\\(' + ESCAPABLE + ')');
  57. var reEntityHere = new RegExp('^' + ENTITY, 'i');
  58. var reEntity = new RegExp(ENTITY, 'gi');
  59. // Matches a character with a special meaning in markdown,
  60. // or a string of non-special characters. Note: we match
  61. // clumps of _ or * or `, because they need to be handled in groups.
  62. var reMain = /^(?:[_*`\n]+|[\[\]\\!<&*_]|(?: *[^\n `\[\]\\!<&*_]+)+|[ \n]+)/m;
  63. // Replace entities and backslash escapes with literal characters.
  64. var unescapeString = function(s) {
  65. return s.replace(reAllEscapedChar, '$1')
  66. .replace(reEntity, entityToChar);
  67. };
  68. // Normalize reference label: collapse internal whitespace
  69. // to single space, remove leading/trailing whitespace, case fold.
  70. var normalizeReference = function(s) {
  71. return s.trim()
  72. .replace(/\s+/,' ')
  73. .toUpperCase();
  74. };
  75. // INLINE PARSER
  76. // These are methods of an InlineParser object, defined below.
  77. // An InlineParser keeps track of a subject (a string to be
  78. // parsed) and a position in that subject.
  79. // If re matches at current position in the subject, advance
  80. // position in subject and return the match; otherwise return null.
  81. var match = function(re) {
  82. var match = re.exec(this.subject.slice(this.pos));
  83. if (match) {
  84. this.pos += match.index + match[0].length;
  85. return match[0];
  86. } else {
  87. return null;
  88. }
  89. };
  90. // Returns the code for the character at the current subject position, or -1
  91. // there are no more characters.
  92. var peek = function() {
  93. if (this.pos < this.subject.length) {
  94. return this.subject.charCodeAt(this.pos);
  95. } else {
  96. return -1;
  97. }
  98. };
  99. // Parse zero or more space characters, including at most one newline
  100. var spnl = function() {
  101. this.match(/^ *(?:\n *)?/);
  102. return 1;
  103. };
  104. // All of the parsers below try to match something at the current position
  105. // in the subject. If they succeed in matching anything, they
  106. // return the inline matched, advancing the subject.
  107. // Attempt to parse backticks, returning either a backtick code span or a
  108. // literal sequence of backticks.
  109. var parseBackticks = function(inlines) {
  110. var startpos = this.pos;
  111. var ticks = this.match(/^`+/);
  112. if (!ticks) {
  113. return 0;
  114. }
  115. var afterOpenTicks = this.pos;
  116. var foundCode = false;
  117. var match;
  118. while (!foundCode && (match = this.match(/`+/m))) {
  119. if (match == ticks) {
  120. inlines.push({ t: 'Code', c: this.subject.slice(afterOpenTicks,
  121. this.pos - ticks.length)
  122. .replace(/[ \n]+/g,' ')
  123. .trim() });
  124. return true;
  125. }
  126. }
  127. // If we got here, we didn't match a closing backtick sequence.
  128. this.pos = afterOpenTicks;
  129. inlines.push({ t: 'Str', c: ticks });
  130. return true;
  131. };
  132. // Parse a backslash-escaped special character, adding either the escaped
  133. // character, a hard line break (if the backslash is followed by a newline),
  134. // or a literal backslash to the 'inlines' list.
  135. var parseBackslash = function(inlines) {
  136. var subj = this.subject,
  137. pos = this.pos;
  138. if (subj.charCodeAt(pos) === C_BACKSLASH) {
  139. if (subj.charAt(pos + 1) === '\n') {
  140. this.pos = this.pos + 2;
  141. inlines.push({ t: 'Hardbreak' });
  142. } else if (reEscapable.test(subj.charAt(pos + 1))) {
  143. this.pos = this.pos + 2;
  144. inlines.push({ t: 'Str', c: subj.charAt(pos + 1) });
  145. } else {
  146. this.pos++;
  147. inlines.push({t: 'Str', c: '\\'});
  148. }
  149. return true;
  150. } else {
  151. return false;
  152. }
  153. };
  154. // Attempt to parse an autolink (URL or email in pointy brackets).
  155. var parseAutolink = function(inlines) {
  156. var m;
  157. var dest;
  158. if ((m = this.match(/^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/))) { // email autolink
  159. dest = m.slice(1,-1);
  160. inlines.push(
  161. {t: 'Link',
  162. label: [{ t: 'Str', c: dest }],
  163. destination: 'mailto:' + encodeURI(unescape(dest)) });
  164. return true;
  165. } else if ((m = this.match(/^<(?:coap|doi|javascript|aaa|aaas|about|acap|cap|cid|crid|data|dav|dict|dns|file|ftp|geo|go|gopher|h323|http|https|iax|icap|im|imap|info|ipp|iris|iris.beep|iris.xpc|iris.xpcs|iris.lwz|ldap|mailto|mid|msrp|msrps|mtqp|mupdate|news|nfs|ni|nih|nntp|opaquelocktoken|pop|pres|rtsp|service|session|shttp|sieve|sip|sips|sms|snmp|soap.beep|soap.beeps|tag|tel|telnet|tftp|thismessage|tn3270|tip|tv|urn|vemmi|ws|wss|xcon|xcon-userid|xmlrpc.beep|xmlrpc.beeps|xmpp|z39.50r|z39.50s|adiumxtra|afp|afs|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|chrome|chrome-extension|com-eventbrite-attendee|content|cvs|dlna-playsingle|dlna-playcontainer|dtn|dvb|ed2k|facetime|feed|finger|fish|gg|git|gizmoproject|gtalk|hcp|icon|ipn|irc|irc6|ircs|itms|jar|jms|keyparc|lastfm|ldaps|magnet|maps|market|message|mms|ms-help|msnim|mumble|mvn|notes|oid|palm|paparazzi|platform|proxy|psyc|query|res|resource|rmi|rsync|rtmp|secondlife|sftp|sgn|skype|smb|soldat|spotify|ssh|steam|svn|teamspeak|things|udp|unreal|ut2004|ventrilo|view-source|webcal|wtai|wyciwyg|xfire|xri|ymsgr):[^<>\x00-\x20]*>/i))) {
  166. dest = m.slice(1,-1);
  167. inlines.push({
  168. t: 'Link',
  169. label: [{ t: 'Str', c: dest }],
  170. destination: encodeURI(unescape(dest)) });
  171. return true;
  172. } else {
  173. return false;
  174. }
  175. };
  176. // Attempt to parse a raw HTML tag.
  177. var parseHtmlTag = function(inlines) {
  178. var m = this.match(reHtmlTag);
  179. if (m) {
  180. inlines.push({ t: 'Html', c: m });
  181. return true;
  182. } else {
  183. return false;
  184. }
  185. };
  186. // Scan a sequence of characters with code cc, and return information about
  187. // the number of delimiters and whether they are positioned such that
  188. // they can open and/or close emphasis or strong emphasis. A utility
  189. // function for strong/emph parsing.
  190. var scanDelims = function(cc) {
  191. var numdelims = 0;
  192. var first_close_delims = 0;
  193. var char_before, char_after, cc_after;
  194. var startpos = this.pos;
  195. char_before = this.pos === 0 ? '\n' :
  196. this.subject.charAt(this.pos - 1);
  197. while (this.peek() === cc) {
  198. numdelims++;
  199. this.pos++;
  200. }
  201. cc_after = this.peek();
  202. if (cc_after === -1) {
  203. char_after = '\n';
  204. } else {
  205. char_after = fromCodePoint(cc_after);
  206. }
  207. var can_open = numdelims > 0 && numdelims <= 3 && !(/\s/.test(char_after));
  208. var can_close = numdelims > 0 && numdelims <= 3 && !(/\s/.test(char_before));
  209. if (cc === C_UNDERSCORE) {
  210. can_open = can_open && !((/[a-z0-9]/i).test(char_before));
  211. can_close = can_close && !((/[a-z0-9]/i).test(char_after));
  212. }
  213. this.pos = startpos;
  214. return { numdelims: numdelims,
  215. can_open: can_open,
  216. can_close: can_close };
  217. };
  218. var Emph = function(ils) {
  219. return {t: 'Emph', c: ils};
  220. };
  221. var Strong = function(ils) {
  222. return {t: 'Strong', c: ils};
  223. };
  224. var Str = function(s) {
  225. return {t: 'Str', c: s};
  226. };
  227. // Attempt to parse emphasis or strong emphasis.
  228. var parseEmphasis = function(cc,inlines) {
  229. var startpos = this.pos;
  230. var c ;
  231. var first_close = 0;
  232. c = fromCodePoint(cc);
  233. var numdelims;
  234. var numclosedelims;
  235. var delimpos;
  236. // Get opening delimiters.
  237. res = this.scanDelims(cc);
  238. numdelims = res.numdelims;
  239. if (numdelims === 0) {
  240. this.pos = startpos;
  241. return false;
  242. }
  243. if (numdelims >= 4 || !res.can_open) {
  244. this.pos += numdelims;
  245. inlines.push(Str(this.subject.slice(startpos, startpos + numdelims)));
  246. return true;
  247. }
  248. this.pos += numdelims;
  249. var delims_to_match = numdelims;
  250. var current = [];
  251. var firstend;
  252. var firstpos;
  253. var state = 0;
  254. var can_close = false;
  255. var can_open = false;
  256. var last_emphasis_closer = null;
  257. while (this.last_emphasis_closer[c] >= this.pos) {
  258. res = this.scanDelims(cc);
  259. numclosedelims = res.numdelims;
  260. if (res.can_close) {
  261. if (last_emphasis_closer === null ||
  262. last_emphasis_closer < this.pos) {
  263. last_emphasis_closer = this.pos;
  264. }
  265. if (numclosedelims === 3 && delims_to_match === 3) {
  266. delims_to_match -= 3;
  267. this.pos += 3;
  268. current = [{t: 'Strong', c: [{t: 'Emph', c: current}]}];
  269. } else if (numclosedelims >= 2 && delims_to_match >= 2) {
  270. delims_to_match -= 2;
  271. this.pos += 2;
  272. firstend = current.length;
  273. firstpos = this.pos;
  274. current = [{t: 'Strong', c: current}];
  275. } else if (numclosedelims >= 1 && delims_to_match >= 1) {
  276. delims_to_match -= 1;
  277. this.pos += 1;
  278. firstend = current.length;
  279. firstpos = this.pos;
  280. current = [{t: 'Emph', c: current}];
  281. } else {
  282. if (!(this.parseInline(current,true))) {
  283. break;
  284. }
  285. }
  286. if (delims_to_match === 0) {
  287. Array.prototype.push.apply(inlines, current);
  288. return true;
  289. }
  290. } else if (!(this.parseInline(current,true))) {
  291. break;
  292. }
  293. }
  294. // we didn't match emphasis: fallback
  295. inlines.push(Str(this.subject.slice(startpos,
  296. startpos + delims_to_match)));
  297. if (delims_to_match < numdelims) {
  298. Array.prototype.push.apply(inlines, current.slice(0,firstend));
  299. this.pos = firstpos;
  300. } else { // delims_to_match === numdelims
  301. this.pos = startpos + delims_to_match;
  302. }
  303. if (last_emphasis_closer) {
  304. this.last_emphasis_closer[c] = last_emphasis_closer;
  305. }
  306. return true;
  307. };
  308. // Attempt to parse link title (sans quotes), returning the string
  309. // or null if no match.
  310. var parseLinkTitle = function() {
  311. var title = this.match(reLinkTitle);
  312. if (title) {
  313. // chop off quotes from title and unescape:
  314. return unescapeString(title.substr(1, title.length - 2));
  315. } else {
  316. return null;
  317. }
  318. };
  319. // Attempt to parse link destination, returning the string or
  320. // null if no match.
  321. var parseLinkDestination = function() {
  322. var res = this.match(reLinkDestinationBraces);
  323. if (res) { // chop off surrounding <..>:
  324. return encodeURI(unescape(unescapeString(res.substr(1, res.length - 2))));
  325. } else {
  326. res = this.match(reLinkDestination);
  327. if (res !== null) {
  328. return encodeURI(unescape(unescapeString(res)));
  329. } else {
  330. return null;
  331. }
  332. }
  333. };
  334. // Attempt to parse a link label, returning number of characters parsed.
  335. var parseLinkLabel = function() {
  336. if (this.peek() != C_OPEN_BRACKET) {
  337. return 0;
  338. }
  339. var startpos = this.pos;
  340. var nest_level = 0;
  341. if (this.label_nest_level > 0) {
  342. // If we've already checked to the end of this subject
  343. // for a label, even with a different starting [, we
  344. // know we won't find one here and we can just return.
  345. // This avoids lots of backtracking.
  346. // Note: nest level 1 would be: [foo [bar]
  347. // nest level 2 would be: [foo [bar [baz]
  348. this.label_nest_level--;
  349. return 0;
  350. }
  351. this.pos++; // advance past [
  352. var c;
  353. while ((c = this.peek()) && c != -1 && (c != C_CLOSE_BRACKET || nest_level > 0)) {
  354. switch (c) {
  355. case C_BACKTICK:
  356. this.parseBackticks([]);
  357. break;
  358. case C_LESSTHAN:
  359. if (!(this.parseAutolink([]) || this.parseHtmlTag([]))) {
  360. this.pos++;
  361. }
  362. break;
  363. case C_OPEN_BRACKET: // nested []
  364. nest_level++;
  365. this.pos++;
  366. break;
  367. case C_CLOSE_BRACKET: // nested []
  368. nest_level--;
  369. this.pos++;
  370. break;
  371. case C_BACKSLASH:
  372. this.parseBackslash([]);
  373. break;
  374. default:
  375. this.parseString([]);
  376. }
  377. }
  378. if (c === C_CLOSE_BRACKET) {
  379. this.label_nest_level = 0;
  380. this.pos++; // advance past ]
  381. return this.pos - startpos;
  382. } else {
  383. if (c === -1) {
  384. this.label_nest_level = nest_level;
  385. }
  386. this.pos = startpos;
  387. return 0;
  388. }
  389. };
  390. // Parse raw link label, including surrounding [], and return
  391. // inline contents. (Note: this is not a method of InlineParser.)
  392. var parseRawLabel = function(s) {
  393. // note: parse without a refmap; we don't want links to resolve
  394. // in nested brackets!
  395. return new InlineParser().parse(s.substr(1, s.length - 2), {});
  396. };
  397. // Attempt to parse a link. If successful, return the link.
  398. var parseLink = function(inlines) {
  399. var startpos = this.pos;
  400. var reflabel;
  401. var n;
  402. var dest;
  403. var title;
  404. n = this.parseLinkLabel();
  405. if (n === 0) {
  406. return false;
  407. }
  408. var afterlabel = this.pos;
  409. var rawlabel = this.subject.substr(startpos, n);
  410. // if we got this far, we've parsed a label.
  411. // Try to parse an explicit link: [label](url "title")
  412. if (this.peek() == C_OPEN_PAREN) {
  413. this.pos++;
  414. if (this.spnl() &&
  415. ((dest = this.parseLinkDestination()) !== null) &&
  416. this.spnl() &&
  417. // make sure there's a space before the title:
  418. (/^\s/.test(this.subject.charAt(this.pos - 1)) &&
  419. (title = this.parseLinkTitle() || '') || true) &&
  420. this.spnl() &&
  421. this.match(/^\)/)) {
  422. inlines.push({ t: 'Link',
  423. destination: dest,
  424. title: title,
  425. label: parseRawLabel(rawlabel) });
  426. return true;
  427. } else {
  428. this.pos = startpos;
  429. return false;
  430. }
  431. }
  432. // If we're here, it wasn't an explicit link. Try to parse a reference link.
  433. // first, see if there's another label
  434. var savepos = this.pos;
  435. this.spnl();
  436. var beforelabel = this.pos;
  437. n = this.parseLinkLabel();
  438. if (n == 2) {
  439. // empty second label
  440. reflabel = rawlabel;
  441. } else if (n > 0) {
  442. reflabel = this.subject.slice(beforelabel, beforelabel + n);
  443. } else {
  444. this.pos = savepos;
  445. reflabel = rawlabel;
  446. }
  447. // lookup rawlabel in refmap
  448. var link = this.refmap[normalizeReference(reflabel)];
  449. if (link) {
  450. inlines.push({t: 'Link',
  451. destination: link.destination,
  452. title: link.title,
  453. label: parseRawLabel(rawlabel) });
  454. return true;
  455. } else {
  456. this.pos = startpos;
  457. return false;
  458. }
  459. // Nothing worked, rewind:
  460. this.pos = startpos;
  461. return false;
  462. };
  463. // Attempt to parse an entity, return Entity object if successful.
  464. var parseEntity = function(inlines) {
  465. var m;
  466. if ((m = this.match(reEntityHere))) {
  467. inlines.push({ t: 'Str', c: entityToChar(m) });
  468. return true;
  469. } else {
  470. return false;
  471. }
  472. };
  473. // Parse a run of ordinary characters, or a single character with
  474. // a special meaning in markdown, as a plain string, adding to inlines.
  475. var parseString = function(inlines) {
  476. var m;
  477. if ((m = this.match(reMain))) {
  478. inlines.push({ t: 'Str', c: m });
  479. return true;
  480. } else {
  481. return false;
  482. }
  483. };
  484. // Parse a newline. If it was preceded by two spaces, return a hard
  485. // line break; otherwise a soft line break.
  486. var parseNewline = function(inlines) {
  487. var m = this.match(/^ *\n/);
  488. if (m) {
  489. if (m.length > 2) {
  490. inlines.push({ t: 'Hardbreak' });
  491. } else if (m.length > 0) {
  492. inlines.push({ t: 'Softbreak' });
  493. }
  494. return true;
  495. }
  496. return false;
  497. };
  498. // Attempt to parse an image. If the opening '!' is not followed
  499. // by a link, return a literal '!'.
  500. var parseImage = function(inlines) {
  501. if (this.match(/^!/)) {
  502. var link = this.parseLink(inlines);
  503. if (link) {
  504. inlines[inlines.length - 1].t = 'Image';
  505. return true;
  506. } else {
  507. inlines.push({ t: 'Str', c: '!' });
  508. return true;
  509. }
  510. } else {
  511. return false;
  512. }
  513. };
  514. // Attempt to parse a link reference, modifying refmap.
  515. var parseReference = function(s, refmap) {
  516. this.subject = s;
  517. this.pos = 0;
  518. this.label_nest_level = 0;
  519. var rawlabel;
  520. var dest;
  521. var title;
  522. var matchChars;
  523. var startpos = this.pos;
  524. var match;
  525. // label:
  526. matchChars = this.parseLinkLabel();
  527. if (matchChars === 0) {
  528. return 0;
  529. } else {
  530. rawlabel = this.subject.substr(0, matchChars);
  531. }
  532. // colon:
  533. if (this.peek() === C_COLON) {
  534. this.pos++;
  535. } else {
  536. this.pos = startpos;
  537. return 0;
  538. }
  539. // link url
  540. this.spnl();
  541. dest = this.parseLinkDestination();
  542. if (dest === null || dest.length === 0) {
  543. this.pos = startpos;
  544. return 0;
  545. }
  546. var beforetitle = this.pos;
  547. this.spnl();
  548. title = this.parseLinkTitle();
  549. if (title === null) {
  550. title = '';
  551. // rewind before spaces
  552. this.pos = beforetitle;
  553. }
  554. // make sure we're at line end:
  555. if (this.match(/^ *(?:\n|$)/) === null) {
  556. this.pos = startpos;
  557. return 0;
  558. }
  559. var normlabel = normalizeReference(rawlabel);
  560. if (!refmap[normlabel]) {
  561. refmap[normlabel] = { destination: dest, title: title };
  562. }
  563. return this.pos - startpos;
  564. };
  565. // Parse the next inline element in subject, advancing subject position.
  566. // If memoize is set, memoize the result.
  567. // On success, add the result to the inlines list, and return true.
  568. // On failure, return false.
  569. var parseInline = function(inlines, memoize) {
  570. var startpos = this.pos;
  571. var origlen = inlines.length;
  572. var memoized = memoize && this.memo[startpos];
  573. if (memoized) {
  574. this.pos = memoized.endpos;
  575. Array.prototype.push.apply(inlines, memoized.inline);
  576. return true;
  577. }
  578. var c = this.peek();
  579. if (c === -1) {
  580. return false;
  581. }
  582. var res;
  583. switch(c) {
  584. case C_NEWLINE:
  585. case C_SPACE:
  586. res = this.parseNewline(inlines);
  587. break;
  588. case C_BACKSLASH:
  589. res = this.parseBackslash(inlines);
  590. break;
  591. case C_BACKTICK:
  592. res = this.parseBackticks(inlines);
  593. break;
  594. case C_ASTERISK:
  595. case C_UNDERSCORE:
  596. res = this.parseEmphasis(c, inlines);
  597. break;
  598. case C_OPEN_BRACKET:
  599. res = this.parseLink(inlines);
  600. break;
  601. case C_BANG:
  602. res = this.parseImage(inlines);
  603. break;
  604. case C_LESSTHAN:
  605. res = this.parseAutolink(inlines) || this.parseHtmlTag(inlines);
  606. break;
  607. case C_AMPERSAND:
  608. res = this.parseEntity(inlines);
  609. break;
  610. default:
  611. res = this.parseString(inlines);
  612. break;
  613. }
  614. if (!res) {
  615. this.pos += 1;
  616. inlines.push({t: 'Str', c: fromCodePoint(c)});
  617. }
  618. if (memoize) {
  619. this.memo[startpos] = { inline: inlines.slice(origlen),
  620. endpos: this.pos };
  621. }
  622. return true;
  623. };
  624. // Parse s as a list of inlines, using refmap to resolve references.
  625. var parseInlines = function(s, refmap) {
  626. this.subject = s;
  627. this.pos = 0;
  628. this.refmap = refmap || {};
  629. this.memo = {};
  630. this.last_emphasis_closer = { '*': s.length, '_': s.length };
  631. var inlines = [];
  632. while (this.parseInline(inlines, false)) {
  633. }
  634. return inlines;
  635. };
  636. // The InlineParser object.
  637. function InlineParser(){
  638. return {
  639. subject: '',
  640. label_nest_level: 0, // used by parseLinkLabel method
  641. last_emphasis_closer: null, // used by parseEmphasis method
  642. pos: 0,
  643. refmap: {},
  644. memo: {},
  645. match: match,
  646. peek: peek,
  647. spnl: spnl,
  648. unescapeString: unescapeString,
  649. parseBackticks: parseBackticks,
  650. parseBackslash: parseBackslash,
  651. parseAutolink: parseAutolink,
  652. parseHtmlTag: parseHtmlTag,
  653. scanDelims: scanDelims,
  654. parseEmphasis: parseEmphasis,
  655. parseLinkTitle: parseLinkTitle,
  656. parseLinkDestination: parseLinkDestination,
  657. parseLinkLabel: parseLinkLabel,
  658. parseLink: parseLink,
  659. parseEntity: parseEntity,
  660. parseString: parseString,
  661. parseNewline: parseNewline,
  662. parseImage: parseImage,
  663. parseReference: parseReference,
  664. parseInline: parseInline,
  665. parse: parseInlines
  666. };
  667. }
  668. module.exports = InlineParser;