build_search_index.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env python3
  2. import ast
  3. import inspect
  4. import json
  5. import os
  6. import re
  7. from _ast import AsyncFunctionDef
  8. from pathlib import Path
  9. from typing import List, Optional, Union
  10. from nicegui import app, ui
  11. dir_path = Path(__file__).parent
  12. os.chdir(dir_path)
  13. def ast_string_node_to_string(node):
  14. if isinstance(node, ast.Str):
  15. return node.s
  16. elif isinstance(node, ast.JoinedStr):
  17. return ''.join(ast_string_node_to_string(part) for part in node.values)
  18. else:
  19. return str(ast.unparse(node))
  20. def cleanup(markdown_string: str) -> str:
  21. # Remove link URLs but keep the description
  22. markdown_string = re.sub(r'\[([^\[]+)\]\([^\)]+\)', r'\1', markdown_string)
  23. # Remove inline code ticks
  24. markdown_string = re.sub(r'`([^`]+)`', r'\1', markdown_string)
  25. # Remove code blocks
  26. markdown_string = re.sub(r'```([^`]+)```', r'\1', markdown_string)
  27. markdown_string = re.sub(r'``([^`]+)``', r'\1', markdown_string)
  28. # Remove braces
  29. markdown_string = re.sub(r'\{([^\}]+)\}', r'\1', markdown_string)
  30. return markdown_string
  31. class DocVisitor(ast.NodeVisitor):
  32. def __init__(self, topic: Optional[str] = None) -> None:
  33. super().__init__()
  34. self.topic = topic
  35. self.current_title = None
  36. self.current_content: List[str] = []
  37. def visit_Call(self, node: ast.Call):
  38. if isinstance(node.func, ast.Name):
  39. function_name = node.func.id
  40. elif isinstance(node.func, ast.Attribute):
  41. function_name = node.func.attr
  42. else:
  43. raise NotImplementedError(f'Unknown function type: {node.func}')
  44. if function_name in ['heading', 'subheading']:
  45. self._handle_new_heading()
  46. self.current_title = node.args[0].s
  47. elif function_name == 'markdown':
  48. if node.args:
  49. raw = ast_string_node_to_string(node.args[0]).splitlines()
  50. raw = ' '.join(l.strip() for l in raw).strip()
  51. self.current_content.append(cleanup(raw))
  52. self.generic_visit(node)
  53. def _handle_new_heading(self) -> None:
  54. if self.current_title:
  55. self.add_to_search_index(self.current_title, self.current_content if self.current_content else 'Overview')
  56. self.current_content = []
  57. def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> None:
  58. self.visit_FunctionDef(node)
  59. def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
  60. if node.name == 'main_demo':
  61. docstring = ast.get_docstring(node)
  62. if docstring is None:
  63. api = getattr(ui, self.topic) if hasattr(ui, self.topic) else getattr(app, self.topic)
  64. docstring = api.__doc__ or api.__init__.__doc__
  65. for name, method in api.__dict__.items():
  66. if not name.startswith('_') and inspect.isfunction(method):
  67. # add method name to docstring
  68. docstring += name + ' '
  69. docstring += method.__doc__ or ''
  70. lines = cleanup(docstring).splitlines()
  71. self.add_to_search_index(lines[0], lines[1:], main=True)
  72. for decorator in node.decorator_list:
  73. if isinstance(decorator, ast.Call):
  74. function = decorator.func
  75. if isinstance(function, ast.Name) and function.id == 'text_demo':
  76. title = decorator.args[0].s
  77. content = cleanup(decorator.args[1].s).splitlines()
  78. self.add_to_search_index(title, content)
  79. if isinstance(function, ast.Name) and function.id == 'element_demo':
  80. attr_name = decorator.args[0].attr
  81. obj_name = decorator.args[0].value.id
  82. if obj_name == 'app':
  83. docstring: str = getattr(app, attr_name).__doc__
  84. docstring = ' '.join(l.strip() for l in docstring.splitlines()).strip()
  85. self.current_content.append(cleanup(docstring))
  86. else:
  87. print(f'Unknown object: {obj_name} for element_demo', flush=True)
  88. self.generic_visit(node)
  89. def add_to_search_index(self, title: str, content: Union[str, list], main: bool = False) -> None:
  90. if isinstance(content, list):
  91. content_str = ' '.join(l.strip() for l in content).strip()
  92. else:
  93. content_str = content
  94. anchor = title.lower().replace(' ', '_')
  95. url = f'/documentation/{self.topic or ""}'
  96. if not main:
  97. url += f'#{anchor}'
  98. if self.topic:
  99. title = f'{self.topic.replace("_", " ").title()}: {title}'
  100. documents.append({
  101. 'title': title,
  102. 'content': content_str,
  103. 'url': url,
  104. })
  105. class MainVisitor(ast.NodeVisitor):
  106. def visit_Call(self, node: ast.Call):
  107. if isinstance(node.func, ast.Name):
  108. function_name = node.func.id
  109. elif isinstance(node.func, ast.Attribute):
  110. function_name = node.func.attr
  111. else:
  112. return
  113. if function_name == 'example_link':
  114. title = ast_string_node_to_string(node.args[0])
  115. name = title.lower().replace(' ', '_')
  116. path = Path(__file__).parent.parent / 'examples' / name
  117. file = 'main.py' if (path / 'main.py').is_file() else ''
  118. documents.append({
  119. 'title': 'Example: ' + title,
  120. 'content': ast_string_node_to_string(node.args[1]),
  121. 'url': f'https://github.com/zauberzeug/nicegui/tree/main/examples/{name}/{file}',
  122. })
  123. def generate_for(file: Path, topic: Optional[str] = None) -> None:
  124. tree = ast.parse(file.read_text())
  125. doc_visitor = DocVisitor(topic)
  126. doc_visitor.visit(tree)
  127. if doc_visitor.current_title:
  128. doc_visitor._handle_new_heading() # to finalize the last heading
  129. documents = []
  130. tree = ast.parse(Path('../main.py').read_text())
  131. MainVisitor().visit(tree)
  132. generate_for(Path('./documentation.py'))
  133. for file in Path('./more_documentation').glob('*.py'):
  134. generate_for(file, file.stem.removesuffix('_documentation'))
  135. with open('static/search_index.json', 'w') as f:
  136. json.dump(documents, f, indent=2)