"},H0.prototype.esc=$,e.HtmlRenderer=F,e.Node=m,e.Parser=function(e){return null==(e=e||{}).minimumHashtagLength&&(e.minimumHashtagLength=3),{doc:new R0,blocks:y0,blockStarts:v0,tip:this.doc,oldtip:this.doc,currentLine:"",lineNumber:0,offset:0,column:0,nextNonspace:0,nextNonspaceColumn:0,indent:0,indented:!1,blank:!1,partiallyConsumedTab:!1,allClosed:!0,lastMatchedContainer:this.doc,refmap:{},lastLineLength:0,inlineParser:new Xu(e),findNextNonspace:D0,advanceOffset:S0,advanceNextNonspace:q0,addLine:Qu,addChild:d0,changeTipType:e0,incorporateLine:T0,finalize:N0,processInlines:O0,closeUnmatchedBlocks:u0,parse:F0,options:e}},e.Renderer=O,e.XmlRenderer=H0,Object.defineProperty(e,"__esModule",{value:!0})});
diff --git a/lib/blocks.js b/lib/blocks.js
index 92f8a43a..15e2995b 100644
--- a/lib/blocks.js
+++ b/lib/blocks.js
@@ -1,7 +1,7 @@
"use strict";
import Node from "./node.js";
-import { unescapeString, OPENTAG, CLOSETAG } from "./common.js";
+import { unescapeString, OPENTAG, CLOSETAG, ESCAPABLE } from "./common.js";
import InlineParser from "./inlines.js";
var CODE_INDENT = 4;
@@ -55,10 +55,6 @@ var reSetextHeadingLine = /^(?:=+|-+)[ \t]*$/;
var reLineEnding = /\r\n|\n|\r/;
-var reTableRow = /^(\|?)(?:(?:\\\||[^|])*\|?)+$/;
-
-var reValidTableDelimiter = /^:?-+:?$/;
-
var MAX_AUTOCOMPLETED_CELLS = 1000;
// Returns true if string contains only space characters.
@@ -113,6 +109,18 @@ var addLine = function() {
this.tip._string_content += this.currentLine.slice(this.offset) + "\n";
};
+// Change the tip to be of type tag as long as the parent can
+// contain a block of that type. Returns whether or not the
+// type could be changed.
+var changeTipType = function(tag) {
+ const validChild = this.blocks[this.tip.parent.type].canContain(tag);
+ if (validChild) {
+ this.tip._type = tag;
+ }
+
+ return validChild;
+};
+
// Add block of type tag as a child of the tip. If the tip can't
// accept children, close and finalize it and try its parent,
// and so on til we find a block that can accept children.
@@ -459,9 +467,6 @@ var blocks = {
for (var row = block.firstChild; row; row = row.next) {
var i = 0;
for (var cell = row.firstChild; cell; cell = cell.next) {
- // copy column alignment to each cell
- cell.align = block.alignColumns[i];
-
i += 1;
// if there's more columns in a row than the header row, GitHub cuts them off
@@ -768,65 +773,83 @@ var blockStarts = [
// table
function(parser, container) {
- if (container.type !== "document") {
- // @hmhealey We should be able to have a table inside of a list item or block quote to match GitHub
- // but I'm not sure if the mobile app can render that, so let's not bother with it for now
+ // Because tables depend on two adjacent lines, the first line is read into a paragraph and then we might turn
+ // that paragraph into a table when we read the second line.
+
+ if (parser.indented || container.type !== "paragraph") {
return 0;
}
-
- if (parser.indented) {
+ if (container._tableVisited) {
return 0;
}
- if (!parser.nextLine) {
- // tables require at least two rows (header and delimiter)
+ // At this point, we're on the second line of the paragraph, so we can check to see the two lines we've read
+ // are the header row and delimiter row of the table
+
+ // Check for a delimiter first since it's stricter than the header row.
+ const delimiterCells = parseTableRow(parser.currentLine, parser.nextNonspace);
+ if (!delimiterCells || !validateDelimiterRow(delimiterCells)) {
+ // The second line of the paragraph isn't a table row, so this paragraph isn't actually a table
+ container._tableVisited = true;
return 0;
}
- const nextColumn = measureNonspaceColumn(parser.nextLine);
- if (Math.abs(nextColumn - parser.column) >= CODE_INDENT) {
- // the delimiter row must be on the same indentation level as the header row
+ // The previous line of text that we've read is stored as the paragraph's _string_content, and that will
+ // contain the header row if this is actually a table
+ const headerCharacters = container._string_content.indexOf('\n'); // TODO this should probably be returned explicilty by parseTableRow
+ const headerCells = parseTableRow(container._string_content.substring(0, container._string_content.indexOf('\n')), 0);
+ if (!headerCells) {
+ // The first line isn't a header row, so this isn't a table
+ container._tableVisited = true;
return 0;
}
- parser.advanceNextNonspace();
+ if (delimiterCells.length !== headerCells.length) {
+ // The first two rows must be the same length for this to be considered a table
+ console.log(' wrong lengths');
- // check for a delimiter first since it's stricter than the header row
- const nextLine = parser.nextLine.trim();
- const delimiterMatch = reTableRow.exec(nextLine);
- if (!delimiterMatch) {
+ // Track that we've already identified that this paragraph isn't a table, so that we don't check the same
+ // paragraph again
+ container._tableVisited = true;
return 0;
}
- const delimiterCells = parseDelimiterRow(delimiterMatch[0]);
- if (!delimiterCells) {
+ // Turn this paragraph into a table if possible
+ if (!parser.changeTipType('table')) {
return 0;
}
- const currentLine = parser.currentLine.slice(parser.nextNonspace).trim();
- var headerMatch = reTableRow.exec(currentLine);
- if (!headerMatch) {
- return 0;
- }
+ // TODO try inserting header paragraph to match GitHub letting you break from a paragraph into a table with only one newline
- var headerCells = parseTableCells(headerMatch[0]);
+ // Store the alignments of the columns and then skip the delimiter line since we've
+ // gotten what we need from it
+ parser.tip.alignColumns = delimiterCells.map(getCellAlignment);
- if (delimiterCells.length !== headerCells.length) {
- // the first two rows must be the same length for this to be considered a table
- return 0;
- }
+ const headerRow = new Node('table_row', [
+ [this.lineNumber - 1, parser.offset + 1],
+ [this.lineNumber - 1 + headerCharacters, parser.offset + 1], // TODO these are probably wrong
+ ]);
+ headerRow._string_content = container._string_content.substring(0, headerCharacters);
+ headerRow._isHeading = true;
- parser.closeUnmatchedBlocks();
+ for (let i = 0; i < headerCells.length; i++) {
+ const cell = new Node('table_cell'); // TODO sourcepos
+ cell._string_content = headerCells[i];
- parser.advanceNextNonspace();
- parser.addChild("table", parser.offset);
+ cell._align = parser.tip.alignColumns[i];
+ cell._isHeading = true;
- // store the alignments of the columns and then skip the delimiter line since we've
- // gotten what we need from it
- parser.tip.alignColumns = delimiterCells.map(getCellAlignment);
+ headerRow.appendChild(cell);
+ }
+
+ parser.tip.appendChild(headerRow);
- parser.skipNextLine();
+ // Mark the rest of the line as read
+ parser.advanceOffset(
+ parser.currentLine.length - parser.offset,
+ false
+ );
return 1;
},
@@ -841,65 +864,152 @@ var blockStarts = [
return 2;
}
- var rowMatch = reTableRow.exec(parser.currentLine.slice(parser.nextNonspace));
- if (!rowMatch) {
+ const cells = parseTableRow(parser.currentLine, parser.nextNonspace);
+ if (!cells) {
return 0;
}
- parser.closeUnmatchedBlocks();
- parser.advanceNextNonspace();
+ parser.addChild("table_row", parser.nextNonspace);
- parser.addChild("table_row", parser.offset);
+ for (let i = 0; i < cells.length; i++) {
+ parser.addChild("table_cell", parser.nextNonspace);
- // advance past leading | if one exists
- parser.advanceOffset(rowMatch[1].length, false);
+ parser.tip._align = parser.tip.parent.parent.alignColumns[i];
+ parser.tip._string_content = cells[i];
- // parse the row into cells
- var cells = parseTableCells(rowMatch[0]);
- var length = cells.length;
- for (var i = 0; i < length; i++) {
- parser.addChild("table_cell", parser.offset);
-
- parser.tip._string_content = cells[i].trim();
-
- parser.advanceOffset(cells[i].length + 1);
+ parser.advanceNextNonspace();
}
+ // Mark the rest of the line as read
+ parser.advanceOffset(
+ parser.currentLine.length - parser.offset,
+ false
+ );
+
return 2;
}
];
-const parseDelimiterRow = function(row) {
- if (row.indexOf("|") === -1) {
+const parseTableRow = function(line, startAt) {
+ // This is attempting to replicate row_from_string from GitHub's Commonmark fork. That function can be found here:
+ // https://github.com/github/cmark-gfm/blob/587a12bb54d95ac37241377e6ddc93ea0e45439b/extensions/table.c#L189
+
+ let cells = [];
+
+ let expectMoreCells = true;
+
+ // Start at the current parser position
+ let offset = startAt;
+
+ // Read past the optional leading pipe
+ offset += scanTableCellEnd(line, offset);
+
+ while (offset < line.length && expectMoreCells) {
+ const cellLength = scanTableCell(line, offset);
+ const pipeLength = scanTableCellEnd(line, offset + cellLength);
+
+ if (cellLength > 0 || pipeLength > 0) {
+ // We're guaranteed to have found a cell because we either found some cell content (cellLength > 0) or
+ // we found an empty cell with a pipe (cellLength == 0 && pipeLength > 0)
+ const cellContents = unescapePipes(line.substring(offset, offset + cellLength));
+
+ cells.push(cellContents);
+
+ offset += cellLength + pipeLength;
+ }
+
+ if (pipeLength > 0) {
+ expectMoreCells = true;
+ } else {
+ // We've read the last cell, so check if we've reached the end of the row
+ const rowEndLength = scanTableRowEnd(line, offset);
+
+ // TODO there's something in the GH code about the row being part of a preceding paragraph which we may not
+ // need. I think it has to do with GH's behaviour where there's no double-newline needed between a
+ // paragraph and a table which we don't support in Marked or Commonmark.
+
+ if (rowEndLength === -1) {
+ // There's more text after this, so this isn't a table row
+ } else {
+ offset += rowEndLength;
+ }
+
+ expectMoreCells = false;
+ }
+ }
+
+ if (offset === line.length) {
+ // We've read the whole line, so it's a valid row
+ return cells;
+ } else {
+ // There's unhandled text here, so it's not actually a table row
return null;
}
+};
+
+const reTableCell = new RegExp('^([\\\\]' + ESCAPABLE + '|[^|\r\n])+');
+const scanTableCell = function(line, offset) {
+ // Reads up until a newline or an unescaped pipe and return the number of characters read
+ const match = reTableCell.exec(line.substring(offset));
+ if (match) {
+ return match[0].length;
+ } else {
+ // If this doesn't match, it may still be valid because there's an empty table cell or we're at the end of the line
+ return 0;
+ }
+};
- const cells = parseTableCells(row).map((cell) => cell.trim());
+const scanTableCellEnd = function(line, offset) {
+ // Read an optional pipe followed by some amount of optional whitespace and return the number of characters read
+ let i = 0;
- for (const cell of cells) {
- if (!reValidTableDelimiter.test(cell)) {
- return null;
- }
+ if (line.charAt(offset + i) === '|') {
+ i += 1;
}
- return cells;
-}
+ let c = line.charAt(offset + i);
+ while (c === ' ' || c === '\t' || c === '\v' || c === '\f') {
+ i += 1;
+ c = line.charAt(offset + i);
+ }
-var parseTableCells = function(row) {
- // remove starting pipe to make life easier
- row = row.replace(/^\|/, "");
+ return i;
+};
- var reTableCell = /\||((?:\\\||[^|])+)\|?/g;
+const scanTableRowEnd = function(line, offset) {
+ // Read any amount of optional whitespace and then ensure that we're at the end of the string
+ let i = 0;
- var match;
- var cells = [];
- while (match = reTableCell.exec(row)) {
- cells.push(match[1] || "");
+ let c = line.charAt(offset + i);
+ while (c === ' ' || c === '\t' || c === '\v' || c === '\f') {
+ i += 1;
+ c = line.charAt(offset + i);
}
- return cells;
+ if (offset + i === line.length) {
+ // This is the end of the row
+ return i;
+ } else {
+ // There's still more after this which means this isn't actually a table row
+ return -1;
+ }
};
+const reValidTableDelimiter = /^[ \t]*:?-+:?[ \t]*$/;
+const validateDelimiterRow = function(cells) {
+ for (const cell of cells) {
+ if (!reValidTableDelimiter.test(cell)) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+const unescapePipes = function(str) {
+ return str.replace('\\|', '|');
+}
+
var getCellAlignment = function(cell) {
cell = cell.trim();
@@ -974,31 +1084,10 @@ var findNextNonspace = function() {
this.indented = this.indent >= CODE_INDENT;
};
-function measureNonspaceColumn(line) {
- // This code is copied from findNextNonspace above
- var i = 0;
- var cols = 0;
- var c;
-
- while ((c = line.charAt(i)) !== "") {
- if (c === " ") {
- i++;
- cols++;
- } else if (c === "\t") {
- i++;
- cols += 4 - (cols % 4);
- } else {
- break;
- }
- }
-
- return cols;
-}
-
// Analyze a line of text and update the document appropriately.
// We parse markdown text by calling this on each line of input,
// then finalizing the document.
-var incorporateLine = function(ln, nextLn) {
+var incorporateLine = function(ln) {
var all_matched = true;
var t;
@@ -1016,7 +1105,6 @@ var incorporateLine = function(ln, nextLn) {
}
this.currentLine = ln;
- this.nextLine = nextLn;
// For each containing block, try to parse the associated line start.
// Bail out on failure: container will point to the last matching block.
@@ -1061,7 +1149,7 @@ var incorporateLine = function(ln, nextLn) {
!this.indented && // starts indented code blocks
!reMaybeSpecial.test(ln.slice(this.nextNonspace)) && // starts lists, block quotes, etc
(container.type !== "table" && container.type !== "table_row") && // start table rows
- (nextLn && !reMaybeDelimiterRow.test(nextLn.slice(this.nextNonspace))) // starts tables
+ !reMaybeDelimiterRow.test(ln.slice(this.nextNonspace)) // starts tables
) {
this.advanceNextNonspace();
break;
@@ -1156,10 +1244,6 @@ var incorporateLine = function(ln, nextLn) {
this.lastLineLength = ln.length;
};
-var skipNextLine = function() {
- this.shouldSkipNextLine = true;
-};
-
// Finalize a block. Close it and do any necessary postprocessing,
// e.g. creating string_content from strings, setting the 'tight'
// or 'loose' status of a list, and parsing the beginnings
@@ -1210,7 +1294,6 @@ var parse = function(input) {
this.column = 0;
this.lastMatchedContainer = this.doc;
this.currentLine = "";
- this.shouldSkipNextLine = false;
if (this.options.time) {
console.time("preparing input");
}
@@ -1227,11 +1310,7 @@ var parse = function(input) {
console.time("block parsing");
}
for (var i = 0; i < len; i++) {
- if (this.shouldSkipNextLine) {
- this.shouldSkipNextLine = false;
- continue;
- }
- this.incorporateLine(lines[i], lines[i + 1]);
+ this.incorporateLine(lines[i]);
}
while (this.tip) {
this.finalize(this.tip, len);
@@ -1283,8 +1362,8 @@ function Parser(options) {
advanceNextNonspace: advanceNextNonspace,
addLine: addLine,
addChild: addChild,
+ changeTipType: changeTipType,
incorporateLine: incorporateLine,
- skipNextLine: skipNextLine,
finalize: finalize,
processInlines: processInlines,
closeUnmatchedBlocks: closeUnmatchedBlocks,
diff --git a/test/tables.txt b/test/tables.txt
index 7d5270c0..7b6cf8dc 100644
--- a/test/tables.txt
+++ b/test/tables.txt
@@ -1205,6 +1205,322 @@ Here's a link to [Freedom Planet 2][].
````````````````````````````````
+### Tables inside of blockquotes
+
+This is a table in a block quote with each line starting with an angle bracket.
+
+```````````````````````````````` example
+> | aaa | bbb |
+> | --- | --- |
+> | ccc | ddd |
+> | eee | fff |
+.
+
+
+
+
+aaa |
+bbb |
+
+
+
+
+ccc |
+ddd |
+
+
+eee |
+fff |
+
+
+````````````````````````````````
+
+Tables can be alongside paragraphs in block quotes.
+
+```````````````````````````````` example
+> This is the text at the start
+>
+> | aaa | bbb |
+> | --- | --- |
+> | ccc | ddd |
+> | eee | fff |
+>
+> This is the text in the middle
+>
+> | aaa | bbb |
+> | --- | --- |
+> | ccc | ddd |
+> | eee | fff |
+>
+> This is the text at the end
+.
+
+This is the text at the start
+
+
+
+aaa |
+bbb |
+
+
+
+
+ccc |
+ddd |
+
+
+eee |
+fff |
+
+This is the text in the middle
+
+
+
+aaa |
+bbb |
+
+
+
+
+ccc |
+ddd |
+
+
+eee |
+fff |
+
+This is the text at the end
+
+````````````````````````````````
+
+At time of writing, GitHub requires that parts of the table in a quote need to
+start with an angle bracket. Any part that isn't following an angle bracket
+will be treated as plain text.
+
+```````````````````````````````` example
+> | aaa | bbb |
+| --- | --- |
+| ccc | ddd |
+| eee | fff |
+.
+
+| aaa | bbb |
+| --- | --- |
+| ccc | ddd |
+| eee | fff |
+
+````````````````````````````````
+
+```````````````````````````````` example
+> | aaa | bbb |
+ | --- | --- |
+ | ccc | ddd |
+ | eee | fff |
+.
+
+| aaa | bbb |
+| --- | --- |
+| ccc | ddd |
+| eee | fff |
+
+````````````````````````````````
+
+Once the first two rows are after angle brackets, the rest will be a plain text
+outside of the block quote
+
+```````````````````````````````` example
+> | aaa | bbb |
+> | --- | --- |
+| ccc | ddd |
+| eee | fff |
+.
+
+
+
+| ccc | ddd |
+| eee | fff |
+````````````````````````````````
+
+```````````````````````````````` example
+> | aaa | bbb |
+> | --- | --- |
+ | ccc | ddd |
+ | eee | fff |
+.
+
+
+
+| ccc | ddd |
+| eee | fff |
+````````````````````````````````
+
+```````````````````````````````` example
+> | aaa | bbb |
+> | --- | --- |
+> | ccc | ddd |
+ | eee | fff |
+.
+
+
+
+
+aaa |
+bbb |
+
+
+
+
+ccc |
+ddd |
+
+
+| eee | fff |
+````````````````````````````````
+
+### Tables inside of lists
+
+Tables can be inside of lists as long as each row of the table is indented enough.
+
+```````````````````````````````` example
+- | aaa | bbb |
+ | --- | --- |
+ | ccc | ddd |
+ | eee | fff |
+.
+
+-
+
+
+
+aaa |
+bbb |
+
+
+
+
+ccc |
+ddd |
+
+
+eee |
+fff |
+
+
+
+````````````````````````````````
+
+```````````````````````````````` example
+ - | aaa | bbb |
+ | --- | --- |
+ | ccc | ddd |
+ | eee | fff |
+.
+
+-
+
+
+
+aaa |
+bbb |
+
+
+
+
+ccc |
+ddd |
+
+
+eee |
+fff |
+
+
+
+````````````````````````````````
+
+```````````````````````````````` example
+1. | aaa | bbb |
+ | --- | --- |
+ | ccc | ddd |
+ | eee | fff |
+.
+
+-
+
+
+
+aaa |
+bbb |
+
+
+
+
+ccc |
+ddd |
+
+
+eee |
+fff |
+
+
+
+````````````````````````````````
+
+These tables aren't indented far enough.
+
+```````````````````````````````` example
+- | aaa | bbb |
+ | --- | --- |
+ | ccc | ddd |
+ | eee | fff |
+.
+
+- | aaa | bbb |
+| --- | --- |
+| ccc | ddd |
+| eee | fff |
+
+````````````````````````````````
+
+```````````````````````````````` example
+ - | aaa | bbb |
+| --- | --- |
+| ccc | ddd |
+| eee | fff |
+.
+
+| --- | --- |
+| ccc | ddd |
+| eee | fff |
+````````````````````````````````
+
+```````````````````````````````` example
+1. | aaa | bbb |
+ | --- | --- |
+ | ccc | ddd |
+ | eee | fff |
+.
+
+- | aaa | bbb |
+| --- | --- |
+| ccc | ddd |
+| eee | fff |
+
+````````````````````````````````
+
### Invalid table (MM-55972/MM-59809)
This table is invalid because of the space in the middle of the third column
@@ -1246,3 +1562,17 @@ dashes in it (`| |`).
| 파타 | 69.3 | | 보험 | (58.7) | 여수신‧금투 | (10.4) | 16 | |
| 하하 | 51.7 | | 금투 | (43.7) | 여수신 | (6.6) | 40 | |