diff --git a/examples/autolinks/_quarto.yml b/examples/autolinks/_quarto.yml new file mode 100644 index 00000000..0e4873a3 --- /dev/null +++ b/examples/autolinks/_quarto.yml @@ -0,0 +1,23 @@ +project: + type: website + +# tell quarto to read the generated sidebar +metadata-files: + - _sidebar.yml + + +quartodoc: + # the name used to import the package you want to create reference docs for + package: quartodoc + + # write sidebar data to this file + sidebar: _sidebar.yml + + sections: + - title: Some functions + desc: Functions to inspect docstrings. + contents: + # the functions being documented in the package. + # you can refer to anything: class methods, modules, etc.. + - get_object + - preview diff --git a/examples/autolinks/app.py b/examples/autolinks/app.py new file mode 100644 index 00000000..29dd9d22 --- /dev/null +++ b/examples/autolinks/app.py @@ -0,0 +1,20 @@ +from shiny.express import render, ui, input +from quartodoc.interlinks_auto import CallVisitor + +ui.input_text_area("raw_code", "Code"), + + +@render.code +def code(): + res = CallVisitor().analyze(input.raw_code()) + chars = [list(row) for row in input.raw_code().split("\n")] + print(chars) + for inter in res: + ii, jj = inter.lineno - 1, inter.col_offset + chars[ii][jj] = "<--" + chars[ii][jj] + + ii, jj = inter.end_lineno - 1, inter.end_col_offset - 1 + chars[ii][jj] = chars[ii][jj] + "-->" + + final = "\n".join(["".join(row) for row in chars]) + return final diff --git a/examples/autolinks/index.qmd b/examples/autolinks/index.qmd new file mode 100644 index 00000000..0e0d83ca --- /dev/null +++ b/examples/autolinks/index.qmd @@ -0,0 +1,13 @@ +--- +title: Autolink example +--- + +```python +from great_tables import GT, exibble + +( + GT(exibble) + .tab_header("I am a header") + .tab_source_note("I am a source not") +) +``` \ No newline at end of file diff --git a/quartodoc/ast.py b/quartodoc/ast.py index 3a3381d4..bf82b237 100644 --- a/quartodoc/ast.py +++ b/quartodoc/ast.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ast import warnings from enum import Enum @@ -278,6 +279,16 @@ def fields(el: object): return None +@dispatch +def fields(el: ast.AST): + return el._fields + + +@dispatch +def fields(el: ast.Attribute | ast.Call | ast.Name): + return el._fields + ("col_offset", "end_col_offset") + + class Formatter: n_spaces = 3 icon_block = "█─" diff --git a/quartodoc/interlinks_auto.py b/quartodoc/interlinks_auto.py new file mode 100644 index 00000000..e54929c3 --- /dev/null +++ b/quartodoc/interlinks_auto.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import jedi + +import ast +from fastapi import FastAPI +from pydantic import BaseModel +import uvicorn + +from typing_extensions import TypeAlias, Union + +ASTItems: TypeAlias = Union[ast.Call, ast.Name, ast.Attribute] + + +class CallVisitor(ast.NodeVisitor): + results: list[ASTItems] + + def __init__(self): + self.results = [] + + def visit_Call(self, node): + if isinstance(node.func, ast.Attribute): + self.results.append(node) + return self.visit(node.func.value) + elif isinstance(node.func, ast.Name): + self.results.append(node) + + def visit_Name(self, node): + self.results.append(node) + + def visit_Attribute(self, node): + self.results.append(node) + + def reset(self): + self.results = [] + + return self + + @classmethod + def analyze(cls, code) -> list[Interlink]: + visitor = cls() + visitor.visit(ast.parse(code)) + script = jedi.Script(code) + all_links = [node_to_interlink_name(script, call) for call in visitor.results] + return [link for link in all_links if link is not None] + + +def narrow_node_start(node: ast.AST) -> tuple[int, int]: + if isinstance(node, ast.Attribute): + return (node.value.end_lineno, node.value.end_col_offset) + + return node.lineno, node.col_offset + + +def node_to_interlink_name(script: jedi.Script, node: ast.AST) -> Interlink | None: + print(node.func.lineno, node.func.end_col_offset) + try: + func = node.func + name = script.goto(func.lineno, func.end_col_offset)[0] + full_name = name.full_name + lineno, col_offset = narrow_node_start(func) + return Interlink( + name=full_name, + lineno=lineno, + end_lineno=func.end_lineno, + col_offset=col_offset, + end_col_offset=func.end_col_offset, + ) + except IndexError: + return None + + +class Code(BaseModel): + content: str + + +class Interlink(BaseModel): + name: str + lineno: int + end_lineno: int + col_offset: int + end_col_offset: int + + +app = FastAPI() + + +@app.post("/analyze") +async def analyze(code: Code): + res = CallVisitor.analyze(code.content) + return {"interlinks": res} + + +if __name__ == "__main__": + import sys + + port = int(sys.argv[1]) + uvicorn.run(app, port=port, host="0.0.0.0")