Skip to content

Commit d38e757

Browse files
authored
fix(tables): only escape pipes outside SB context (#1292)
* fix(tables): only escape pipes outside SB context Fixes #943 * fix(tables): detect command context in tables Fixes #1016 * Added tests for parsing tables
1 parent e93bff7 commit d38e757

File tree

3 files changed

+96
-7
lines changed

3 files changed

+96
-7
lines changed

common/markdown_parser/parser.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,43 @@ Deno.test("Test NakedURL parser", () => {
240240
"http://abc.com?e=2.71",
241241
]);
242242
});
243+
244+
const tableSample = `
245+
| Header A | Header B |
246+
|----------|----------|
247+
| [[Wiki|Alias]] | 1B |
248+
| 2A | 2B |
249+
| 3A | {[My: Command|Alias]("args", 1)} |
250+
`;
251+
252+
Deno.test("Test table parser", () => {
253+
const tree = parseMarkdown(tableSample);
254+
const cells = collectNodesOfType(tree, "TableCell");
255+
256+
assertEquals(cells.map((x) => x.children![0].text), [
257+
"Header A",
258+
"Header B",
259+
undefined,
260+
"1B",
261+
"2A",
262+
"2B",
263+
"3A",
264+
undefined,
265+
]);
266+
267+
// Check the Wiki Link - Make sure no backslash has been added (issue 943)
268+
assertEquals(cells[2].children![0].type, "WikiLink");
269+
const wikiName = findNodeOfType(cells[2], "WikiLinkPage");
270+
const wikiAlias = findNodeOfType(cells[2], "WikiLinkAlias");
271+
assertEquals(wikiName!.children![0].text, "Wiki");
272+
assertEquals(wikiAlias!.children![0].text, "Alias");
273+
274+
// Check the Command Link - Make sure no backslash has been added (issue 1016)
275+
assertEquals(cells[7].children![0].type, "CommandLink");
276+
const commandName = findNodeOfType(cells[7], "CommandLinkName");
277+
const commandArgs = findNodeOfType(cells[7], "CommandLinkArgs");
278+
const commandAlias = findNodeOfType(cells[7], "CommandLinkAlias");
279+
assertEquals(commandName!.children![0].text, "My: Command");
280+
assertEquals(commandAlias!.children![0].text, "Alias");
281+
assertEquals(commandArgs!.children![0].text, '"args", 1');
282+
});

common/markdown_parser/table_parser.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { tags as t } from "@lezer/highlight";
1111
// Forked from https://github.com/lezer-parser/markdown/blob/main/src/extension.ts
1212
// MIT License
1313
// Author: Marijn Haverbeke
14-
// Change made: Avoid wiki links with aliases [[link|alias]] from being parsed as table row separators
14+
// Change made: Avoid wiki links and commands with aliases [[link|alias]], {[My: Command|alias]}
15+
// from being parsed as table row separators
1516

1617
function parseRow(
1718
cx: BlockContext,
@@ -36,16 +37,29 @@ function parseRow(
3637
};
3738

3839
let inWikilink = false;
40+
let inCommandButton = false;
3941
for (let i = startI; i < line.length; i++) {
4042
const next = line.charCodeAt(i);
41-
if (next === 91 /* '[' */ && line.charAt(i + 1) === "[") {
43+
44+
if (line[i] === "[" && line.charAt(i + 1) === "[") {
4245
inWikilink = true;
43-
} else if (
44-
next === 93 /* ']' */ && line.charAt(i - 1) === "]" && inWikilink
45-
) {
46+
} else if (line[i] === "]" && line.charAt(i - 1) === "]" && inWikilink) {
4647
inWikilink = false;
4748
}
48-
if (next == 124 /* '|' */ && !esc && !inWikilink) {
49+
50+
if (line[i] === "{" && line.charAt(i + 1) === "[") {
51+
inCommandButton = true;
52+
} else if (inCommandButton) {
53+
if (line[i] === "}" && line.charAt(i - 1) === "]") {
54+
// Command button without arguments: {[My Command: BeepBoop|Alias]}
55+
inCommandButton = false;
56+
} else if (line[i] === "}" && line.charAt(i - 1) === ")") {
57+
// Command button with arguments: {[My Command: BeepBoop|Alias]("args")}
58+
inCommandButton = false;
59+
}
60+
}
61+
62+
if (next == 124 /* '|' */ && !esc && !inWikilink && !inCommandButton) {
4963
if (!first || cellStart > -1) count++;
5064
first = false;
5165
if (elts) {

plugs/template/util.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,49 @@ export function defaultTransformer(v: any): Promise<string> {
88
return Promise.resolve("");
99
}
1010
if (typeof v === "string") {
11-
return Promise.resolve(v.replaceAll("\n", " ").replaceAll("|", "\\|"));
11+
return Promise.resolve(escapeRegularPipes(v.replaceAll("\n", " ")));
1212
}
1313
if (v && typeof v === "object") {
1414
return Promise.resolve(luaToString(v));
1515
}
1616
return Promise.resolve("" + v);
1717
}
1818

19+
/**
20+
* Escapes all pipes that would inadvertently delimit a markdown table column.
21+
* Does not escape columns that are used for aliasing in WikiLinks or Commands:
22+
* `[[WikiLink|Alias]]` and `{[Command: Name|Click Me!]("args")}`
23+
* @param s The text to replace
24+
* @returns The text where the pipes outside of silverbullet specific context is
25+
* replaced with an escaped pipe.
26+
*/
27+
function escapeRegularPipes(s: string) {
28+
let result = "";
29+
let isInWikiLink = false;
30+
let isInCommandButton = false;
31+
32+
for (let i = 0; i < s.length; i++) {
33+
if (s[i] == "[" && s[i + 1] == "[") {
34+
isInWikiLink = true;
35+
} else if (s[i] == "]" && s[i + 1] == "]" && isInWikiLink) {
36+
isInWikiLink = false;
37+
}
38+
if (s[i] == "{" && s[i + 1] == "[") {
39+
isInCommandButton = true;
40+
} else if (
41+
(s[i] == "]" || s[i] == ")") && s[i + 1] == "}" && isInCommandButton
42+
) {
43+
isInCommandButton = false;
44+
} else if (s[i] == "|" && !isInWikiLink && !isInCommandButton) {
45+
result += "\\";
46+
}
47+
48+
result += s[i];
49+
}
50+
51+
return result;
52+
}
53+
1954
// Nicely format an array of JSON objects as a Markdown table
2055
export async function jsonToMDTable(
2156
jsonArray: any[],

0 commit comments

Comments
 (0)