Repository: zauberzeug/nicegui
Pull request #1800 (2023-10-13 23:32:09)

element props can be enclosed in double quotes
We had a short discussion on discord the other day about props with quotes
https://discord.com/channels/1089836369431498784/1161093934953537646/1161172250859405322

I added this functionality and added tests.


Patch:
diff --git a/nicegui/element.py b/nicegui/element.py
index c8c62065..666049c5 100644
--- a/nicegui/element.py
+++ b/nicegui/element.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import ast
 import inspect
 import re
 from copy import copy, deepcopy
@@ -8,8 +9,6 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional,
 
 from typing_extensions import Self
 
-from nicegui import json
-
 from . import events, globals, outbox, storage  # pylint: disable=redefined-builtin
 from .dependencies import Component, Library, register_library, register_vue_component
 from .elements.mixins.visibility import Visibility
@@ -20,7 +19,31 @@ from .tailwind import Tailwind
 if TYPE_CHECKING:
     from .client import Client
 
-PROPS_PATTERN = re.compile(r'([:\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
+PROPS_PATTERN = re.compile(r'''
+# Match a key-value pair optionally followed by whitespace or end of string
+([:\w\-]+)          # Capture group 1: Key
+(?:                 # Optional non-capturing group for value
+    =               # Match the equal sign
+    (?:             # Non-capturing group for value options
+        (           # Capture group 2: Value enclosed in double quotes
+            "       # Match  double quote
+            [^"\\]* # Match any character except quotes or backslashes zero or more times
+            (?:\\.[^"\\]*)*  # Match any escaped character followed by any character except quotes or backslashes zero or more times
+            "       # Match the closing quote
+        )
+        |
+        (           # Capture group 3: Value enclosed in single quotes
+            '       # Match a single quote
+            [^'\\]* # Match any character except quotes or backslashes zero or more times
+            (?:\\.[^'\\]*)*  # Match any escaped character followed by any character except quotes or backslashes zero or more times
+            '       # Match the closing quote
+        )
+        |           # Or
+        ([\w\-.%:\/]+)  # Capture group 4: Value without quotes
+    )
+)?                  # End of optional non-capturing group for value
+(?:$|\s)            # Match end of string or whitespace
+''', re.VERBOSE)
 
 
 class Element(Visibility):
@@ -258,10 +281,13 @@ class Element(Visibility):
         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
+            value = match.group(2) or match.group(3) or match.group(4)
+            if value is None:
+                dictionary[key] = True
+            else:
+                if (value.startswith("'") and value.endswith("'")) or (value.startswith('"') and value.endswith('"')):
+                    value = ast.literal_eval(value)
+                dictionary[key] = value
         return dictionary
 
     def props(self, add: Optional[str] = None, *, remove: Optional[str] = None) -> Self:


Test patch:
diff --git a/tests/test_element.py b/tests/test_element.py
index 5e4c0fdb..772f1511 100644
--- a/tests/test_element.py
+++ b/tests/test_element.py
@@ -51,6 +51,19 @@ def test_props_parsing():
     assert ui.element._parse_props('hint="Your \\"given\\" name"') == {'hint': 'Your "given" name'}
     assert ui.element._parse_props('input-style="{ color: #ff0000 }"') == {'input-style': '{ color: #ff0000 }'}
 
+    assert ui.element._parse_props('empty=""') == {'empty': ''}
+    assert ui.element._parse_props("empty=''") == {'empty': ''}
+
+    assert ui.element._parse_props("""hint='Your \\"given\\" name'""") == {'hint': 'Your "given" name'}
+    assert ui.element._parse_props("one two=1 three='abc def'") == {'one': True, 'two': '1', 'three': 'abc def'}
+    assert ui.element._parse_props('''three='abc def' four="hhh jjj"''') == {'three': 'abc def', 'four': 'hhh jjj', }
+    assert ui.element._parse_props('''foo="quote'quote"''') == {'foo': "quote'quote"}
+    assert ui.element._parse_props("""foo='quote"quote'""") == {'foo': 'quote"quote'}
+    assert ui.element._parse_props("""foo="single '" bar='double "'""") == {'foo': "single '", 'bar': 'double "'}
+    assert ui.element._parse_props("""foo="single '" bar='double \\"'""") == {'foo': "single '", 'bar': 'double "'}
+    assert ui.element._parse_props("input-style='{ color: #ff0000 }'") == {'input-style': '{ color: #ff0000 }'}
+    assert ui.element._parse_props("""input-style='{ myquote: "quote" }'""") == {'input-style': '{ myquote: "quote" }'}
+
 
 def test_style(screen: Screen):
     label = ui.label('Some label')