build_search_index.py 5.1 KB

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