From f4d408e261cb8f614e760bbdbee42f33d50e00cd Mon Sep 17 00:00:00 2001 From: Dustin Hagemeier Date: Mon, 3 Apr 2017 17:07:02 +0200 Subject: [PATCH 1/6] feat(table): add table --- src/index.js | 177 ++++++++++++++++++++++++++------------------------ test/index.js | 47 +++++++++----- 2 files changed, 123 insertions(+), 101 deletions(-) diff --git a/src/index.js b/src/index.js index fd8a678..35a9957 100644 --- a/src/index.js +++ b/src/index.js @@ -1,107 +1,114 @@ const TAGS = { - '' : ['',''], - _ : ['',''], - '\n' : ['
'], - ' ' : ['
'], - '-': ['
'] + '' : ['',''], + _ : ['',''], + '\n' : ['
'], + ' ' : ['
'], + '-': ['
'] }; /** Outdent a string based on the first indented line's leading whitespace * @private */ function outdent(str) { - return str.replace(RegExp('^'+(str.match(/^(\t| )+/) || '')[0], 'gm'), ''); + return str.replace(RegExp('^'+(str.match(/^(\t| )+/) || '')[0], 'gm'), ''); } /** Encode special attribute characters to HTML entities in a String. * @private */ function encodeAttr(str) { - return (str+'').replace(/"/g, '"').replace(//g, '>'); + return (str+'').replace(/"/g, '"').replace(//g, '>'); } /** Parse Markdown into an HTML String. */ export default function parse(md) { - let tokenizer = /((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^```(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:\!\[([^\]]*?)\]\(([^\)]+?)\))|(\[)|(\](?:\(([^\)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(\-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,3})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*])/gm, - context = [], - out = '', - last = 0, - links = {}, - chunk, prev, token, inner, t; + let tokenizer = /((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^```(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:\!\[([^\]]*?)\]\(([^\)]+?)\))|(\[)|(\](?:\(([^\)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(\-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,3})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*])/gm, + context = [], + out = '', + last = 0, + links = {}, + chunk, prev, token, inner, t; - function tag(token) { - var desc = TAGS[token.replace(/\*/g,'_')[1] || ''], - end = context[context.length-1]==token; - if (!desc) return token; - if (!desc[1]) return desc[0]; - context[end?'pop':'push'](token); - return desc[end|0]; - } + function tag(token) { + var desc = TAGS[token.replace(/\*/g,'_')[1] || ''], + end = context[context.length-1]==token; + if (!desc) return token; + if (!desc[1]) return desc[0]; + context[end?'pop':'push'](token); + return desc[end|0]; + } - function flush() { - let str = ''; - while (context.length) str += tag(context[context.length-1]); - return str; - } + function flush() { + let str = ''; + while (context.length) str += tag(context[context.length-1]); + return str; + } - md = md.replace(/^\n+|\n+$/g, '').replace(/^\[(.+?)\]:\s*(.+)$/gm, (s, name, url) => { - links[name.toLowerCase()] = url; - return ''; - }); + md = md.replace(/^\n+|\n+$/g, '').replace(/^\[(.+?)\]:\s*(.+)$/gm, (s, name, url) => { + links[name.toLowerCase()] = url; + return ''; + }); - while ( (token=tokenizer.exec(md)) ) { - prev = md.substring(last, token.index); - last = tokenizer.lastIndex; - chunk = token[0]; - if (prev.match(/[^\\](\\\\)*\\$/)) { - // escaped - } - // Code/Indent blocks: - else if (token[3] || token[4]) { - chunk = '
'+outdent(encodeAttr(token[3] || token[4]).replace(/^\n+|\n+$/g, ''))+'
'; - } - // > Quotes, -* lists: - else if (token[6]) { - t = token[6]; - if (t.match(/\./)) { - token[5] = token[5].replace(/^\d+/gm, ''); - } - inner = parse(outdent(token[5].replace(/^\s*[>*+.-]/gm, ''))); - if (t==='>') t = 'blockquote'; - else { - t = t.match(/\./) ? 'ol' : 'ul'; - inner = inner.replace(/^(.*)(\n|$)/gm, '
  • $1
  • '); - } - chunk = '<'+t+'>' + inner + ''; - } - // Images: - else if (token[8]) { - chunk = `${encodeAttr(token[7])}`; - } - // Links: - else if (token[10]) { - out = out.replace('', ``); - chunk = flush() + ''; - } - else if (token[9]) { - chunk = ''; - } - // Headings: - else if (token[12] || token[14]) { - t = 'h' + (token[14] ? token[14].length : (token[13][0]==='='?1:2)); - chunk = '<'+t+'>' + parse(token[12] || token[15]) + ''; - } - // `code`: - else if (token[16]) { - chunk = ''+encodeAttr(token[16])+''; - } - // Inline formatting: *em*, **strong** & friends - else if (token[17] || token[1]) { - chunk = tag(token[17] || '--'); - } - out += prev; - out += chunk; - } + while ( (token=tokenizer.exec(md)) ) { + prev = md.substring(last, token.index); + last = tokenizer.lastIndex; + chunk = token[0]; + if (prev.match(/[^\\](\\\\)*\\$/)) { + // escaped + } + // Code/Indent blocks: + else if (token[3] || token[4]) { + chunk = '
    '+outdent(encodeAttr(token[3] || token[4]).replace(/^\n+|\n+$/g, ''))+'
    '; + } + // > Quotes, -* lists: + else if (token[6]) { + t = token[6]; + if (t.match(/\./)) { + token[5] = token[5].replace(/^\d+/gm, ''); + } + inner = parse(outdent(token[5].replace(/^\s*[>*+.-]/gm, ''))); + if (t==='>') t = 'blockquote'; + else { + t = t.match(/\./) ? 'ol' : 'ul'; + inner = inner.replace(/^(.*)(\n|$)/gm, '
  • $1
  • '); + } + chunk = '<'+t+'>' + inner + ''; + } + // Images: + else if (token[8]) { + chunk = `${encodeAttr(token[7])}`; + } + // Links: + else if (token[10]) { + out = out.replace('
    ', ``); + chunk = flush() + ''; + } + else if (token[9]) { + chunk = ''; + } + // Headings: + else if (token[12] || token[14]) { + t = 'h' + (token[14] ? token[14].length : (token[13][0]==='='?1:2)); + chunk = '<'+t+'>' + parse(token[12] || token[15]) + ''; + } + // `code`: + else if (token[16]) { + chunk = ''+encodeAttr(token[16])+''; + } + // Inline formatting: *em*, **strong** & friends + else if (token[17] || token[1]) { + chunk = tag(token[17] || '--'); + } + // Table parser + else if (token[18]) { + var tr = (a, r) => ('' + a.split('|').reduce((b, v) => b + (v ? ('<' + r + v.trim() + ''), + h = token[19] ? tr(token[19], token[20] ? 'th>' : 'td>') : '', + c = token[21] ? token[21].split('\n').reduce((a, v) => a + (v ? tr(v, 'td>') : '')) : ''; + chunk = '' + h + c + '
    '; + } + out += prev; + out += chunk; + } - return (out + md.substring(last) + flush()).trim(); -} + return (out + md.substring(last) + flush()).trim(); +} \ No newline at end of file diff --git a/test/index.js b/test/index.js index d57e8e3..6716828 100644 --- a/test/index.js +++ b/test/index.js @@ -141,20 +141,35 @@ describe('snarkdown()', () => { }); }); - describe('edge cases', () => { - it('should close unclosed tags', () => { - expect(snarkdown('*foo')).to.equal('foo'); - expect(snarkdown('foo**')).to.equal('foo'); - expect(snarkdown('[some **bold text](#winning)')).to.equal('
    some bold text'); - expect(snarkdown('`foo')).to.equal('`foo'); - }); - - it('should not choke on single characters', () => { - expect(snarkdown('*')).to.equal(''); - expect(snarkdown('_')).to.equal(''); - expect(snarkdown('**')).to.equal(''); - expect(snarkdown('>')).to.equal('>'); - expect(snarkdown('`')).to.equal('`'); - }); - }); + describe('edge cases', () => { + it('should close unclosed tags', () => { + expect(snarkdown('*foo')).to.equal('foo'); + expect(snarkdown('foo**')).to.equal('foo'); + expect(snarkdown('[some **bold text](#winning)')).to.equal('some bold text'); + expect(snarkdown('`foo')).to.equal('`foo'); + }); + + it('should not choke on single characters', () => { + expect(snarkdown('*')).to.equal(''); + expect(snarkdown('_')).to.equal(''); + expect(snarkdown('**')).to.equal(''); + expect(snarkdown('>')).to.equal('>'); + expect(snarkdown('`')).to.equal('`'); + }); + }); + + describe('tables', () => { + it('should parse content', () => { + expect(snarkdown('| a | hallo welt | c |')).to.equal('
    ahallo weltc
    '); + expect(snarkdown('| a | b |')).to.equal('
    ab
    '); + expect(snarkdown('| a | b \n| c | d')).to.equal('
    ab
    cd
    '); + expect(snarkdown('| a | b \n| c | d \n| e | f')).to.equal('
    ab
    cd
    ef
    '); + expect(snarkdown('| a')).to.equal('
    a
    '); + }); + + it('should parse header', () => { + expect(snarkdown('| a | hallo welt | c |\n| ---')).to.equal('
    ahallo weltc
    '); + expect(snarkdown('| a | b \n| --- | --- \n| e | f')).to.equal('
    ab
    ef
    '); + }); + }); }); From 84bc14002a4768e29240b8ad0088e2100e5a03bb Mon Sep 17 00:00:00 2001 From: Dustin Hagemeier Date: Mon, 3 Apr 2017 23:46:59 +0200 Subject: [PATCH 2/6] chore(whitespace): space to tab --- src/index.js | 14 +++++------ test/index.js | 68 +++++++++++++++++++++++++-------------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/index.js b/src/index.js index 0b5052b..9a9dc6b 100644 --- a/src/index.js +++ b/src/index.js @@ -99,13 +99,13 @@ export default function parse(md, prevLinks) { else if (token[17] || token[1]) { chunk = tag(token[17] || '--'); } - // Table parser - else if (token[18]) { - var tr = (a, r) => ('' + a.split('|').reduce((b, v) => b + (v ? ('<' + r + v.trim() + ''), - h = token[19] ? tr(token[19], token[20] ? 'th>' : 'td>') : '', - c = token[21] ? token[21].split('\n').reduce((a, v) => a + (v ? tr(v, 'td>') : '')) : ''; - chunk = '' + h + c + '
    '; - } + // Table parser + else if (token[18]) { + var tr = (a, r) => ('' + a.split('|').reduce((b, v) => b + (v ? ('<' + r + v.trim() + ''), + h = token[19] ? tr(token[19], token[20] ? 'th>' : 'td>') : '', + c = token[21] ? token[21].split('\n').reduce((a, v) => a + (v ? tr(v, 'td>') : '')) : ''; + chunk = '' + h + c + '
    '; + } out += prev; out += chunk; } diff --git a/test/index.js b/test/index.js index 0c5e42b..a880a65 100644 --- a/test/index.js +++ b/test/index.js @@ -67,9 +67,9 @@ describe('snarkdown()', () => { expect(snarkdown('\nhello [World]!\n[world]: http://world.com')).to.equal('hello World!'); }); - it('parses reference links without creating excessive linebreaks', () => { - expect(snarkdown('\nhello [World]!\n\n[world]: http://world.com')).to.equal('hello World!'); - }); + it('parses reference links without creating excessive linebreaks', () => { + expect(snarkdown('\nhello [World]!\n\n[world]: http://world.com')).to.equal('hello World!'); + }); }); describe('lists', () => { @@ -151,35 +151,35 @@ describe('snarkdown()', () => { }); }); - describe('edge cases', () => { - it('should close unclosed tags', () => { - expect(snarkdown('*foo')).to.equal('foo'); - expect(snarkdown('foo**')).to.equal('foo'); - expect(snarkdown('[some **bold text](#winning)')).to.equal('some bold text'); - expect(snarkdown('`foo')).to.equal('`foo'); - }); - - it('should not choke on single characters', () => { - expect(snarkdown('*')).to.equal(''); - expect(snarkdown('_')).to.equal(''); - expect(snarkdown('**')).to.equal(''); - expect(snarkdown('>')).to.equal('>'); - expect(snarkdown('`')).to.equal('`'); - }); - }); - - describe('tables', () => { - it('should parse content', () => { - expect(snarkdown('| a | hallo welt | c |')).to.equal('
    ahallo weltc
    '); - expect(snarkdown('| a | b |')).to.equal('
    ab
    '); - expect(snarkdown('| a | b \n| c | d')).to.equal('
    ab
    cd
    '); - expect(snarkdown('| a | b \n| c | d \n| e | f')).to.equal('
    ab
    cd
    ef
    '); - expect(snarkdown('| a')).to.equal('
    a
    '); - }); - - it('should parse header', () => { - expect(snarkdown('| a | hallo welt | c |\n| ---')).to.equal('
    ahallo weltc
    '); - expect(snarkdown('| a | b \n| --- | --- \n| e | f')).to.equal('
    ab
    ef
    '); - }); - }); + describe('edge cases', () => { + it('should close unclosed tags', () => { + expect(snarkdown('*foo')).to.equal('foo'); + expect(snarkdown('foo**')).to.equal('foo'); + expect(snarkdown('[some **bold text](#winning)')).to.equal('some bold text'); + expect(snarkdown('`foo')).to.equal('`foo'); + }); + + it('should not choke on single characters', () => { + expect(snarkdown('*')).to.equal(''); + expect(snarkdown('_')).to.equal(''); + expect(snarkdown('**')).to.equal(''); + expect(snarkdown('>')).to.equal('>'); + expect(snarkdown('`')).to.equal('`'); + }); + }); + + describe('tables', () => { + it('should parse content', () => { + expect(snarkdown('| a | hallo welt | c |')).to.equal('
    ahallo weltc
    '); + expect(snarkdown('| a | b |')).to.equal('
    ab
    '); + expect(snarkdown('| a | b \n| c | d')).to.equal('
    ab
    cd
    '); + expect(snarkdown('| a | b \n| c | d \n| e | f')).to.equal('
    ab
    cd
    ef
    '); + expect(snarkdown('| a')).to.equal('
    a
    '); + }); + + it('should parse header', () => { + expect(snarkdown('| a | hallo welt | c |\n| ---')).to.equal('
    ahallo weltc
    '); + expect(snarkdown('| a | b \n| --- | --- \n| e | f')).to.equal('
    ab
    ef
    '); + }); + }); }); From 7430ef68b866e979222a8eb01d4da9329ffece2e Mon Sep 17 00:00:00 2001 From: Dustin Hagemeier Date: Mon, 3 Apr 2017 23:54:00 +0200 Subject: [PATCH 3/6] fix(tokenizer): fixed merge fail --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 9a9dc6b..f3c4957 100644 --- a/src/index.js +++ b/src/index.js @@ -22,7 +22,7 @@ function encodeAttr(str) { /** Parse Markdown into an HTML String. */ export default function parse(md, prevLinks) { - let tokenizer = /((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^```(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:\!\[([^\]]*?)\]\(([^\)]+?)\))|(\[)|(\](?:\(([^\)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(\-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,3})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*])/gm, + let tokenizer = /((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^```(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:\!\[([^\]]*?)\]\(([^\)]+?)\))|(\[)|(\](?:\(([^\)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(\-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,3})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*])|(((?:^|\n+)\|.*)(\n\|\s+---+.*)?((?:\n\|\s+.*)*))/gm, context = [], out = '', links = prevLinks || {}, From 14bc7dfa211897a362e70b43f8c2d20b41161b30 Mon Sep 17 00:00:00 2001 From: Dustin Hagemeier Date: Tue, 4 Apr 2017 18:30:13 +0200 Subject: [PATCH 4/6] feat(table): add inline parsing --- src/index.js | 4 ++-- test/index.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index f3c4957..eec2807 100644 --- a/src/index.js +++ b/src/index.js @@ -101,10 +101,10 @@ export default function parse(md, prevLinks) { } // Table parser else if (token[18]) { - var tr = (a, r) => ('' + a.split('|').reduce((b, v) => b + (v ? ('<' + r + v.trim() + ''), + var tr = (a, r) => `${a.split(/\|\s*/).reduce((b, v) => b + (v ? `<${r+parse(v)}`, h = token[19] ? tr(token[19], token[20] ? 'th>' : 'td>') : '', c = token[21] ? token[21].split('\n').reduce((a, v) => a + (v ? tr(v, 'td>') : '')) : ''; - chunk = '' + h + c + '
    '; + chunk = `${h+c}
    `; } out += prev; out += chunk; diff --git a/test/index.js b/test/index.js index a880a65..f9b92c8 100644 --- a/test/index.js +++ b/test/index.js @@ -171,9 +171,9 @@ describe('snarkdown()', () => { describe('tables', () => { it('should parse content', () => { expect(snarkdown('| a | hallo welt | c |')).to.equal('
    ahallo weltc
    '); - expect(snarkdown('| a | b |')).to.equal('
    ab
    '); + expect(snarkdown('| a | b |')).to.equal('
    ab
    '); expect(snarkdown('| a | b \n| c | d')).to.equal('
    ab
    cd
    '); - expect(snarkdown('| a | b \n| c | d \n| e | f')).to.equal('
    ab
    cd
    ef
    '); + expect(snarkdown('| a | b \n| c | d \n| e | f')).to.equal('
    ab
    cd
    ef
    '); expect(snarkdown('| a')).to.equal('
    a
    '); }); @@ -181,5 +181,11 @@ describe('snarkdown()', () => { expect(snarkdown('| a | hallo welt | c |\n| ---')).to.equal('
    ahallo weltc
    '); expect(snarkdown('| a | b \n| --- | --- \n| e | f')).to.equal('
    ab
    ef
    '); }); + + it('should allow inline styles', () => { + expect(snarkdown('| [Example](#example) | **strong** |')).to.equal('
    Examplestrong
    '); + expect(snarkdown('| a | # hallo welt | c |\n| ---')).to.equal('
    a

    hallo welt

    c
    '); + expect(snarkdown('| [some **bold text](#winning) | b \n| --- | --- \n| > To be or not to be | f')).to.equal('
    some bold textb
    To be or not to be
    f
    '); + }); }); }); From d1189697b4a3a8a63a998b9d3e13cc767c90cca4 Mon Sep 17 00:00:00 2001 From: Dustin Hagemeier Date: Wed, 5 Apr 2017 10:53:21 +0200 Subject: [PATCH 5/6] feat(table): remove reduce --- src/index.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index eec2807..03fd66a 100644 --- a/src/index.js +++ b/src/index.js @@ -22,7 +22,7 @@ function encodeAttr(str) { /** Parse Markdown into an HTML String. */ export default function parse(md, prevLinks) { - let tokenizer = /((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^```(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:\!\[([^\]]*?)\]\(([^\)]+?)\))|(\[)|(\](?:\(([^\)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(\-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,3})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*])|(((?:^|\n+)\|.*)(\n\|\s+---+.*)?((?:\n\|\s+.*)*))/gm, + let tokenizer = /((?:^|\n+)(?:\n---+|\* \*(?: \*)+)\n)|(?:^```(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t| {2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:\!\[([^\]]*?)\]\(([^\)]+?)\))|(\[)|(\](?:\(([^\)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(\-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,3})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|( \n\n*|\n{2,}|__|\*\*|[_*])|((?:(?:^|\n+)(?:\|.*))+)/gm, context = [], out = '', links = prevLinks || {}, @@ -101,10 +101,25 @@ export default function parse(md, prevLinks) { } // Table parser else if (token[18]) { - var tr = (a, r) => `${a.split(/\|\s*/).reduce((b, v) => b + (v ? `<${r+parse(v)}`, - h = token[19] ? tr(token[19], token[20] ? 'th>' : 'td>') : '', - c = token[21] ? token[21].split('\n').reduce((a, v) => a + (v ? tr(v, 'td>') : '')) : ''; - chunk = `${h+c}
    `; + var l = token[18].split('\n'), + i = l.length, + table = '', + r = 'td>'; + while ( i-- ) { + if(l[i].match(/^\|\s+---+.*$/)) { + r = 'th>'; + continue; + } + var c = l[i].split(/\|\s*/), + j = c.length, + tr = ''; + while ( j-- ) { + tr = (c[j] ? `<${r+parse(c[j])}${tr}` + table; + r = 'td>'; + } + chunk = `${table}
    `; } out += prev; out += chunk; From 186ab704d6b29e2515b9c58534d4c32d5d43004e Mon Sep 17 00:00:00 2001 From: Dustin Hagemeier Date: Mon, 23 Apr 2018 13:43:23 +0200 Subject: [PATCH 6/6] feat(table): remove unnecessary spaces --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 03fd66a..03b74b7 100644 --- a/src/index.js +++ b/src/index.js @@ -113,7 +113,7 @@ export default function parse(md, prevLinks) { var c = l[i].split(/\|\s*/), j = c.length, tr = ''; - while ( j-- ) { + while (j--) { tr = (c[j] ? `<${r+parse(c[j])}${tr}` + table;