add first proc doc
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import re
|
||||
from typing import Dict, List
|
||||
|
||||
from tclint.syntax_tree import Visitor, Command
|
||||
from tools.parser import CustomParser
|
||||
|
||||
|
||||
def _strip_comment_prefix(line: str) -> str:
|
||||
"""Strip leading '# ' or '#' from a line."""
|
||||
if line.lstrip().startswith("#"):
|
||||
# remove up to one leading '#' and one optional following space
|
||||
return re.sub(r"^\s*#\s?", "", line)
|
||||
return line
|
||||
|
||||
|
||||
def extract_doc_block_above(lines: List[str], start_line_index: int) -> str | None:
|
||||
"""
|
||||
Extract a contiguous block of line comments immediately above the given line index.
|
||||
|
||||
- lines: document split into lines
|
||||
- start_line_index: 0-based index of the line where the proc command starts
|
||||
|
||||
Returns the cleaned documentation text or None if no comment block found.
|
||||
"""
|
||||
i = start_line_index - 1
|
||||
if i < 0:
|
||||
return None
|
||||
|
||||
doc_lines: List[str] = []
|
||||
|
||||
# Skip trailing empty lines directly above
|
||||
while i >= 0 and lines[i].strip() == "":
|
||||
i -= 1
|
||||
|
||||
# Collect contiguous comment lines going upwards
|
||||
while i >= 0 and lines[i].lstrip().startswith("#"):
|
||||
doc_lines.append(lines[i])
|
||||
i -= 1
|
||||
|
||||
if not doc_lines:
|
||||
return None
|
||||
|
||||
# Reverse to original order and strip comment prefixes
|
||||
doc_lines.reverse()
|
||||
cleaned = [_strip_comment_prefix(l) for l in doc_lines]
|
||||
|
||||
# Simple tag -> markdown conversions for nicer rendering
|
||||
md_lines: List[str] = []
|
||||
tag_map = {
|
||||
"<Documentation>": "### Documentation",
|
||||
"<Arguments>": "### Arguments",
|
||||
"<Returnvalue>": "### Return value",
|
||||
"<Example>": "### Example",
|
||||
"<Internal Documentation>": "### Internal Documentation",
|
||||
"<Internal Example>": "### Internal Example",
|
||||
}
|
||||
|
||||
for line in cleaned:
|
||||
stripped = line.strip()
|
||||
if stripped in tag_map:
|
||||
md_lines.append(tag_map[stripped])
|
||||
else:
|
||||
md_lines.append(line)
|
||||
|
||||
return "\n".join(md_lines).rstrip()
|
||||
|
||||
|
||||
class ProcDocExtractor(Visitor):
|
||||
"""Visitor that collects documentation blocks above proc declarations."""
|
||||
|
||||
def __init__(self, source_text: str):
|
||||
super().__init__()
|
||||
self._lines = source_text.split("\n")
|
||||
self.docs: Dict[str, str] = {}
|
||||
|
||||
def visit_command(self, command: Command):
|
||||
routine = getattr(command.routine, "contents", None)
|
||||
if routine != "proc":
|
||||
return
|
||||
|
||||
if not command.args:
|
||||
return
|
||||
name_node = command.args[0]
|
||||
proc_name = getattr(name_node, "contents", None)
|
||||
if not proc_name:
|
||||
return
|
||||
|
||||
# Prefer line of the 'proc' keyword; fallback to the name node
|
||||
pos = getattr(command.routine, "pos", None) or getattr(name_node, "pos", None)
|
||||
if not pos:
|
||||
return
|
||||
line_idx = pos[0] - 1 # 0-based
|
||||
block = extract_doc_block_above(self._lines, line_idx)
|
||||
if block:
|
||||
self.docs[proc_name] = block
|
||||
|
||||
|
||||
def build_proc_docs(tree, source_text: str) -> Dict[str, str]:
|
||||
"""Build a mapping of proc name -> markdown doc from a parsed tree and source text."""
|
||||
extractor = ProcDocExtractor(source_text)
|
||||
tree.accept(extractor, recurse=True)
|
||||
return extractor.docs
|
||||
|
||||
|
||||
def is_proc_declaration_position(source_text: str, line_zero_based: int, char_zero_based: int) -> bool:
|
||||
"""Return True if the position is on a proc name within its declaration."""
|
||||
parser = CustomParser()
|
||||
tree = parser.parse(source_text)
|
||||
|
||||
# Walk commands to find 'proc' declarations and check if position intersects the name arg
|
||||
class _DeclFinder(Visitor):
|
||||
def __init__(self):
|
||||
self.is_decl = False
|
||||
|
||||
def visit_command(self, command: Command):
|
||||
if self.is_decl:
|
||||
return
|
||||
routine = getattr(command.routine, "contents", None)
|
||||
if routine != "proc" or not command.args:
|
||||
return
|
||||
name_node = command.args[0]
|
||||
if not hasattr(name_node, "pos"):
|
||||
return
|
||||
# Calculate range for the name token
|
||||
try:
|
||||
start_line, start_col = name_node.pos
|
||||
end_line, end_col = getattr(name_node, "end_pos", name_node.pos)
|
||||
except Exception:
|
||||
return
|
||||
if start_line - 1 == line_zero_based:
|
||||
length = 0
|
||||
if hasattr(name_node, "value") and name_node.value is not None:
|
||||
length = len(name_node.value)
|
||||
elif hasattr(name_node, "contents") and name_node.contents is not None:
|
||||
length = len(name_node.contents)
|
||||
if length:
|
||||
start_c = start_col - 1
|
||||
end_c = start_c + length
|
||||
if start_c <= char_zero_based <= end_c:
|
||||
self.is_decl = True
|
||||
|
||||
finder = _DeclFinder()
|
||||
tree.accept(finder, recurse=True)
|
||||
return finder.is_decl
|
||||
Reference in New Issue
Block a user