Skip to content

Commit

Permalink
Fix quantizor#258: Allow escaping pipes within tables
Browse files Browse the repository at this point in the history
Should fix quantizor#258

A port of ariabuckles/simple-markdown@f32a628
to markdown-to-jsx.

Previously, tables were parsed using String.prototype.split on pipe
(`|`) characters.

This commit changes table parsing to use a full parse() call, using a
new `tableSeparator` rule to match tableSeparators. Because
`tableSeparator` is now a rule, the `escapedText` rule can handle
escaped pipes within the table, and the `codeInline` rule can handle
inline code with pipes in it.

Test plan:

Added tests to index.compiler.spec.js
  • Loading branch information
ariabuckles committed Aug 5, 2019
1 parent 5d44703 commit aad86c8
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 24 deletions.
83 changes: 83 additions & 0 deletions index.compiler.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,89 @@ describe('GFM tables', () => {
`);
});

it('should handle escaped pipes inside a table', () => {
render(
compiler(
[
'| \\|Attribute\\| | \\|Type\\| |',
'| --------------- | ------------------ |',
'| pos\\|position | "left" \\| "right" |',
].join('\n')
)
);

expect(root.innerHTML).toMatchInlineSnapshot(`
<table data-reactroot>
<thead>
<tr>
<th>
|Attribute|
</th>
<th>
|Type|
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
pos|position
</td>
<td>
"left" | "right"
</td>
</tr>
</tbody>
</table>
`);
});

it('should handle pipes in code inside a table', () => {
render(
compiler(
[
'| Attribute | Type |',
'| ------------ | --------------------- |',
'| `position` | `"left" | "right"` |',
].join('\n')
)
);

expect(root.innerHTML).toMatchInlineSnapshot(`
<table data-reactroot>
<thead>
<tr>
<th>
Attribute
</th>
<th>
Type
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
position
</code>
</td>
<td>
<code>
"left" | "right"
</code>
</td>
</tr>
</tbody>
</table>
`);
});

});

describe('arbitrary HTML', () => {
Expand Down
74 changes: 50 additions & 24 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,12 @@ const REFERENCE_LINK_R = /^\[([^\]]*)\] ?\[([^\]]*)\]/;
const SQUARE_BRACKETS_R = /(\[|\])/g;
const SHOULD_RENDER_AS_BLOCK_R = /(\n|^[-*]\s|^#|^ {2,}|^-{2,}|^>\s)/;
const TAB_R = /\t/g;
const TABLE_SEPARATOR_R = /^ *\| */;
const TABLE_TRIM_PIPES = /(^ *\||\| *$)/g;
const TABLE_CELL_END_TRIM = / *$/;
const TABLE_CENTER_ALIGN = /^ *:-+: *$/;
const TABLE_LEFT_ALIGN = /^ *:-+ *$/;
const TABLE_RIGHT_ALIGN = /^ *-+: *$/;
const TABLE_ROW_SPLIT = / *\| */;

const TEXT_BOLD_R = /^([*_])\1((?:\[.*?\][([].*?[)\]]|<.*?>(?:.*?<.*?>)?|`.*?`|~+.*?~+|.)*?)\1\1(?!\1)/;
const TEXT_EMPHASIZED_R = /^([*_])((?:\[.*?\][([].*?[)\]]|<.*?>(?:.*?<.*?>)?|`.*?`|~+.*?~+|.)*?)\1(?!\1)/;
Expand Down Expand Up @@ -287,46 +288,56 @@ function parseTableAlignCapture(alignCapture) {
return null;
}

function parseTableHeader(capture, parse, state) {
const headerText = capture[1]
.replace(TABLE_TRIM_PIPES, '')
.trim()
.split(TABLE_ROW_SPLIT);

return headerText.map(function(text) {
return parse(text, state);
function parseTableRow(source, parse, state) {
const prevInTable = state.inTable;
state.inTable = true;
const tableRow = parse(source.trim(), state);
state.inTable = prevInTable;

let cells = [[]];
tableRow.forEach(function(node, i) {
if (node.type === 'tableSeparator') {
// Filter out empty table separators at the start/end:
if (i !== 0 && i !== tableRow.length - 1) {
// Split the current row:
cells.push([]);
}
} else {
if (node.type === 'text' && (
tableRow[i + 1] == null ||
tableRow[i + 1].type === 'tableSeparator'
)) {
node.content = node.content.replace(TABLE_CELL_END_TRIM, "");
}
cells[cells.length - 1].push(node);
}
});
return cells;
}

function parseTableAlign(capture /*, parse, state*/) {
const alignText = capture[2]
function parseTableAlign(source /*, parse, state*/) {
const alignText = source
.replace(TABLE_TRIM_PIPES, '')
.trim()
.split(TABLE_ROW_SPLIT);
.split('|');

return alignText.map(parseTableAlignCapture);
}

function parseTableCells(capture, parse, state) {
const rowsText = capture[3]
function parseTableCells(source, parse, state) {
const rowsText = source
.trim()
.split('\n');

return rowsText.map(function(rowText) {
return rowText
.replace(TABLE_TRIM_PIPES, '')
.split(TABLE_ROW_SPLIT)
.map(function(text) {
return parse(text.trim(), state);
});
return parseTableRow(rowText, parse, state);
});
}

function parseTable(capture, parse, state) {
state.inline = true;
const header = parseTableHeader(capture, parse, state);
const align = parseTableAlign(capture, parse, state);
const cells = parseTableCells(capture, parse, state);
const header = parseTableRow(capture[1], parse, state);
const align = parseTableAlign(capture[2], parse, state);
const cells = parseTableCells(capture[3], parse, state);
state.inline = false;

return {
Expand Down Expand Up @@ -1413,6 +1424,21 @@ export function compiler(markdown, options) {
},
},

tableSeparator: {
match: function(source, state) {
if (!state.inTable) {
return null;
}
return TABLE_SEPARATOR_R.exec(source);
},
order: PARSE_PRIORITY_HIGH,
parse: function() {
return { type: 'tableSeparator' };
},
// These shouldn't be reached, but in case they are, be reasonable:
react() { return ' | '; }
},

text: {
// Here we look for anything followed by non-symbols,
// double newlines, or double-space-newlines
Expand Down

0 comments on commit aad86c8

Please sign in to comment.