Skip to content

Commit be3fd54

Browse files
holmboeclaude
andcommitted
fix: Restore Rich markup escaping lost in refactoring
The refactor commits (4f19573, 0fb3d1b) moved display logic from phabfive/maniphest.py to phabfive/cli.py but failed to carry over the rich.markup.escape() fix from PR #137. This restores the escaping by: - Adding _escape_for_rich() helper function - Escaping user content in display_task_rich() and display_task_tree() Fixes regression of #135 (MarkupError on [/path] patterns) and #136 (silent stripping of [bug] tags). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 30f2d04 commit be3fd54

1 file changed

Lines changed: 52 additions & 25 deletions

File tree

phabfive/cli.py

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
# 3rd party imports
1212
from docopt import DocoptExit, Option, docopt, extras
13+
from rich.markup import escape
1314
from rich.text import Text
1415
from rich.tree import Tree
1516
from ruamel.yaml import YAML
@@ -26,6 +27,32 @@
2627
# =============================================================================
2728

2829

30+
def _escape_for_rich(content):
31+
"""Escape user content for safe Rich printing.
32+
33+
Rich interprets square brackets [...] as markup syntax.
34+
This causes issues with user content containing:
35+
- [/path] patterns -> MarkupError crash (interpreted as closing tag)
36+
- [bug] tags -> Silently stripped (interpreted as invalid style)
37+
38+
Parameters
39+
----------
40+
content : any
41+
User-provided content to escape
42+
43+
Returns
44+
-------
45+
str or Text
46+
Empty string for None, Text objects unchanged, escaped string otherwise
47+
"""
48+
if content is None:
49+
return ""
50+
if isinstance(content, Text):
51+
# Text objects are already safe Rich objects (e.g., hyperlinks)
52+
return content
53+
return escape(str(content))
54+
55+
2956
def _needs_yaml_quoting(value):
3057
"""Check if a string value needs YAML quoting.
3158
@@ -72,12 +99,12 @@ def display_task_rich(console, task_dict, phabfive_instance):
7299
# Multi-line value
73100
console.print(f" {key}: |-")
74101
for line in str(value).splitlines():
75-
console.print(f" {line}")
102+
console.print(f" {_escape_for_rich(line)}")
76103
elif _needs_yaml_quoting(value):
77-
escaped = value.replace("'", "''")
78-
console.print(f" {key}: '{escaped}'")
104+
escaped = str(value).replace("'", "''")
105+
console.print(f" {key}: '{_escape_for_rich(escaped)}'")
79106
else:
80-
console.print(f" {key}: {value}")
107+
console.print(f" {key}: {_escape_for_rich(value)}")
81108

82109
# Print Assignee
83110
if assignee:
@@ -127,10 +154,10 @@ def display_task_rich(console, task_dict, phabfive_instance):
127154
)
128155
continue
129156
if _needs_yaml_quoting(value):
130-
escaped = value.replace("'", "''")
131-
console.print(f" {key}: '{escaped}'")
157+
escaped = str(value).replace("'", "''")
158+
console.print(f" {key}: '{_escape_for_rich(escaped)}'")
132159
else:
133-
console.print(f" {key}: {value}")
160+
console.print(f" {key}: {_escape_for_rich(value)}")
134161

135162
# Print History section
136163
if history:
@@ -139,13 +166,13 @@ def display_task_rich(console, task_dict, phabfive_instance):
139166
if hist_key == "Boards" and isinstance(hist_value, dict):
140167
console.print(" Boards:")
141168
for board_name, transitions in hist_value.items():
142-
console.print(f" {board_name}:")
169+
console.print(f" {_escape_for_rich(board_name)}:")
143170
for trans in transitions:
144-
console.print(f" - {trans}")
171+
console.print(f" - {_escape_for_rich(trans)}")
145172
elif isinstance(hist_value, list):
146173
console.print(f" {hist_key}:")
147174
for trans in hist_value:
148-
console.print(f" - {trans}")
175+
console.print(f" - {_escape_for_rich(trans)}")
149176

150177
# Print Comments section
151178
comments = task_dict.get("Comments", [])
@@ -155,11 +182,11 @@ def display_task_rich(console, task_dict, phabfive_instance):
155182
if isinstance(comment, PreservedScalarString) or "\n" in str(comment):
156183
# Multi-line comment
157184
lines = str(comment).splitlines()
158-
console.print(f" - {lines[0]}")
185+
console.print(f" - {_escape_for_rich(lines[0])}")
159186
for line in lines[1:]:
160-
console.print(f" {line}")
187+
console.print(f" {_escape_for_rich(line)}")
161188
else:
162-
console.print(f" - {comment}")
189+
console.print(f" - {_escape_for_rich(comment)}")
163190

164191
# Print Metadata section
165192
if metadata:
@@ -169,11 +196,11 @@ def display_task_rich(console, task_dict, phabfive_instance):
169196
if meta_value:
170197
console.print(f" {meta_key}:")
171198
for item in meta_value:
172-
console.print(f" - {item}")
199+
console.print(f" - {_escape_for_rich(item)}")
173200
else:
174201
console.print(f" {meta_key}: []")
175202
else:
176-
console.print(f" {meta_key}: {meta_value}")
203+
console.print(f" {meta_key}: {_escape_for_rich(meta_value)}")
177204

178205

179206
def display_task_tree(console, task_dict, phabfive_instance):
@@ -208,9 +235,9 @@ def display_task_tree(console, task_dict, phabfive_instance):
208235
first_line = str(value).split("\n")[0]
209236
if len(first_line) > 60:
210237
first_line = first_line[:57] + "..."
211-
task_branch.add(f"{key}: {first_line}")
238+
task_branch.add(f"{key}: {_escape_for_rich(first_line)}")
212239
else:
213-
task_branch.add(f"{key}: {value}")
240+
task_branch.add(f"{key}: {_escape_for_rich(value)}")
214241

215242
# Add Assignee
216243
if assignee:
@@ -244,7 +271,7 @@ def display_task_tree(console, task_dict, phabfive_instance):
244271
)
245272
board_branch.add(Text.assemble("Column: ", column_link))
246273
continue
247-
board_branch.add(f"{key}: {value}")
274+
board_branch.add(f"{key}: {_escape_for_rich(value)}")
248275

249276
# Add History section
250277
if history:
@@ -253,13 +280,13 @@ def display_task_tree(console, task_dict, phabfive_instance):
253280
if hist_key == "Boards" and isinstance(hist_value, dict):
254281
boards_hist = history_branch.add("Boards")
255282
for board_name, transitions in hist_value.items():
256-
board_hist = boards_hist.add(board_name)
283+
board_hist = boards_hist.add(_escape_for_rich(board_name))
257284
for trans in transitions:
258-
board_hist.add(trans)
285+
board_hist.add(_escape_for_rich(trans))
259286
elif isinstance(hist_value, list):
260287
hist_type_branch = history_branch.add(hist_key)
261288
for trans in hist_value:
262-
hist_type_branch.add(trans)
289+
hist_type_branch.add(_escape_for_rich(trans))
263290

264291
# Add Comments section
265292
comments = task_dict.get("Comments", [])
@@ -271,9 +298,9 @@ def display_task_tree(console, task_dict, phabfive_instance):
271298
first_line = str(comment).split("\n")[0]
272299
if len(first_line) > 60:
273300
first_line = first_line[:57] + "..."
274-
comments_branch.add(first_line)
301+
comments_branch.add(_escape_for_rich(first_line))
275302
else:
276-
comments_branch.add(str(comment))
303+
comments_branch.add(_escape_for_rich(str(comment)))
277304

278305
# Add Metadata section
279306
if metadata:
@@ -283,11 +310,11 @@ def display_task_tree(console, task_dict, phabfive_instance):
283310
if meta_value:
284311
list_branch = meta_branch.add(meta_key)
285312
for item in meta_value:
286-
list_branch.add(str(item))
313+
list_branch.add(_escape_for_rich(str(item)))
287314
else:
288315
meta_branch.add(f"{meta_key}: []")
289316
else:
290-
meta_branch.add(f"{meta_key}: {meta_value}")
317+
meta_branch.add(f"{meta_key}: {_escape_for_rich(meta_value)}")
291318

292319
console.print(tree)
293320

0 commit comments

Comments
 (0)