Repository navigation
Expand file tree
/
Copy pathhighlight.py
More file actions
145 lines (114 loc) · 3.82 KB
/
Copy pathhighlight.py
File metadata and controls
145 lines (114 loc) · 3.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "click",
# ]
# ///
import sys
import re
from dataclasses import dataclass
from typing import List, Union, Tuple
import click
@dataclass
class Text:
"""Represents non-matching text"""
content: str
@dataclass
class Match:
"""Represents matching text that should be highlighted"""
content: str
@dataclass
class Ellipsis:
"""Represents ... for truncated content"""
pass
def find_matches(
text: str, terms: List[str], context: int = 30
) -> List[Union[Text, Match, Ellipsis]]:
"""
Find all matches of the given terms in the text, with surrounding context.
Returns a list of Text, Match, and Ellipsis objects.
"""
if not terms:
return [Text(text)]
# Create pattern that matches any of the terms
pattern = "|".join(re.escape(term) for term in terms)
regex = re.compile(pattern, re.IGNORECASE)
# Find all matches with their positions
matches = list(regex.finditer(text))
if not matches:
return [Text(text)]
# Create segments with context
segments: List[Tuple[int, int]] = [] # List of (start, end) for context regions
match_positions: List[Tuple[int, int]] = (
[]
) # List of (start, end) for actual matches
for match in matches:
start, end = match.span()
match_positions.append((start, end))
context_start = max(0, start - context)
context_end = min(len(text), end + context)
segments.append((context_start, context_end))
# Merge overlapping context segments
merged_segments = []
current = segments[0]
for next_seg in segments[1:]:
if next_seg[0] <= current[1]:
# Segments overlap, merge them
current = (current[0], max(current[1], next_seg[1]))
else:
merged_segments.append(current)
current = next_seg
merged_segments.append(current)
# Convert segments to result objects
result = []
last_end = 0
for seg_start, seg_end in merged_segments:
# Add ellipsis if there's a gap
if seg_start > last_end:
if last_end > 0: # Only add if not at the start
result.append(Ellipsis())
# Find all matches within this segment
segment_matches = [
(s, e) for s, e in match_positions if s >= seg_start and e <= seg_end
]
# Add content with proper highlighting
pos = seg_start
for match_start, match_end in segment_matches:
# Add text before match
if match_start > pos:
result.append(Text(text[pos:match_start]))
# Add match
result.append(Match(text[match_start:match_end]))
pos = match_end
# Add remaining text in segment
if pos < seg_end:
result.append(Text(text[pos:seg_end]))
last_end = seg_end
# Add final ellipsis if we're not at the end
if last_end < len(text):
result.append(Ellipsis())
return result
@click.command()
@click.argument("terms", nargs=-1, required=True)
@click.option(
"-c", "--context", default=30, help="Number of context characters around matches"
)
def main(terms: List[str], context: int):
"""Search for terms in stdin and output colored matches with context."""
text = sys.stdin.read()
results = find_matches(text, terms, context)
# ANSI color codes
YELLOW = "\033[93m"
RED = "\033[91m"
END = "\033[0m"
# Convert results to colored output
for item in results:
if isinstance(item, Text):
print(item.content, end="")
elif isinstance(item, Match):
print(f"{RED}{item.content}{END}", end="")
elif isinstance(item, Ellipsis):
print(f"{YELLOW}...{END}", end="")
print() # Final newline
if __name__ == "__main__":
main()