Skip to content

Commit 47f28e5

Browse files
holmboeclaude
andcommitted
Fix Rich markup issues with square brackets in user content
Fixes #135: MarkupError crash on [/path] patterns Fixes #136: Silent stripping of [bug] tags from titles Uses rich.markup.escape() to escape user-provided content while keeping Rich markup enabled for intentional formatting (hyperlinks, bold labels, etc.). Changes: - Import markup.escape from rich library - Add _escape() helper method to safely escape user content - Escape all user content in _display_task_yaml() (15 locations): * Task fields (Name, Description, etc.) * Board/Column names * History transitions * Comments * Metadata values - Escape all user content in _display_task_tree() (15 locations): * Task fields * Board/Column names * History transitions * Comments * Metadata values - Preserve Text objects (hyperlinks) without escaping - Combine YAML quote escaping with Rich markup escaping where needed Testing: - T525 now displays without crashing (had [/path] patterns) - T2163 now shows full title with [bug] prefix (was being stripped) - Search results display correctly - --format=strict still works as expected 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b2cdda3 commit 47f28e5

1 file changed

Lines changed: 55 additions & 27 deletions

File tree

phabfive/maniphest.py

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from jinja2 import Environment, Template, meta
1818

1919
# 3rd party imports
20+
from rich.markup import escape
2021
from rich.text import Text
2122
from rich.tree import Tree
2223
from ruamel.yaml import YAML
@@ -43,6 +44,31 @@ class Maniphest(Phabfive):
4344
def __init__(self):
4445
super(Maniphest, self).__init__()
4546

47+
def _escape(self, content):
48+
"""
49+
Safely escape user-provided content for Rich markup display.
50+
51+
Rich library treats square brackets [...] as markup syntax. This method
52+
escapes user-provided content so literal brackets are displayed correctly
53+
without being interpreted as Rich formatting tags.
54+
55+
Parameters
56+
----------
57+
content : any
58+
User-provided content to escape (task names, descriptions, comments, etc.)
59+
60+
Returns
61+
-------
62+
str or Text
63+
Escaped string safe for Rich display, or original Text object if already safe
64+
"""
65+
if content is None:
66+
return ""
67+
if isinstance(content, Text):
68+
# Text objects are already safe Rich objects, don't escape
69+
return content
70+
return escape(str(content))
71+
4672
def _resolve_project_phids(self, project: str) -> list[str]:
4773
"""
4874
Resolve project name, hashtag, or wildcard pattern to list of project PHIDs.
@@ -2102,12 +2128,13 @@ def _display_task_yaml(self, console, task_dict):
21022128
# Multi-line value
21032129
console.print(f" {key}: |-")
21042130
for line in str(value).splitlines():
2105-
console.print(f" {line}")
2131+
console.print(f" {self._escape(line)}")
21062132
elif self._needs_yaml_quoting(value):
2107-
escaped = value.replace("'", "''")
2108-
console.print(f" {key}: '{escaped}'")
2133+
# YAML quote escaping ('' for single quotes) + Rich markup escaping
2134+
yaml_escaped = str(value).replace("'", "''")
2135+
console.print(f" {key}: '{self._escape(yaml_escaped)}'")
21092136
else:
2110-
console.print(f" {key}: {value}")
2137+
console.print(f" {key}: {self._escape(value)}")
21112138

21122139
# Print Assignee
21132140
if assignee:
@@ -2145,18 +2172,19 @@ def _display_task_yaml(self, console, task_dict):
21452172
)
21462173
)
21472174
else:
2148-
escaped = column_link.replace("'", "''")
2149-
console.print(f" Column: '{escaped}'")
2175+
# column_link is a string, needs both YAML and Rich escaping
2176+
yaml_escaped = column_link.replace("'", "''")
2177+
console.print(f" Column: '{self._escape(yaml_escaped)}'")
21502178
else:
21512179
console.print(
21522180
Text.assemble(" Column: ", column_link)
21532181
)
21542182
continue
21552183
if self._needs_yaml_quoting(value):
2156-
escaped = value.replace("'", "''")
2157-
console.print(f" {key}: '{escaped}'")
2184+
yaml_escaped = str(value).replace("'", "''")
2185+
console.print(f" {key}: '{self._escape(yaml_escaped)}'")
21582186
else:
2159-
console.print(f" {key}: {value}")
2187+
console.print(f" {key}: {self._escape(value)}")
21602188

21612189
# Print History section
21622190
if history:
@@ -2165,13 +2193,13 @@ def _display_task_yaml(self, console, task_dict):
21652193
if hist_key == "Boards" and isinstance(hist_value, dict):
21662194
console.print(" Boards:")
21672195
for board_name, transitions in hist_value.items():
2168-
console.print(f" {board_name}:")
2196+
console.print(f" {self._escape(board_name)}:")
21692197
for trans in transitions:
2170-
console.print(f" - {trans}")
2198+
console.print(f" - {self._escape(trans)}")
21712199
elif isinstance(hist_value, list):
21722200
console.print(f" {hist_key}:")
21732201
for trans in hist_value:
2174-
console.print(f" - {trans}")
2202+
console.print(f" - {self._escape(trans)}")
21752203

21762204
# Print Comments section
21772205
comments = task_dict.get("Comments", [])
@@ -2181,11 +2209,11 @@ def _display_task_yaml(self, console, task_dict):
21812209
if isinstance(comment, PreservedScalarString) or "\n" in str(comment):
21822210
# Multi-line comment
21832211
lines = str(comment).splitlines()
2184-
console.print(f" - {lines[0]}")
2212+
console.print(f" - {self._escape(lines[0])}")
21852213
for line in lines[1:]:
2186-
console.print(f" {line}")
2214+
console.print(f" {self._escape(line)}")
21872215
else:
2188-
console.print(f" - {comment}")
2216+
console.print(f" - {self._escape(comment)}")
21892217

21902218
# Print Metadata section
21912219
if metadata:
@@ -2195,11 +2223,11 @@ def _display_task_yaml(self, console, task_dict):
21952223
if meta_value:
21962224
console.print(f" {meta_key}:")
21972225
for item in meta_value:
2198-
console.print(f" - {item}")
2226+
console.print(f" - {self._escape(item)}")
21992227
else:
22002228
console.print(f" {meta_key}: []")
22012229
else:
2202-
console.print(f" {meta_key}: {meta_value}")
2230+
console.print(f" {meta_key}: {self._escape(meta_value)}")
22032231

22042232
def _display_task_tree(self, console, task_dict):
22052233
"""Display a single task in tree format using Rich Tree.
@@ -2230,9 +2258,9 @@ def _display_task_tree(self, console, task_dict):
22302258
first_line = str(value).split("\n")[0]
22312259
if len(first_line) > 60:
22322260
first_line = first_line[:57] + "..."
2233-
task_branch.add(f"{key}: {first_line}")
2261+
task_branch.add(f"{key}: {self._escape(first_line)}")
22342262
else:
2235-
task_branch.add(f"{key}: {value}")
2263+
task_branch.add(f"{key}: {self._escape(value)}")
22362264

22372265
# Add Assignee
22382266
if assignee:
@@ -2262,7 +2290,7 @@ def _display_task_tree(self, console, task_dict):
22622290
)
22632291
board_branch.add(Text.assemble("Column: ", column_link))
22642292
continue
2265-
board_branch.add(f"{key}: {value}")
2293+
board_branch.add(f"{key}: {self._escape(value)}")
22662294

22672295
# Add History section
22682296
if history:
@@ -2271,13 +2299,13 @@ def _display_task_tree(self, console, task_dict):
22712299
if hist_key == "Boards" and isinstance(hist_value, dict):
22722300
boards_hist = history_branch.add("Boards")
22732301
for board_name, transitions in hist_value.items():
2274-
board_hist = boards_hist.add(board_name)
2302+
board_hist = boards_hist.add(self._escape(board_name))
22752303
for trans in transitions:
2276-
board_hist.add(trans)
2304+
board_hist.add(self._escape(trans))
22772305
elif isinstance(hist_value, list):
22782306
hist_type_branch = history_branch.add(hist_key)
22792307
for trans in hist_value:
2280-
hist_type_branch.add(trans)
2308+
hist_type_branch.add(self._escape(trans))
22812309

22822310
# Add Comments section
22832311
comments = task_dict.get("Comments", [])
@@ -2289,9 +2317,9 @@ def _display_task_tree(self, console, task_dict):
22892317
first_line = str(comment).split("\n")[0]
22902318
if len(first_line) > 60:
22912319
first_line = first_line[:57] + "..."
2292-
comments_branch.add(first_line)
2320+
comments_branch.add(self._escape(first_line))
22932321
else:
2294-
comments_branch.add(str(comment))
2322+
comments_branch.add(self._escape(str(comment)))
22952323

22962324
# Add Metadata section
22972325
if metadata:
@@ -2301,11 +2329,11 @@ def _display_task_tree(self, console, task_dict):
23012329
if meta_value:
23022330
list_branch = meta_branch.add(meta_key)
23032331
for item in meta_value:
2304-
list_branch.add(str(item))
2332+
list_branch.add(self._escape(str(item)))
23052333
else:
23062334
meta_branch.add(f"{meta_key}: []")
23072335
else:
2308-
meta_branch.add(f"{meta_key}: {meta_value}")
2336+
meta_branch.add(f"{meta_key}: {self._escape(meta_value)}")
23092337

23102338
console.print(tree)
23112339

0 commit comments

Comments
 (0)