reveal-cli 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plugins/c-header.yaml +89 -0
- plugins/gdscript.yaml +96 -0
- plugins/python.yaml +94 -0
- plugins/yaml.yaml +87 -0
- reveal/__init__.py +26 -0
- reveal/analyzers/__init__.py +39 -0
- reveal/analyzers/bash.py +44 -0
- reveal/analyzers/dockerfile.py +179 -0
- reveal/analyzers/gdscript.py +176 -0
- reveal/analyzers/go.py +13 -0
- reveal/analyzers/javascript.py +21 -0
- reveal/analyzers/jupyter_analyzer.py +230 -0
- reveal/analyzers/markdown.py +79 -0
- reveal/analyzers/nginx.py +185 -0
- reveal/analyzers/python.py +15 -0
- reveal/analyzers/rust.py +13 -0
- reveal/analyzers/toml.py +96 -0
- reveal/analyzers/typescript.py +24 -0
- reveal/analyzers/yaml_json.py +110 -0
- reveal/base.py +267 -0
- reveal/main.py +355 -0
- reveal/tests/__init__.py +1 -0
- reveal/tests/test_json_yaml_line_numbers.py +238 -0
- reveal/tests/test_line_numbers.py +151 -0
- reveal/tests/test_toml_analyzer.py +220 -0
- reveal/tree_view.py +105 -0
- reveal/treesitter.py +281 -0
- reveal_cli-0.8.0.dist-info/METADATA +352 -0
- reveal_cli-0.8.0.dist-info/RECORD +33 -0
- reveal_cli-0.8.0.dist-info/WHEEL +5 -0
- reveal_cli-0.8.0.dist-info/entry_points.txt +2 -0
- reveal_cli-0.8.0.dist-info/licenses/LICENSE +21 -0
- reveal_cli-0.8.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""GDScript file analyzer - for Godot game engine scripts."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, List, Any, Optional
|
|
5
|
+
from ..base import FileAnalyzer, register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register('.gd', name='GDScript', icon='🎮')
|
|
9
|
+
class GDScriptAnalyzer(FileAnalyzer):
|
|
10
|
+
"""GDScript file analyzer for Godot Engine.
|
|
11
|
+
|
|
12
|
+
Extracts classes, functions, signals, and variables.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
16
|
+
"""Extract GDScript structure."""
|
|
17
|
+
classes = []
|
|
18
|
+
functions = []
|
|
19
|
+
signals = []
|
|
20
|
+
variables = []
|
|
21
|
+
|
|
22
|
+
for i, line in enumerate(self.lines, 1):
|
|
23
|
+
# Match class definition: class ClassName:
|
|
24
|
+
class_match = re.match(r'^\s*class\s+(\w+)\s*:', line)
|
|
25
|
+
if class_match:
|
|
26
|
+
classes.append({
|
|
27
|
+
'line': i,
|
|
28
|
+
'name': class_match.group(1),
|
|
29
|
+
})
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
# Match function definition: func function_name(...):
|
|
33
|
+
func_match = re.match(r'^\s*func\s+(\w+)\s*\((.*?)\)\s*(?:->\s*(.+?))?\s*:', line)
|
|
34
|
+
if func_match:
|
|
35
|
+
name = func_match.group(1)
|
|
36
|
+
params = func_match.group(2).strip()
|
|
37
|
+
return_type = func_match.group(3).strip() if func_match.group(3) else None
|
|
38
|
+
|
|
39
|
+
signature = f"({params})"
|
|
40
|
+
if return_type:
|
|
41
|
+
signature += f" -> {return_type}"
|
|
42
|
+
|
|
43
|
+
functions.append({
|
|
44
|
+
'line': i,
|
|
45
|
+
'name': name,
|
|
46
|
+
'signature': signature,
|
|
47
|
+
})
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# Match signal: signal signal_name or signal signal_name(params)
|
|
51
|
+
signal_match = re.match(r'^\s*signal\s+(\w+)(?:\((.*?)\))?\s*$', line)
|
|
52
|
+
if signal_match:
|
|
53
|
+
name = signal_match.group(1)
|
|
54
|
+
params = signal_match.group(2) if signal_match.group(2) else ''
|
|
55
|
+
|
|
56
|
+
signals.append({
|
|
57
|
+
'line': i,
|
|
58
|
+
'name': name,
|
|
59
|
+
'signature': f"({params})" if params else "()",
|
|
60
|
+
})
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Match variables: var/const/export
|
|
64
|
+
var_match = re.match(r'^\s*(?:(export|onready)\s+)?(?:(var|const)\s+)?(\w+)(?:\s*:\s*(\w+))?(?:\s*=\s*(.+?))?\s*(?:#.*)?$', line)
|
|
65
|
+
if var_match and var_match.group(2) in ('var', 'const'):
|
|
66
|
+
modifier = var_match.group(1) or ''
|
|
67
|
+
var_type = var_match.group(2)
|
|
68
|
+
name = var_match.group(3)
|
|
69
|
+
type_hint = var_match.group(4) or ''
|
|
70
|
+
|
|
71
|
+
# Skip if this looks like a function call or other syntax
|
|
72
|
+
if name and not name.startswith('_'):
|
|
73
|
+
var_kind = f"{modifier} {var_type}".strip()
|
|
74
|
+
type_info = f": {type_hint}" if type_hint else ""
|
|
75
|
+
|
|
76
|
+
variables.append({
|
|
77
|
+
'line': i,
|
|
78
|
+
'name': name,
|
|
79
|
+
'kind': var_kind,
|
|
80
|
+
'type': type_hint or 'Variant',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
result = {}
|
|
84
|
+
if classes:
|
|
85
|
+
result['classes'] = classes
|
|
86
|
+
if functions:
|
|
87
|
+
result['functions'] = functions
|
|
88
|
+
if signals:
|
|
89
|
+
result['signals'] = signals
|
|
90
|
+
if variables:
|
|
91
|
+
result['variables'] = variables
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
def extract_element(self, element_type: str, name: str) -> Optional[Dict[str, Any]]:
|
|
96
|
+
"""Extract a specific GDScript element.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
element_type: 'function', 'class', 'signal', or 'variable'
|
|
100
|
+
name: Name of the element
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict with element info and source
|
|
104
|
+
"""
|
|
105
|
+
# Find the element
|
|
106
|
+
for i, line in enumerate(self.lines, 1):
|
|
107
|
+
# Check for function
|
|
108
|
+
if element_type == 'function':
|
|
109
|
+
func_match = re.match(r'^\s*func\s+(\w+)\s*\(', line)
|
|
110
|
+
if func_match and func_match.group(1) == name:
|
|
111
|
+
return self._extract_function(i)
|
|
112
|
+
|
|
113
|
+
# Check for class
|
|
114
|
+
elif element_type == 'class':
|
|
115
|
+
class_match = re.match(r'^\s*class\s+(\w+)\s*:', line)
|
|
116
|
+
if class_match and class_match.group(1) == name:
|
|
117
|
+
return self._extract_class(i)
|
|
118
|
+
|
|
119
|
+
# Check for signal or variable (single line)
|
|
120
|
+
elif re.search(rf'\b{re.escape(name)}\b', line):
|
|
121
|
+
return {
|
|
122
|
+
'name': name,
|
|
123
|
+
'line_start': i,
|
|
124
|
+
'line_end': i,
|
|
125
|
+
'source': line,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Fallback to grep-based search
|
|
129
|
+
return super().extract_element(element_type, name)
|
|
130
|
+
|
|
131
|
+
def _extract_function(self, start_line: int) -> Dict[str, Any]:
|
|
132
|
+
"""Extract a complete function definition."""
|
|
133
|
+
# Find the end of the function (next func/class/end of file)
|
|
134
|
+
indent_level = len(self.lines[start_line - 1]) - len(self.lines[start_line - 1].lstrip())
|
|
135
|
+
end_line = len(self.lines)
|
|
136
|
+
|
|
137
|
+
for i in range(start_line, len(self.lines)):
|
|
138
|
+
line = self.lines[i]
|
|
139
|
+
# Check if we've hit another function/class at same or lower indent
|
|
140
|
+
if line.strip() and not line.startswith('#'):
|
|
141
|
+
current_indent = len(line) - len(line.lstrip())
|
|
142
|
+
if current_indent <= indent_level and (line.strip().startswith('func ') or line.strip().startswith('class ')):
|
|
143
|
+
end_line = i
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
source = '\n'.join(self.lines[start_line - 1:end_line])
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
'name': re.search(r'func\s+(\w+)', self.lines[start_line - 1]).group(1),
|
|
150
|
+
'line_start': start_line,
|
|
151
|
+
'line_end': end_line,
|
|
152
|
+
'source': source,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
def _extract_class(self, start_line: int) -> Dict[str, Any]:
|
|
156
|
+
"""Extract a complete class definition."""
|
|
157
|
+
# Find the end of the class (next class at same level or end of file)
|
|
158
|
+
indent_level = len(self.lines[start_line - 1]) - len(self.lines[start_line - 1].lstrip())
|
|
159
|
+
end_line = len(self.lines)
|
|
160
|
+
|
|
161
|
+
for i in range(start_line, len(self.lines)):
|
|
162
|
+
line = self.lines[i]
|
|
163
|
+
if line.strip() and not line.startswith('#'):
|
|
164
|
+
current_indent = len(line) - len(line.lstrip())
|
|
165
|
+
if current_indent <= indent_level and line.strip().startswith('class '):
|
|
166
|
+
end_line = i
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
source = '\n'.join(self.lines[start_line - 1:end_line])
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
'name': re.search(r'class\s+(\w+)', self.lines[start_line - 1]).group(1),
|
|
173
|
+
'line_start': start_line,
|
|
174
|
+
'line_end': end_line,
|
|
175
|
+
'source': source,
|
|
176
|
+
}
|
reveal/analyzers/go.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Go file analyzer - tree-sitter based."""
|
|
2
|
+
|
|
3
|
+
from ..base import register
|
|
4
|
+
from ..treesitter import TreeSitterAnalyzer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register('.go', name='Go', icon='🔷')
|
|
8
|
+
class GoAnalyzer(TreeSitterAnalyzer):
|
|
9
|
+
"""Go file analyzer.
|
|
10
|
+
|
|
11
|
+
Full Go support in 3 lines!
|
|
12
|
+
"""
|
|
13
|
+
language = 'go'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""JavaScript file analyzer - tree-sitter based."""
|
|
2
|
+
|
|
3
|
+
from ..base import register
|
|
4
|
+
from ..treesitter import TreeSitterAnalyzer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register('.js', name='JavaScript', icon='📜')
|
|
8
|
+
class JavaScriptAnalyzer(TreeSitterAnalyzer):
|
|
9
|
+
"""JavaScript file analyzer.
|
|
10
|
+
|
|
11
|
+
Full JavaScript support via tree-sitter!
|
|
12
|
+
Extracts:
|
|
13
|
+
- Import/export statements
|
|
14
|
+
- Function declarations
|
|
15
|
+
- Class definitions
|
|
16
|
+
- Arrow functions
|
|
17
|
+
- Object methods
|
|
18
|
+
|
|
19
|
+
Works on all platforms (Windows, Linux, macOS).
|
|
20
|
+
"""
|
|
21
|
+
language = 'javascript'
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Jupyter Notebook (.ipynb) analyzer."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
from ..base import FileAnalyzer, register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register('.ipynb', name='Jupyter', icon='📓')
|
|
9
|
+
class JupyterAnalyzer(FileAnalyzer):
|
|
10
|
+
"""Analyzer for Jupyter Notebook files"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, path: str):
|
|
13
|
+
super().__init__(path)
|
|
14
|
+
self.parse_error = None
|
|
15
|
+
self.notebook_data = None
|
|
16
|
+
self.cells = []
|
|
17
|
+
self.metadata = {}
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
self.notebook_data = json.loads(self.content)
|
|
21
|
+
self.cells = self.notebook_data.get('cells', [])
|
|
22
|
+
self.metadata = self.notebook_data.get('metadata', {})
|
|
23
|
+
except Exception as e:
|
|
24
|
+
self.parse_error = str(e)
|
|
25
|
+
|
|
26
|
+
def get_structure(self) -> Dict[str, Any]:
|
|
27
|
+
"""Analyze Jupyter notebook structure."""
|
|
28
|
+
if self.parse_error:
|
|
29
|
+
return {
|
|
30
|
+
'error': self.parse_error,
|
|
31
|
+
'cells': [],
|
|
32
|
+
'cell_counts': {},
|
|
33
|
+
'kernel': 'unknown',
|
|
34
|
+
'language': 'unknown',
|
|
35
|
+
'total_cells': 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Count cells by type
|
|
39
|
+
cell_counts = {}
|
|
40
|
+
for cell in self.cells:
|
|
41
|
+
cell_type = cell.get('cell_type', 'unknown')
|
|
42
|
+
cell_counts[cell_type] = cell_counts.get(cell_type, 0) + 1
|
|
43
|
+
|
|
44
|
+
# Get kernel info
|
|
45
|
+
kernelspec = self.metadata.get('kernelspec', {})
|
|
46
|
+
kernel_name = kernelspec.get('display_name', kernelspec.get('name', 'unknown'))
|
|
47
|
+
|
|
48
|
+
# Get language info
|
|
49
|
+
language_info = self.metadata.get('language_info', {})
|
|
50
|
+
language = language_info.get('name', 'unknown')
|
|
51
|
+
|
|
52
|
+
# Get cell summaries with line numbers
|
|
53
|
+
cell_summaries = []
|
|
54
|
+
current_line = 1
|
|
55
|
+
|
|
56
|
+
# Navigate through JSON to find approximate line numbers
|
|
57
|
+
# This is approximate since JSON formatting varies
|
|
58
|
+
for idx, cell in enumerate(self.cells):
|
|
59
|
+
cell_type = cell.get('cell_type', 'unknown')
|
|
60
|
+
source = cell.get('source', [])
|
|
61
|
+
|
|
62
|
+
# Calculate approximate line in source file
|
|
63
|
+
# Look for cell marker in original lines
|
|
64
|
+
cell_line = self._find_cell_line(idx)
|
|
65
|
+
|
|
66
|
+
# Get first line of content
|
|
67
|
+
first_line = ""
|
|
68
|
+
if source:
|
|
69
|
+
first_line = (source[0] if isinstance(source, list) else source).strip()
|
|
70
|
+
if len(first_line) > 50:
|
|
71
|
+
first_line = first_line[:50] + "..."
|
|
72
|
+
|
|
73
|
+
# Count execution info for code cells
|
|
74
|
+
execution_count = cell.get('execution_count', None)
|
|
75
|
+
outputs_count = len(cell.get('outputs', []))
|
|
76
|
+
|
|
77
|
+
# Create a descriptive name for the cell
|
|
78
|
+
if cell_type == 'markdown':
|
|
79
|
+
name = first_line if first_line else f"Markdown cell #{idx + 1}"
|
|
80
|
+
elif cell_type == 'code':
|
|
81
|
+
exec_info = f"[{execution_count}]" if execution_count else "[not executed]"
|
|
82
|
+
name = f"Code {exec_info}: {first_line}" if first_line else f"Code cell #{idx + 1}"
|
|
83
|
+
else:
|
|
84
|
+
name = f"{cell_type} cell #{idx + 1}"
|
|
85
|
+
|
|
86
|
+
cell_summaries.append({
|
|
87
|
+
'line': cell_line,
|
|
88
|
+
'name': name,
|
|
89
|
+
'type': cell_type,
|
|
90
|
+
'execution_count': execution_count,
|
|
91
|
+
'outputs_count': outputs_count,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# Return only the cells list for display
|
|
95
|
+
# The structure format expects dict[str, List[Dict]]
|
|
96
|
+
result = {}
|
|
97
|
+
if cell_summaries:
|
|
98
|
+
result['cells'] = cell_summaries
|
|
99
|
+
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
def _find_cell_line(self, cell_index: int) -> int:
|
|
103
|
+
"""
|
|
104
|
+
Find approximate line number where a cell starts in the JSON.
|
|
105
|
+
|
|
106
|
+
This searches for cell markers in the original source.
|
|
107
|
+
"""
|
|
108
|
+
# Look for "cell_type" string followed by the type for this cell
|
|
109
|
+
if cell_index < len(self.cells):
|
|
110
|
+
cell = self.cells[cell_index]
|
|
111
|
+
cell_type = cell.get('cell_type', '')
|
|
112
|
+
|
|
113
|
+
# Count how many cells of this type we've seen before
|
|
114
|
+
cells_before = sum(1 for c in self.cells[:cell_index] if c.get('cell_type') == cell_type)
|
|
115
|
+
|
|
116
|
+
# Search for the nth occurrence of this cell_type in the file
|
|
117
|
+
count = 0
|
|
118
|
+
search_str = f'"cell_type": "{cell_type}"'
|
|
119
|
+
for i, line in enumerate(self.lines, 1):
|
|
120
|
+
if search_str in line:
|
|
121
|
+
if count == cells_before:
|
|
122
|
+
return i
|
|
123
|
+
count += 1
|
|
124
|
+
|
|
125
|
+
return 1 # Fallback
|
|
126
|
+
|
|
127
|
+
def generate_preview(self) -> List[tuple[int, str]]:
|
|
128
|
+
"""Generate Jupyter notebook preview."""
|
|
129
|
+
preview = []
|
|
130
|
+
|
|
131
|
+
if self.parse_error:
|
|
132
|
+
# Fallback to first 20 lines of JSON
|
|
133
|
+
for i, line in enumerate(self.lines[:20], 1):
|
|
134
|
+
preview.append((i, line))
|
|
135
|
+
return preview
|
|
136
|
+
|
|
137
|
+
# Show metadata section
|
|
138
|
+
if self.metadata:
|
|
139
|
+
kernelspec = self.metadata.get('kernelspec', {})
|
|
140
|
+
kernel = kernelspec.get('display_name', kernelspec.get('name', 'unknown'))
|
|
141
|
+
lang_info = self.metadata.get('language_info', {})
|
|
142
|
+
language = lang_info.get('name', 'unknown')
|
|
143
|
+
|
|
144
|
+
preview.append((1, f"Kernel: {kernel}"))
|
|
145
|
+
preview.append((1, f"Language: {language}"))
|
|
146
|
+
preview.append((1, ""))
|
|
147
|
+
|
|
148
|
+
# Show preview of each cell
|
|
149
|
+
for idx, cell in enumerate(self.cells[:10]): # Limit to first 10 cells
|
|
150
|
+
cell_type = cell.get('cell_type', 'unknown')
|
|
151
|
+
source = cell.get('source', [])
|
|
152
|
+
execution_count = cell.get('execution_count', None)
|
|
153
|
+
|
|
154
|
+
# Cell header
|
|
155
|
+
cell_line = self._find_cell_line(idx)
|
|
156
|
+
header = f"[{idx + 1}] {cell_type.upper()}"
|
|
157
|
+
if execution_count is not None:
|
|
158
|
+
header += f" (exec: {execution_count})"
|
|
159
|
+
preview.append((cell_line, header))
|
|
160
|
+
preview.append((cell_line, "─" * 60))
|
|
161
|
+
|
|
162
|
+
# Cell content (first 5 lines)
|
|
163
|
+
if source:
|
|
164
|
+
source_lines = source if isinstance(source, list) else [source]
|
|
165
|
+
for i, line in enumerate(source_lines[:5]):
|
|
166
|
+
# Remove trailing newlines for display
|
|
167
|
+
clean_line = line.rstrip('\n')
|
|
168
|
+
preview.append((cell_line + i + 1, clean_line))
|
|
169
|
+
|
|
170
|
+
if len(source_lines) > 5:
|
|
171
|
+
preview.append((cell_line + 6, f"... ({len(source_lines) - 5} more lines)"))
|
|
172
|
+
|
|
173
|
+
# Show output summary for code cells
|
|
174
|
+
outputs = cell.get('outputs', [])
|
|
175
|
+
if outputs:
|
|
176
|
+
preview.append((cell_line, f"Outputs: {len(outputs)} items"))
|
|
177
|
+
# Show first output if available
|
|
178
|
+
if outputs[0]:
|
|
179
|
+
output_type = outputs[0].get('output_type', 'unknown')
|
|
180
|
+
preview.append((cell_line, f" └─ {output_type}"))
|
|
181
|
+
|
|
182
|
+
preview.append((cell_line, "")) # Blank line between cells
|
|
183
|
+
|
|
184
|
+
if len(self.cells) > 10:
|
|
185
|
+
preview.append((1, f"... ({len(self.cells) - 10} more cells)"))
|
|
186
|
+
|
|
187
|
+
return preview
|
|
188
|
+
|
|
189
|
+
def format_structure(self, structure: Dict[str, Any]) -> List[str]:
|
|
190
|
+
"""Format structure output for Jupyter notebooks."""
|
|
191
|
+
if structure.get('error'):
|
|
192
|
+
return [f"Error parsing notebook: {structure['error']}"]
|
|
193
|
+
|
|
194
|
+
lines = []
|
|
195
|
+
|
|
196
|
+
# Overview
|
|
197
|
+
lines.append(f"Kernel: {structure['kernel']}")
|
|
198
|
+
lines.append(f"Language: {structure['language']}")
|
|
199
|
+
lines.append(f"Total Cells: {structure['total_cells']}")
|
|
200
|
+
lines.append("")
|
|
201
|
+
|
|
202
|
+
# Cell type breakdown
|
|
203
|
+
if structure['cell_counts']:
|
|
204
|
+
lines.append("Cell Types:")
|
|
205
|
+
for cell_type, count in sorted(structure['cell_counts'].items()):
|
|
206
|
+
lines.append(f" {cell_type}: {count}")
|
|
207
|
+
lines.append("")
|
|
208
|
+
|
|
209
|
+
# Cell listing
|
|
210
|
+
if structure['cells']:
|
|
211
|
+
lines.append("Cells:")
|
|
212
|
+
for cell in structure['cells']:
|
|
213
|
+
loc = self.format_location(cell['line'])
|
|
214
|
+
cell_info = f"[{cell['index'] + 1}] {cell['type']}"
|
|
215
|
+
|
|
216
|
+
if cell['execution_count'] is not None:
|
|
217
|
+
cell_info += f" (exec: {cell['execution_count']})"
|
|
218
|
+
if cell['outputs_count'] > 0:
|
|
219
|
+
cell_info += f" [{cell['outputs_count']} outputs]"
|
|
220
|
+
|
|
221
|
+
# Show first line of content
|
|
222
|
+
if cell['first_line']:
|
|
223
|
+
cell_info += f" - {cell['first_line']}"
|
|
224
|
+
|
|
225
|
+
if loc:
|
|
226
|
+
lines.append(f" {loc:<30} {cell_info}")
|
|
227
|
+
else:
|
|
228
|
+
lines.append(f" {cell_info}")
|
|
229
|
+
|
|
230
|
+
return lines
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Markdown file analyzer."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, List, Any, Optional
|
|
5
|
+
from ..base import FileAnalyzer, register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register('.md', '.markdown', name='Markdown', icon='📝')
|
|
9
|
+
class MarkdownAnalyzer(FileAnalyzer):
|
|
10
|
+
"""Markdown file analyzer.
|
|
11
|
+
|
|
12
|
+
Extracts sections based on headings.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
16
|
+
"""Extract markdown headings."""
|
|
17
|
+
headings = []
|
|
18
|
+
|
|
19
|
+
for i, line in enumerate(self.lines, 1):
|
|
20
|
+
# Match heading syntax: # Heading, ## Heading, etc.
|
|
21
|
+
match = re.match(r'^(#{1,6})\s+(.+)$', line)
|
|
22
|
+
if match:
|
|
23
|
+
level = len(match.group(1))
|
|
24
|
+
title = match.group(2).strip()
|
|
25
|
+
|
|
26
|
+
headings.append({
|
|
27
|
+
'line': i,
|
|
28
|
+
'level': level,
|
|
29
|
+
'name': title,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return {'headings': headings}
|
|
33
|
+
|
|
34
|
+
def extract_element(self, element_type: str, name: str) -> Optional[Dict[str, Any]]:
|
|
35
|
+
"""Extract a markdown section.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
element_type: 'section' or 'heading'
|
|
39
|
+
name: Heading text to find
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dict with section content
|
|
43
|
+
"""
|
|
44
|
+
# Find the heading
|
|
45
|
+
start_line = None
|
|
46
|
+
heading_level = None
|
|
47
|
+
|
|
48
|
+
for i, line in enumerate(self.lines, 1):
|
|
49
|
+
match = re.match(r'^(#{1,6})\s+(.+)$', line)
|
|
50
|
+
if match:
|
|
51
|
+
title = match.group(2).strip()
|
|
52
|
+
if title.lower() == name.lower():
|
|
53
|
+
start_line = i
|
|
54
|
+
heading_level = len(match.group(1))
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
if not start_line:
|
|
58
|
+
return super().extract_element(element_type, name)
|
|
59
|
+
|
|
60
|
+
# Find the end of this section (next heading of same or higher level)
|
|
61
|
+
end_line = len(self.lines)
|
|
62
|
+
for i in range(start_line, len(self.lines)):
|
|
63
|
+
line = self.lines[i]
|
|
64
|
+
match = re.match(r'^(#{1,6})\s+', line)
|
|
65
|
+
if match:
|
|
66
|
+
level = len(match.group(1))
|
|
67
|
+
if level <= heading_level:
|
|
68
|
+
end_line = i
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# Extract the section
|
|
72
|
+
source = '\n'.join(self.lines[start_line-1:end_line])
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
'name': name,
|
|
76
|
+
'line_start': start_line,
|
|
77
|
+
'line_end': end_line,
|
|
78
|
+
'source': source,
|
|
79
|
+
}
|