瀏覽代碼

Merge branch 'update_queue' of github.com:zauberzeug/nicegui into update_queue

Falko Schindler 2 年之前
父節點
當前提交
216d315c45

+ 19 - 13
nicegui/element.py

@@ -1,6 +1,7 @@
 from __future__ import annotations
 
-import shlex
+import json
+import re
 from abc import ABC
 from copy import deepcopy
 from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union
@@ -13,6 +14,8 @@ from .slot import Slot
 if TYPE_CHECKING:
     from .client import Client
 
+PROPS_PATTERN = re.compile(r'([\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
+
 
 class Element(ABC, Visibility):
 
@@ -94,7 +97,13 @@ class Element(ABC, Visibility):
 
     @staticmethod
     def _parse_style(text: Optional[str]) -> Dict[str, str]:
-        return dict(_split(part, ':') for part in text.strip('; ').split(';')) if text else {}
+        result = {}
+        for word in (text or '').split(';'):
+            word = word.strip()
+            if word:
+                key, value = word.split(':', 1)
+                result[key.strip()] = value.strip()
+        return result
 
     def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
         '''CSS style sheet definitions to modify the look of the element.
@@ -115,12 +124,14 @@ class Element(ABC, Visibility):
 
     @staticmethod
     def _parse_props(text: Optional[str]) -> Dict[str, Any]:
-        if not text:
-            return {}
-        lexer = shlex.shlex(text, posix=True)
-        lexer.whitespace = ' '
-        lexer.wordchars += '=-.%:/'
-        return dict(_split(word, '=') if '=' in word else (word, True) for word in lexer)
+        dictionary = {}
+        for match in PROPS_PATTERN.finditer(text or ''):
+            key = match.group(1)
+            value = match.group(2) or match.group(3)
+            if value and value.startswith('"') and value.endswith('"'):
+                value = json.loads(value)
+            dictionary[key] = value or True
+        return dictionary
 
     def props(self, add: Optional[str] = None, *, remove: Optional[str] = None):
         '''Quasar props https://quasar.dev/vue-components/button#design to modify the look of the element.
@@ -201,8 +212,3 @@ class Element(ABC, Visibility):
 
         Can be overridden to perform cleanup.
         """
-
-
-def _split(text: str, separator: str) -> Tuple[str, str]:
-    words = text.split(separator, 1)
-    return words[0].strip(), words[1].strip()

File diff suppressed because it is too large
+ 0 - 0
nicegui/elements/lib/mermaid.min.js


+ 21 - 0
nicegui/elements/markdown.js

@@ -0,0 +1,21 @@
+export default {
+  template: `<div></div>`,
+  mounted() {
+    this.update(this.$el.innerHTML);
+  },
+  methods: {
+    update(content) {
+      this.$el.innerHTML = content;
+      this.$el.querySelectorAll(".mermaid-pre").forEach((pre, i) => {
+        const code = decodeHtml(pre.children[0].innerHTML);
+        mermaid.render(`mermaid_${this.$el.id}_${i}`, code, (svg) => (pre.innerHTML = svg));
+      });
+    },
+  },
+};
+
+function decodeHtml(html) {
+  const txt = document.createElement("textarea");
+  txt.innerHTML = html;
+  return txt.value;
+}

+ 21 - 18
nicegui/elements/markdown.py

@@ -5,24 +5,10 @@ from typing import List
 
 import markdown2
 
+from ..dependencies import register_component
 from .mixins.content_element import ContentElement
 
-
-def apply_tailwind(html: str) -> str:
-    rep = {
-        '<h1': '<h1 class="text-5xl mb-4 mt-6"',
-        '<h2': '<h2 class="text-4xl mb-3 mt-5"',
-        '<h3': '<h3 class="text-3xl mb-2 mt-4"',
-        '<h4': '<h4 class="text-2xl mb-1 mt-3"',
-        '<h5': '<h5 class="text-1xl mb-0.5 mt-2"',
-        '<a': '<a class="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"',
-        '<ul': '<ul class="list-disc ml-6"',
-        '<p>': '<p class="mb-2">',
-        '<div\ class="codehilite">': '<div class="codehilite mb-2 p-2">',
-        '<code': '<code style="background-color: transparent"',
-    }
-    pattern = re.compile('|'.join(rep.keys()))
-    return pattern.sub(lambda m: rep[re.escape(m.group(0))], html)
+register_component('markdown', __file__, 'markdown.js', ['lib/mermaid.min.js'])
 
 
 class Markdown(ContentElement):
@@ -36,16 +22,33 @@ class Markdown(ContentElement):
         :param extras: list of `markdown2 extensions <https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras>`_ (default: `['fenced-code-blocks', 'tables']`)
         """
         self.extras = extras
-        super().__init__(tag='div', content=content)
+        super().__init__(tag='markdown', content=content)
 
     def on_content_change(self, content: str) -> None:
         html = prepare_content(content, extras=' '.join(self.extras))
         if self._props.get('innerHTML') != html:
             self._props['innerHTML'] = html
-            self.update()
+            self.run_method('update', html)
 
 
 @lru_cache(maxsize=int(os.environ.get('MARKDOWN_CONTENT_CACHE_SIZE', '1000')))
 def prepare_content(content: str, extras: str) -> str:
     html = markdown2.markdown(content, extras=extras.split())
     return apply_tailwind(html)  # we need explicit markdown styling because tailwind CSS removes all default styles
+
+
+def apply_tailwind(html: str) -> str:
+    rep = {
+        '<h1': '<h1 class="text-5xl mb-4 mt-6"',
+        '<h2': '<h2 class="text-4xl mb-3 mt-5"',
+        '<h3': '<h3 class="text-3xl mb-2 mt-4"',
+        '<h4': '<h4 class="text-2xl mb-1 mt-3"',
+        '<h5': '<h5 class="text-1xl mb-0.5 mt-2"',
+        '<a': '<a class="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"',
+        '<ul': '<ul class="list-disc ml-6"',
+        '<p>': '<p class="mb-2">',
+        '<div\ class="codehilite">': '<div class="codehilite mb-2 p-2">',
+        '<code': '<code style="background-color: transparent"',
+    }
+    pattern = re.compile('|'.join(rep.keys()))
+    return pattern.sub(lambda m: rep[re.escape(m.group(0))], html)

+ 11 - 0
nicegui/elements/mermaid.js

@@ -0,0 +1,11 @@
+export default {
+  template: `<div></div>`,
+  mounted() {
+    this.update(this.$el.innerText);
+  },
+  methods: {
+    update(content) {
+      mermaid.render("mermaid" + this.$el.id, content, (svg) => (this.$el.innerHTML = svg));
+    },
+  },
+};

+ 20 - 0
nicegui/elements/mermaid.py

@@ -0,0 +1,20 @@
+from ..dependencies import register_component
+from .mixins.content_element import ContentElement
+
+register_component('mermaid', __file__, 'mermaid.js', ['lib/mermaid.min.js'])
+
+
+class Mermaid(ContentElement):
+
+    def __init__(self, content: str) -> None:
+        '''Mermaid Diagrams
+
+        Renders diagrams and charts written in the Markdown-inspired `Mermaid <https://mermaid.js.org/>`_ language.
+
+        :param content: the Mermaid content to be displayed
+        '''
+        super().__init__(tag='mermaid', content=content)
+
+    def on_content_change(self, content: str) -> None:
+        self._props['innerHTML'] = content
+        self.run_method('update', content)

+ 1 - 1
nicegui/run.py

@@ -47,7 +47,7 @@ def run(*,
     :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'.py'`)
     :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
     :param exclude: comma-separated string to exclude elements (with corresponding JavaScript libraries) to save bandwidth
-      (possible entries: audio, chart, colors, interactive_image, joystick, keyboard, log, scene, table, video)
+      (possible entries: audio, chart, colors, interactive_image, joystick, keyboard, log, mermaid, scene, table, video)
     :param tailwind: whether to use Tailwind (experimental, default: `True`)
     '''
     globals.ui_run_has_been_called = True

+ 1 - 0
nicegui/ui.py

@@ -30,6 +30,7 @@ from .elements.log import Log as log
 from .elements.markdown import Markdown as markdown
 from .elements.menu import Menu as menu
 from .elements.menu import MenuItem as menu_item
+from .elements.mermaid import Mermaid as mermaid
 from .elements.number import Number as number
 from .elements.progress import CircularProgress as circular_progress
 from .elements.progress import LinearProgress as linear_progress

+ 5 - 13
poetry.lock

@@ -437,7 +437,6 @@ files = [
     {file = "debugpy-1.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b5d1b13d7c7bf5d7cf700e33c0b8ddb7baf030fcf502f76fc061ddd9405d16c"},
     {file = "debugpy-1.6.6-cp38-cp38-win32.whl", hash = "sha256:70ab53918fd907a3ade01909b3ed783287ede362c80c75f41e79596d5ccacd32"},
     {file = "debugpy-1.6.6-cp38-cp38-win_amd64.whl", hash = "sha256:c05349890804d846eca32ce0623ab66c06f8800db881af7a876dc073ac1c2225"},
-    {file = "debugpy-1.6.6-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:11a0f3a106f69901e4a9a5683ce943a7a5605696024134b522aa1bfda25b5fec"},
     {file = "debugpy-1.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a771739902b1ae22a120dbbb6bd91b2cae6696c0e318b5007c5348519a4211c6"},
     {file = "debugpy-1.6.6-cp39-cp39-win32.whl", hash = "sha256:549ae0cb2d34fc09d1675f9b01942499751d174381b6082279cf19cdb3c47cbe"},
     {file = "debugpy-1.6.6-cp39-cp39-win_amd64.whl", hash = "sha256:de4a045fbf388e120bb6ec66501458d3134f4729faed26ff95de52a754abddb1"},
@@ -688,19 +687,19 @@ files = [
 
 [[package]]
 name = "isort"
-version = "5.11.4"
+version = "5.11.5"
 description = "A Python utility / library to sort Python imports."
 category = "dev"
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
-    {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
+    {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"},
+    {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"},
 ]
 
 [package.extras]
 colors = ["colorama (>=0.4.3,<0.5.0)"]
-pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
+pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
 plugins = ["setuptools"]
 requirements-deprecated-finder = ["pip-api", "pipreqs"]
 
@@ -1127,13 +1126,6 @@ files = [
     {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"},
     {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"},
     {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"},
-    {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"},
-    {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"},
-    {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"},
-    {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"},
-    {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"},
-    {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"},
-    {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"},
     {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"},
     {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"},
     {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
@@ -2060,4 +2052,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7"
-content-hash = "99911c0418f3637a5aa69bb4f1411cb4704e5d24583afa69269b311fca710a7d"
+content-hash = "d295b6331955c188539cab3df96edbc39a6c5329b5ca37d73733378473367126"

+ 1 - 1
pyproject.toml

@@ -11,7 +11,7 @@ keywords = ["gui", "ui", "web", "interface", "live"]
 [tool.poetry.dependencies]
 python = "^3.7"
 typing-extensions = ">=3.10.0"
-markdown2 = "^2.4.3"
+markdown2 = "^2.4.7"
 Pygments = "^2.9.0"
 docutils = "^0.17.1"
 uvicorn = {extras = ["standard"], version = "^0.20.0"}

+ 4 - 0
tests/test_element.py

@@ -34,6 +34,7 @@ def test_classes(screen: Screen):
 
 
 def test_style_parsing():
+    assert Element._parse_style(None) == {}
     assert Element._parse_style('color: red; background-color: green') == {'color': 'red', 'background-color': 'green'}
     assert Element._parse_style('width:12em;height:34.5em') == {'width': '12em', 'height': '34.5em'}
     assert Element._parse_style('transform: translate(120.0px, 50%)') == {'transform': 'translate(120.0px, 50%)'}
@@ -41,10 +42,13 @@ def test_style_parsing():
 
 
 def test_props_parsing():
+    assert Element._parse_props(None) == {}
     assert Element._parse_props('one two=1 three="abc def"') == {'one': True, 'two': '1', 'three': 'abc def'}
     assert Element._parse_props('loading percentage=12.5') == {'loading': True, 'percentage': '12.5'}
     assert Element._parse_props('size=50%') == {'size': '50%'}
     assert Element._parse_props('href=http://192.168.42.100/') == {'href': 'http://192.168.42.100/'}
+    assert Element._parse_props('hint="Your \\"given\\" name"') == {'hint': 'Your "given" name'}
+    assert Element._parse_props('input-style="{ color: #ff0000 }"') == {'input-style': '{ color: #ff0000 }'}
 
 
 def test_style(screen: Screen):

+ 45 - 0
tests/test_markdown.py

@@ -0,0 +1,45 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_markdown(screen: Screen):
+    m = ui.markdown('This is **markdown**')
+
+    screen.open('/')
+    element = screen.find('This is')
+    assert element.text == 'This is markdown'
+    assert element.get_attribute('innerHTML') == 'This is <strong>markdown</strong>'
+
+    m.set_content('New **content**')
+    element = screen.find('New')
+    assert element.text == 'New content'
+    assert element.get_attribute('innerHTML') == 'New <strong>content</strong>'
+
+
+def test_markdown_with_mermaid(screen: Screen):
+    m = ui.markdown('''
+Mermaid:
+
+```mermaid
+graph TD;
+    Node_A --> Node_B;
+```
+''', extras=['mermaid', 'fenced-code-blocks'])
+
+    screen.open('/')
+    screen.should_contain('Mermaid')
+    assert screen.find_by_tag('svg').get_attribute('id') == f'mermaid_{m.id}_0'
+    assert screen.find('Node_A').get_attribute('class') == 'nodeLabel'
+
+    m.set_content('''
+New:
+    
+```mermaid
+graph TD;
+    Node_C --> Node_D;
+```
+''')
+    screen.should_contain('New')
+    assert screen.find('Node_C').get_attribute('class') == 'nodeLabel'
+    screen.should_not_contain('Node_A')

+ 20 - 0
tests/test_mermaid.py

@@ -0,0 +1,20 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_mermaid(screen: Screen):
+    m = ui.mermaid('''
+graph TD;
+    Node_A --> Node_B;
+''')
+
+    screen.open('/')
+    assert screen.find('Node_A').get_attribute('class') == 'nodeLabel'
+
+    m.set_content('''
+graph TD;
+    Node_C --> Node_D;
+''')
+    assert screen.find('Node_C').get_attribute('class') == 'nodeLabel'
+    screen.should_not_contain('Node_A')

+ 8 - 0
website/reference.py

@@ -168,6 +168,14 @@ def create_full() -> None:
     def markdown_example():
         ui.markdown('''This is **Markdown**.''')
 
+    @example(ui.mermaid)
+    def mermaid_example():
+        ui.mermaid('''
+        graph LR;
+            A --> B;
+            A --> C;
+        ''')
+
     @example(ui.html)
     def html_example():
         ui.html('This is <strong>HTML</strong>.')

Some files were not shown because too many files changed in this diff