Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ All notable changes to this project will be documented here.
`name:` from `Tests` to `CI` for cross-repo consistency. README
badge URL updated to match.

### Fixed
- Pipe (`|`) and backslash (`\`) in table cell values are now escaped in the
`jira` and `confluence` output formats, preventing cell-boundary corruption
and accidental header syntax. Closes #17.

## [2.0.2] - 2026-04-05

### Added
Expand Down
11 changes: 9 additions & 2 deletions src/mcp_clipboard/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,11 @@ def _format_slack(rows: list[list[str]]) -> str:
return f"{header}\n```\n" + "\n".join(data_lines) + "\n```"


def _escape_jira_cell(cell: str) -> str:
"""Escape characters that break Jira/Confluence wiki markup tables."""
return cell.replace("\\", "\\\\").replace("|", "\\|")


def _format_jira(rows: list[list[str]]) -> str:
"""Render rows as Jira/Confluence wiki markup: ||Header|| / |Cell| syntax."""
if not rows:
Expand All @@ -440,8 +445,10 @@ def _format_jira(rows: list[list[str]]) -> str:
max_cols = max(len(row) for row in rows)
normalized = [row + [""] * (max_cols - len(row)) for row in rows]

lines = ["||" + "||".join(normalized[0]) + "||"]
for row in normalized[1:]:
escaped = [[_escape_jira_cell(cell) for cell in row] for row in normalized]

lines = ["||" + "||".join(escaped[0]) + "||"]
for row in escaped[1:]:
lines.append("|" + "|".join(row) + "|")

return "\n".join(lines)
Expand Down
61 changes: 61 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,3 +545,64 @@ def test_format_notion_escapes_pipe():
notion_result = format_table(rows, "notion")
assert md_result == notion_result
assert r"x\|y" in notion_result


# ---------------------------------------------------------------------------
# Jira/Confluence escaping (#17)
# ---------------------------------------------------------------------------


def test_format_jira_escapes_pipe_in_data():
"""Pipe in a data cell must not break cell boundaries."""
rows = [["Cmd", "Desc"], ["cat | grep", "filter"]]
result = format_table(rows, "jira")
lines = result.strip().split("\n")
assert len(lines) == 2 # 1 header + 1 data row
# Header line has 3 cells: ||Cmd||Desc||
assert lines[0].count("||") == 3 # opening + between + closing = 3 occurrences for 2 columns
# Data line: |cat \| grep|filter|
assert r"cat \| grep" in result


def test_format_jira_escapes_pipe_in_header():
"""Pipe in a header cell must not create extra columns."""
rows = [["A|B", "C"], ["1", "2"]]
result = format_table(rows, "jira")
lines = result.strip().split("\n")
assert len(lines) == 2
assert r"A\|B" in result


def test_format_jira_escapes_double_pipe():
"""|| in a data cell must not create accidental header markup."""
rows = [["X", "Y"], ["a||b", "c"]]
result = format_table(rows, "jira")
lines = result.strip().split("\n")
# Data row should still have exactly 2 cells
data_line = lines[1]
# The || should be escaped to \|\|
assert r"a\|\|b" in data_line


def test_format_jira_escapes_backslash():
r"""Backslash must be escaped to avoid swallowing the next character."""
rows = [["Val"], [r"a\b"]]
result = format_table(rows, "jira")
assert r"a\\b" in result


def test_format_jira_leading_trailing_pipe():
"""Leading/trailing pipes in cell values must be escaped."""
rows = [["Col"], ["|leading"], ["trailing|"]]
result = format_table(rows, "jira")
lines = result.strip().split("\n")
assert len(lines) == 3 # 1 header + 2 data rows
assert r"\|leading" in result
assert r"trailing\|" in result


def test_format_confluence_escapes_same_as_jira():
"""Confluence uses _format_jira, so escaping must apply."""
rows = [["A"], ["x|y"]]
assert format_table(rows, "confluence") == format_table(rows, "jira")
assert r"x\|y" in format_table(rows, "confluence")
Loading