feat: tables support (#2573)

* Tables support

* Linter fixes

* Regex Redos fix

* Fix empty row and variables proper naming

* Default cell style

* Now unncessary

* Support rows without a starting pipe char

* Striped rows

* Fix parsing issues

* Support tabs in separators
This commit is contained in:
Arnaud Brochard 2023-12-07 15:49:49 +01:00 committed by GitHub
parent ed190cd41e
commit 787cf2a9fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 83 additions and 0 deletions

View File

@ -0,0 +1,81 @@
import { CSSProperties } from "react";
import { inlineElementParserList } from ".";
import { marked } from "..";
import { matcher } from "../matcher";
class TableRegExp extends RegExp {
[Symbol.match](str: string): RegExpMatchArray | null {
const result = RegExp.prototype[Symbol.match].call(this, str);
// regex will only be considered valid if headers and delimiters column count matches
if (!result || splitPipeDelimiter(result[1]).length != splitPipeDelimiter(result[2]).length) {
return null;
}
return result;
}
}
export const TABLE_REG = new TableRegExp(/^([^\n|]*\|[^\n]*)\n([ \t:-]*(?<!\\)\|[ \t:|-]*)((?:\n[^\n|]*\|[^\n]*)+)/);
const splitPipeDelimiter = (rawStr: string) => {
// loose pipe delimiter for markdown tables. escaped pipes are supported. some examples:
// | aaaa | bbbb | cc\|cc | => ["aaaa", "bbbb", "cc|cc"]
// aaaa | bbbb | cc\|cc => ["aaaa", "bbbb", "cc|cc"]
// |a|f => ["a", "f"]
// ||a|f| => ["", "a", "f"]
// |||| => ["", "", ""]
// |\||\||\|| => ["|", "|", "|"]
return (
rawStr
.replaceAll(/(?<!\\)\|/g, "| ")
.trim()
.match(/(?:\\\||[^|])+/g) || []
).map((cell) => cell.replaceAll("\\|", "|").trim());
// TODO: Need to move backslash escaping (to PlainText ?) for all characters
// described in markdown spec (\`*_{}[]()#+-.!), and not just the pipe symbol here
};
const renderer = (rawStr: string) => {
const matchResult = matcher(rawStr, TABLE_REG);
if (!matchResult) {
return rawStr;
}
const headerContents = splitPipeDelimiter(matchResult[1]);
const cellStyles: CSSProperties[] = splitPipeDelimiter(matchResult[2]).map((cell) => {
const left = cell.startsWith(":");
const right = cell.endsWith(":");
// github markdown spec says that by default, content is left aligned
return {
textAlign: left && right ? "center" : right ? "right" : "left",
};
});
const rowContents = matchResult[3].substring(1).split(/\r?\n/).map(splitPipeDelimiter);
return (
<table>
<thead>
<tr>
{headerContents.map((header, index) => (
<th key={index}>{marked(header, [], inlineElementParserList)}</th>
))}
</tr>
</thead>
<tbody>
{rowContents.map((row, rowIndex) => (
<tr key={rowIndex} className="dark:even:bg-zinc-600 even:bg-zinc-100">
{headerContents.map((_, cellIndex) => (
<td key={cellIndex} style={cellStyles[cellIndex]}>
{cellIndex < row.length ? marked(row[cellIndex], [], inlineElementParserList) : null}
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default {
name: "table",
regexp: TABLE_REG,
renderer,
};

View File

@ -17,6 +17,7 @@ import Paragraph from "./Paragraph";
import PlainLink from "./PlainLink";
import PlainText from "./PlainText";
import Strikethrough from "./Strikethrough";
import Table from "./Table";
import Tag from "./Tag";
import TodoList from "./TodoList";
import UnorderedList from "./UnorderedList";
@ -31,6 +32,7 @@ export const blockElementParserList = [
Br,
CodeBlock,
Blockquote,
Table,
Heading,
TodoList,
DoneList,