From 9277e2a331ba90cc9972ef30b4196713614b4c6a Mon Sep 17 00:00:00 2001 From: doyaGu Date: Sat, 3 Oct 2026 08:20:23 -0400 Subject: [PATCH] fix(export): escape backslashes in tag table cells A backslash right before a pipe, as in the tag name a\|b, turned the escaped pipe into "\|": the table splitter reads "\\" as an escaped backslash and the pipe as a column separator, shifting the row. Cells now escape '\' before '|'. Recorded in PLAN 5.8. --- YKanBan.Tests/Export/MarkdownExporterTests.cs | 23 +++++++++++++++++++ YKanBan/Export/MarkdownExporter.cs | 8 ++++--- docs/PLAN.md | 2 +- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/YKanBan.Tests/Export/MarkdownExporterTests.cs b/YKanBan.Tests/Export/MarkdownExporterTests.cs index 9d8e51a..646b1bd 100644 --- a/YKanBan.Tests/Export/MarkdownExporterTests.cs +++ b/YKanBan.Tests/Export/MarkdownExporterTests.cs @@ -204,6 +204,29 @@ public class MarkdownExporterTests } } + [TestMethod] + public void TagTableCellsEscapeBackslashesSoAPipeAfterOneStaysEscaped() + { + CultureInfo? original = Resources.Culture; + try + { + Resources.Culture = new CultureInfo("en"); + var tag = new TagModel(1, @"a\|b", RgbColor.Parse("#112233"), @"C:\temp\"); + string markdown = MarkdownExporter.Export( + "W", + [new ColumnModel(1, "C", string.Empty, 1, 1)], + [], + [(tag, 0L)], + ExportedAt); + + StringAssert.Contains(markdown, @"| a\\\|b | #112233 | C:\\temp\\ | 0 |"); + } + finally + { + Resources.Culture = original; + } + } + [TestMethod] [DataRow("plain", "`plain`")] [DataRow("a`b", "``a`b``")] diff --git a/YKanBan/Export/MarkdownExporter.cs b/YKanBan/Export/MarkdownExporter.cs index d2a99fa..32077dd 100644 --- a/YKanBan/Export/MarkdownExporter.cs +++ b/YKanBan/Export/MarkdownExporter.cs @@ -11,7 +11,7 @@ namespace YKanBan.Export; /// uses that one per-language pattern. The whole document uses LF line endings /// on every platform. Card bodies and column descriptions are emitted verbatim /// apart from line endings; column and card titles are collapsed to one line; -/// tag names become inline code with a long enough backtick fence; tag table cells escape '|' and turn line breaks into <br>. The +/// tag names become inline code with a long enough backtick fence; tag table cells escape '\' and '|' and turn line breaks into <br>. The /// output is meant for reading only — there is no re-import. /// public static class MarkdownExporter @@ -130,12 +130,14 @@ public static class MarkdownExporter } /// - /// Formats a table cell: escaped pipe characters, line breaks as <br>. + /// Formats a table cell: escaped backslashes and pipe characters, line breaks as <br>. /// /// The raw cell text. /// The escaped single-line text. private static string Cell(string text) => - text.Replace("|", "\\|").Replace("\r\n", "
").Replace("\r", "
").Replace("\n", "
"); + // Backslashes first: the table splitter reads "\\" as an escaped backslash, so the + // '|' after a literal backslash would otherwise end the cell. + text.Replace("\\", "\\\\").Replace("|", "\\|").Replace("\r\n", "
").Replace("\r", "
").Replace("\n", "
"); /// /// Renders text as an inline code span whose backtick fence is one longer than diff --git a/docs/PLAN.md b/docs/PLAN.md index 4e4c669..346a547 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -246,7 +246,7 @@ card_tags: (card_id, tag_id) 复合主键,双向 ON DELETE CASCADE - 空列也保留(体现看板结构);无标题卡片以 `### #61` 呈现 - 转义规则: - 标签名以行内代码呈现,定界反引号数量 = 名称内最长连续反引号串长度 + 1(名称以反引号开头/结尾时两侧加空格) - - 标签总表单元格内 `|` → `\|`,换行 → `
` + - 标签总表单元格内 `\` → `\\`(先于 `|` 处理,否则 `a\|b` 中的 `|` 会被当成列分隔符),`|` → `\|`,换行 → `
` - 卡片正文与列 description **原样输出**(接受其中 Markdown 结构干扰文档层级,已接受) ### 5.9 主题(亮 / 暗 / 跟随系统)