1
0
Falko Schindler 1 жил өмнө
parent
commit
a2cd28c568

+ 8 - 8
website/documentation/demo.py

@@ -12,14 +12,14 @@ from .windows import browser_window, python_window
 UNCOMMENT_PATTERN = re.compile(r'^(\s*)# ?')
 
 
-def uncomment(text: str) -> str:
-    """non-executed lines should be shown in the code examples"""
-    return UNCOMMENT_PATTERN.sub(r'\1', text)
+def _uncomment(text: str) -> str:
+    return UNCOMMENT_PATTERN.sub(r'\1', text)  # NOTE: non-executed lines should be shown in the code examples
 
 
 def demo(f: Callable) -> Callable:
+    """Render a callable as a demo with Python code and browser window."""
     with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
-        code = inspect.getsource(f).split('# END OF DEMO')[0].strip().splitlines()
+        code = inspect.getsource(f).split('# END OF DEMO', 1)[0].strip().splitlines()
         code = [line for line in code if not line.endswith("# HIDE")]
         while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
             del code[0]
@@ -30,17 +30,17 @@ def demo(f: Callable) -> Callable:
             del code[0]
         indentation = len(code[0]) - len(code[0].lstrip())
         code = [line[indentation:] for line in code]
-        code = ['from nicegui import ui'] + [uncomment(line) for line in code]
+        code = ['from nicegui import ui'] + [_uncomment(line) for line in code]
         code = ['' if line == '#' else line for line in code]
         if not code[-1].startswith('ui.run('):
             code.append('')
             code.append('ui.run()')
-        code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
+        full_code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
         with python_window(classes='w-full max-w-[44rem]'):
             def copy_code():
-                ui.run_javascript('navigator.clipboard.writeText(`' + code + '`)')
+                ui.run_javascript('navigator.clipboard.writeText(`' + full_code + '`)')
                 ui.notify('Copied to clipboard', type='positive', color='primary')
-            ui.markdown(f'````python\n{code}\n````')
+            ui.markdown(f'````python\n{full_code}\n````')
             ui.icon('content_copy', size='xs') \
                 .classes('absolute right-2 top-10 opacity-10 hover:opacity-80 cursor-pointer') \
                 .on('click', copy_code, [])

+ 21 - 7
website/documentation/intro.py

@@ -1,11 +1,15 @@
+from typing import Callable
+
 from nicegui import ui
 
-from .tools import main_page_demo
+from ..style import subheading
+from .demo import demo
 
 
 def create_intro() -> None:
-    @main_page_demo('Styling',
-                    'While having reasonable defaults, you can still modify the look of your app with CSS as well as Tailwind and Quasar classes.')
+    @_main_page_demo('Styling', '''
+        While having reasonable defaults, you can still modify the look of your app with CSS as well as Tailwind and Quasar classes.
+    ''')
     def formatting_demo():
         ui.icon('thumb_up')
         ui.markdown('This is **Markdown**.')
@@ -16,8 +20,9 @@ def create_intro() -> None:
             ui.label('Quasar').classes('q-ml-xl')
         ui.link('NiceGUI on GitHub', 'https://github.com/zauberzeug/nicegui')
 
-    @main_page_demo('Common UI Elements',
-                    'NiceGUI comes with a collection of commonly used UI elements.')
+    @_main_page_demo('Common UI Elements', '''
+        NiceGUI comes with a collection of commonly used UI elements.
+    ''')
     def common_elements_demo():
         from nicegui.events import ValueChangeEventArguments
 
@@ -35,8 +40,9 @@ def create_intro() -> None:
             ui.select(['One', 'Two'], value='One', on_change=show)
         ui.link('And many more...', '/documentation').classes('mt-8')
 
-    @main_page_demo('Value Binding',
-                    'Binding values between UI elements and data models is built into NiceGUI.')
+    @_main_page_demo('Value Binding', '''
+        Binding values between UI elements and data models is built into NiceGUI.
+    ''')
     def binding_demo():
         class Demo:
             def __init__(self):
@@ -48,3 +54,11 @@ def create_intro() -> None:
             ui.slider(min=1, max=3).bind_value(demo, 'number')
             ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
             ui.number().bind_value(demo, 'number')
+
+
+def _main_page_demo(title: str, explanation: str) -> Callable:
+    def decorator(f: Callable) -> Callable:
+        subheading(title, make_menu_entry=False)
+        ui.markdown(explanation).classes('bold-links arrow-links')
+        return demo(f)
+    return decorator

+ 101 - 0
website/documentation/reference.py

@@ -0,0 +1,101 @@
+import inspect
+import re
+from typing import Callable, Optional
+
+import docutils.core
+
+from nicegui import binding, ui
+from nicegui.elements.markdown import apply_tailwind, remove_indentation
+
+from ..style import subheading
+
+
+def generate_class_doc(class_obj: type) -> None:
+    """Generate documentation for a class including all its methods and properties."""
+    mro = [base for base in class_obj.__mro__ if base.__module__.startswith('nicegui.')]
+    ancestors = mro[1:]
+    attributes = {}
+    for base in reversed(mro):
+        for name in dir(base):
+            if not name.startswith('_') and _is_method_or_property(base, name):
+                attributes[name] = getattr(base, name, None)
+    properties = {name: attribute for name, attribute in attributes.items() if not callable(attribute)}
+    methods = {name: attribute for name, attribute in attributes.items() if callable(attribute)}
+
+    if properties:
+        subheading('Properties')
+        with ui.column().classes('gap-2'):
+            for name, property_ in sorted(properties.items()):
+                ui.markdown(f'**`{name}`**`{_generate_property_signature_description(property_)}`')
+                if property_.__doc__:
+                    _render_docstring(property_.__doc__).classes('ml-8')
+    if methods:
+        subheading('Methods')
+        with ui.column().classes('gap-2'):
+            for name, method in sorted(methods.items()):
+                ui.markdown(f'**`{name}`**`{_generate_method_signature_description(method)}`')
+                if method.__doc__:
+                    _render_docstring(method.__doc__).classes('ml-8')
+    if ancestors:
+        subheading('Inherited from')
+        with ui.column().classes('gap-2'):
+            for ancestor in ancestors:
+                ui.markdown(f'- `{ancestor.__name__}`')
+
+
+def _is_method_or_property(cls: type, attribute_name: str) -> bool:
+    attribute = cls.__dict__.get(attribute_name, None)
+    return (
+        inspect.isfunction(attribute) or
+        inspect.ismethod(attribute) or
+        isinstance(attribute, (property, binding.BindableProperty))
+    )
+
+
+def _generate_property_signature_description(property_: Optional[property]) -> str:
+    description = ''
+    if property_ is None:
+        return ': BindableProperty'
+    if property_.fget:
+        return_annotation = inspect.signature(property_.fget).return_annotation
+        if return_annotation != inspect.Parameter.empty:
+            return_type = inspect.formatannotation(return_annotation)
+            description += f': {return_type}'
+    if property_.fset:
+        description += ' (settable)'
+    if property_.fdel:
+        description += ' (deletable)'
+    return description
+
+
+def _generate_method_signature_description(method: Callable) -> str:
+    param_strings = []
+    for param in inspect.signature(method).parameters.values():
+        param_string = param.name
+        if param_string == 'self':
+            continue
+        if param.annotation != inspect.Parameter.empty:
+            param_type = inspect.formatannotation(param.annotation)
+            param_string += f''': {param_type.strip("'")}'''
+        if param.default != inspect.Parameter.empty:
+            param_string += ' = [...]' if callable(param.default) else f' = {repr(param.default)}'
+        if param.kind == inspect.Parameter.VAR_POSITIONAL:
+            param_string = f'*{param_string}'
+        param_strings.append(param_string)
+    method_signature = ', '.join(param_strings)
+    description = f'({method_signature})'
+    return_annotation = inspect.signature(method).return_annotation
+    if return_annotation != inspect.Parameter.empty:
+        return_type = inspect.formatannotation(return_annotation)
+        description += f''' -> {return_type.strip("'").replace("typing_extensions.", "").replace("typing.", "")}'''
+    return description
+
+
+def _render_docstring(doc: str, with_params: bool = True) -> ui.html:
+    doc = remove_indentation(doc)
+    doc = doc.replace('param ', '')
+    html = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
+    html = apply_tailwind(html)
+    if not with_params:
+        html = re.sub(r'<dl class=".* simple">.*?</dl>', '', html, flags=re.DOTALL)
+    return ui.html(html).classes('documentation bold-links arrow-links')

+ 4 - 10
website/documentation/rendering.py

@@ -1,10 +1,10 @@
 from nicegui import ui
 
 from ..header import add_head_html, add_header
-from ..style import section_heading
+from ..style import section_heading, subheading
 from .demo import demo
 from .model import Documentation, UiElementDocumentation
-from .tools import generate_class_doc
+from .reference import generate_class_doc
 
 
 def render_page(documentation: Documentation, *, is_main: bool = False) -> None:
@@ -24,9 +24,6 @@ def render_page(documentation: Documentation, *, is_main: bool = False) -> None:
                 .style('height: calc(100% + 20px) !important') as menu:
             ui.markdown(f'[← back]({documentation.back_link})').classes('bold-links')
             ui.markdown(f'**{documentation.title.replace("*", "")}**').classes('mt-4')
-            for part in documentation:
-                if part.title and part.link_target:
-                    ui.link(part.title, f'#{part.link_target}')
 
     # content
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
@@ -39,11 +36,8 @@ def render_page(documentation: Documentation, *, is_main: bool = False) -> None:
             if part.title:
                 if part.link_target:
                     ui.link_target(part.link_target)
-                if part.link and part.link != documentation.route:
-                    with ui.link(target=part.link):
-                        ui.markdown(f'### {part.title}')
-                else:
-                    ui.markdown(f'### {part.title}')
+                link = part.link if part.link != documentation.route else None
+                subheading(part.title, make_menu_entry=not is_main, link=link)
             if part.description:
                 ui.markdown(part.description)
             if part.ui:

+ 0 - 191
website/documentation/tools.py

@@ -1,191 +0,0 @@
-import importlib
-import inspect
-import re
-from typing import Callable, Optional, Union
-
-import docutils.core
-
-from nicegui import context, ui
-from nicegui.binding import BindableProperty
-from nicegui.elements.markdown import apply_tailwind, remove_indentation
-
-from .demo import demo
-
-SPECIAL_CHARACTERS = re.compile('[^(a-z)(A-Z)(0-9)-]')
-
-
-def pascal_to_snake(name: str) -> str:
-    return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
-
-
-def create_anchor_name(text: str) -> str:
-    return SPECIAL_CHARACTERS.sub('_', text).lower()
-
-
-def get_menu() -> ui.left_drawer:
-    return [element for element in context.get_client().elements.values() if isinstance(element, ui.left_drawer)][0]
-
-
-def heading(text: str, *, make_menu_entry: bool = True) -> None:
-    ui.link_target(create_anchor_name(text))
-    ui.html(f'<em>{text}</em>').classes('mt-8 text-3xl font-weight-500')
-    if make_menu_entry:
-        with get_menu():
-            ui.label(text).classes('font-bold mt-4')
-
-
-def subheading(text: str, *, make_menu_entry: bool = True, more_link: Optional[str] = None) -> None:
-    name = create_anchor_name(text)
-    ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
-    with ui.row().classes('gap-2 items-center relative'):
-        if more_link:
-            ui.link(text, f'/documentation/{more_link}').classes('text-2xl')
-        else:
-            ui.label(text).classes('text-2xl')
-        with ui.link(target=f'#{name}').classes('absolute').style('transform: translateX(-150%)'):
-            ui.icon('link', size='sm').classes('opacity-10 hover:opacity-80')
-    if make_menu_entry:
-        with get_menu() as menu:
-            async def click():
-                if await ui.run_javascript('!!document.querySelector("div.q-drawer__backdrop")', timeout=5.0):
-                    menu.hide()
-                    ui.open(f'#{name}')
-            ui.link(text, target=f'#{name}').props('data-close-overlay').on('click', click, [])
-
-
-def render_docstring(doc: str, with_params: bool = True) -> ui.html:
-    doc = remove_indentation(doc)
-    doc = doc.replace('param ', '')
-    html = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
-    html = apply_tailwind(html)
-    if not with_params:
-        html = re.sub(r'<dl class=".* simple">.*?</dl>', '', html, flags=re.DOTALL)
-    return ui.html(html).classes('documentation bold-links arrow-links')
-
-
-class text_demo:
-
-    def __init__(self, title: str, explanation: str, *,
-                 tab: Optional[Union[str, Callable]] = None,
-                 more_link: Optional[str] = None,
-                 make_menu_entry: bool = True
-                 ) -> None:
-        self.title = title
-        self.explanation = explanation
-        self.make_menu_entry = make_menu_entry
-        self.tab = tab
-        self.more_link = more_link
-
-    def __call__(self, f: Callable) -> Callable:
-        subheading(self.title, make_menu_entry=self.make_menu_entry, more_link=self.more_link)
-        ui.markdown(self.explanation).classes('bold-links arrow-links')
-        f.tab = self.tab
-        return demo(f)
-
-
-class main_page_demo(text_demo):
-
-    def __init__(self, title: str, explanation: str) -> None:
-        super().__init__(title, explanation, make_menu_entry=False)
-
-
-class element_demo:
-
-    def __init__(self, element_class: Union[Callable, type, str]) -> None:
-        if isinstance(element_class, str):
-            module = importlib.import_module(f'website.documentation.more.{element_class}_documentation')
-            element_class = getattr(module, 'main_demo')
-        self.element_class = element_class
-
-    def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
-        doc = f.__doc__ or self.element_class.__doc__ or self.element_class.__init__.__doc__  # type: ignore
-        title, documentation = doc.split('\n', 1)
-        with ui.column().classes('w-full mb-8 gap-2'):
-            if more_link:
-                subheading(title, more_link=more_link)
-            render_docstring(documentation, with_params=more_link is None)
-            result = demo(f)
-            if more_link:
-                ui.markdown(f'See [more...](/documentation/{more_link})').classes('bold-links arrow-links')
-        return result
-
-
-def is_method_or_property(cls: type, attribute_name: str) -> bool:
-    attribute = cls.__dict__.get(attribute_name, None)
-    return (
-        inspect.isfunction(attribute) or
-        inspect.ismethod(attribute) or
-        isinstance(attribute, property) or
-        isinstance(attribute, BindableProperty)
-    )
-
-
-def generate_class_doc(class_obj: type) -> None:
-    mro = [base for base in class_obj.__mro__ if base.__module__.startswith('nicegui.')]
-    ancestors = mro[1:]
-    attributes = {}
-    for base in reversed(mro):
-        for name in dir(base):
-            if not name.startswith('_') and is_method_or_property(base, name):
-                attributes[name] = getattr(base, name, None)
-    properties = {name: attribute for name, attribute in attributes.items() if not callable(attribute)}
-    methods = {name: attribute for name, attribute in attributes.items() if callable(attribute)}
-
-    if properties:
-        subheading('Properties')
-        with ui.column().classes('gap-2'):
-            for name, property_ in sorted(properties.items()):
-                ui.markdown(f'**`{name}`**`{generate_property_signature_description(property_)}`')
-                if property_.__doc__:
-                    render_docstring(property_.__doc__).classes('ml-8')
-    if methods:
-        subheading('Methods')
-        with ui.column().classes('gap-2'):
-            for name, method in sorted(methods.items()):
-                ui.markdown(f'**`{name}`**`{generate_method_signature_description(method)}`')
-                if method.__doc__:
-                    render_docstring(method.__doc__).classes('ml-8')
-    if ancestors:
-        subheading('Inherited from')
-        with ui.column().classes('gap-2'):
-            for ancestor in ancestors:
-                ui.markdown(f'- `{ancestor.__name__}`')
-
-
-def generate_method_signature_description(method: Callable) -> str:
-    param_strings = []
-    for param in inspect.signature(method).parameters.values():
-        param_string = param.name
-        if param_string == 'self':
-            continue
-        if param.annotation != inspect.Parameter.empty:
-            param_type = inspect.formatannotation(param.annotation)
-            param_string += f''': {param_type.strip("'")}'''
-        if param.default != inspect.Parameter.empty:
-            param_string += ' = [...]' if callable(param.default) else f' = {repr(param.default)}'
-        if param.kind == inspect.Parameter.VAR_POSITIONAL:
-            param_string = f'*{param_string}'
-        param_strings.append(param_string)
-    method_signature = ', '.join(param_strings)
-    description = f'({method_signature})'
-    return_annotation = inspect.signature(method).return_annotation
-    if return_annotation != inspect.Parameter.empty:
-        return_type = inspect.formatannotation(return_annotation)
-        description += f''' -> {return_type.strip("'").replace("typing_extensions.", "").replace("typing.", "")}'''
-    return description
-
-
-def generate_property_signature_description(property_: Optional[property]) -> str:
-    description = ''
-    if property_ is None:
-        return ': BindableProperty'
-    if property_.fget:
-        return_annotation = inspect.signature(property_.fget).return_annotation
-        if return_annotation != inspect.Parameter.empty:
-            return_type = inspect.formatannotation(return_annotation)
-            description += f': {return_type}'
-    if property_.fset:
-        description += ' (settable)'
-    if property_.fdel:
-        description += ' (deletable)'
-    return description

+ 52 - 12
website/style.py

@@ -1,53 +1,93 @@
+import re
 from pathlib import Path
-from typing import List
+from typing import List, Optional
 
-from nicegui import ui
+from nicegui import context, ui
+
+SPECIAL_CHARACTERS = re.compile('[^(a-z)(A-Z)(0-9)-]')
 
 
 def link_target(name: str, offset: str = '0') -> ui.link_target:
+    """Create a link target that can be linked to with a hash."""
     target = ui.link_target(name).style(f'position: absolute; top: {offset}; left: 0')
+    assert target.parent_slot is not None
     target.parent_slot.parent.classes('relative')
     return target
 
 
-def section_heading(subtitle: str, title: str) -> None:
-    ui.label(subtitle).classes('md:text-lg font-bold')
-    ui.markdown(title).classes('text-3xl md:text-5xl font-medium mt-[-12px]')
+def section_heading(subtitle_: str, title_: str) -> None:
+    """Render a section heading with a subtitle."""
+    ui.label(subtitle_).classes('md:text-lg font-bold')
+    ui.markdown(title_).classes('text-3xl md:text-5xl font-medium mt-[-12px]')
 
 
-def heading(title: str) -> ui.label:
-    return ui.markdown(title).classes('text-2xl md:text-3xl xl:text-4xl font-medium text-white')
+def heading(title_: str) -> ui.markdown:
+    """Render a heading."""
+    return ui.markdown(title_).classes('text-2xl md:text-3xl xl:text-4xl font-medium text-white')
 
 
 def title(content: str) -> ui.markdown:
+    """Render a title."""
     return ui.markdown(content).classes('text-4xl sm:text-5xl md:text-6xl font-medium')
 
 
 def subtitle(content: str) -> ui.markdown:
+    """Render a subtitle."""
     return ui.markdown(content).classes('text-xl sm:text-2xl md:text-3xl leading-7')
 
 
-def example_link(title: str, description: str) -> None:
-    name = title.lower().replace(' ', '_')
+def example_link(title_: str, description: str) -> None:
+    """Render a link to an example."""
+    name = title_.lower().replace(' ', '_')
     directory = Path(__file__).parent.parent / 'examples' / name
     content = [p for p in directory.glob('*') if p.name != '__pycache__' and not p.name.startswith('.')]
     filename = 'main.py' if len(content) == 1 else ''
     with ui.link(target=f'https://github.com/zauberzeug/nicegui/tree/main/examples/{name}/{filename}') \
             .classes('bg-[#5898d420] p-4 self-stretch rounded flex flex-col gap-2') \
             .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
-        ui.label(title).classes(replace='font-bold')
+        ui.label(title_).classes(replace='font-bold')
         ui.markdown(description).classes(replace='bold-links arrow-links')
 
 
-def features(icon: str, title: str, items: List[str]) -> None:
+def features(icon: str, title_: str, items: List[str]) -> None:
+    """Render a list of features."""
     with ui.column().classes('gap-1'):
         ui.icon(icon).classes('max-sm:hidden text-3xl md:text-5xl mb-3 text-primary opacity-80')
-        ui.label(title).classes('font-bold mb-3')
+        ui.label(title_).classes('font-bold mb-3')
         for item in items:
             ui.markdown(f'- {item}').classes('bold-links arrow-links')
 
 
 def side_menu() -> ui.left_drawer:
+    """Render the side menu."""
     return ui.left_drawer() \
         .classes('column no-wrap gap-1 bg-[#eee] dark:bg-[#1b1b1b] mt-[-20px] px-8 py-20') \
         .style('height: calc(100% + 20px) !important')
+
+
+def subheading(text: str, *, make_menu_entry: bool = True, link: Optional[str] = None) -> None:
+    """Render a subheading with an anchor that can be linked to with a hash."""
+    name = _create_anchor_name(text)
+    ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
+    with ui.row().classes('gap-2 items-center relative'):
+        if link:
+            ui.link(text, link).classes('text-2xl')
+        else:
+            ui.label(text).classes('text-2xl')
+        with ui.link(target=f'#{name}').classes('absolute').style('transform: translateX(-150%)'):
+            ui.icon('link', size='sm').classes('opacity-10 hover:opacity-80')
+    if make_menu_entry:
+        with _get_menu() as menu:
+            async def click():
+                if await ui.run_javascript('!!document.querySelector("div.q-drawer__backdrop")', timeout=5.0):
+                    menu.hide()
+                    ui.open(f'#{name}')
+            ui.link(text, target=f'#{name}').props('data-close-overlay').on('click', click, [])
+
+
+def _create_anchor_name(text: str) -> str:
+    return SPECIAL_CHARACTERS.sub('_', text).lower()
+
+
+def _get_menu() -> ui.left_drawer:
+    return [element for element in context.get_client().elements.values() if isinstance(element, ui.left_drawer)][0]