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.
@@ -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
+ }