Skip to content

Commit

Permalink
Merge branch 'update_queue' of github.com:zauberzeug/nicegui into upd…
Browse files Browse the repository at this point in the history
…ate_queue
  • Loading branch information
falkoschindler committed Feb 7, 2023
2 parents f284f0a + 790591f commit 216d315
Show file tree
Hide file tree
Showing 14 changed files with 1,461 additions and 46 deletions.
32 changes: 19 additions & 13 deletions nicegui/element.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,6 +14,8 @@
if TYPE_CHECKING:
from .client import Client

PROPS_PATTERN = re.compile(r'([\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')


class Element(ABC, Visibility):

Expand Down Expand Up @@ -94,7 +97,13 @@ def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, re

@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.
Expand All @@ -115,12 +124,14 @@ def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, repl

@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.
Expand Down Expand Up @@ -201,8 +212,3 @@ def delete(self) -> None:
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()
1,284 changes: 1,284 additions & 0 deletions nicegui/elements/lib/mermaid.min.js

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions nicegui/elements/markdown.js
Original file line number Diff line number Diff line change
@@ -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;
}
39 changes: 21 additions & 18 deletions nicegui/elements/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,10 @@

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):
Expand All @@ -36,16 +22,33 @@ def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-block
: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 changes: 11 additions & 0 deletions nicegui/elements/mermaid.js
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions nicegui/elements/mermaid.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion nicegui/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions nicegui/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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
Expand Down
18 changes: 5 additions & 13 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
4 changes: 4 additions & 0 deletions tests/test_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,21 @@ def assert_classes(classes: str) -> None:


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%)'}
assert Element._parse_style('box-shadow: 0 0 0.5em #1976d2') == {'box-shadow': '0 0 0.5em #1976d2'}


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):
Expand Down
45 changes: 45 additions & 0 deletions tests/test_markdown.py
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions tests/test_mermaid.py
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions website/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ def upload_example():
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>.')
Expand Down

0 comments on commit 216d315

Please sign in to comment.