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 主题(亮 / 暗 / 跟随系统)