nativelayout 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ from .errors import NativeLayoutError, extract_line
2
+ from . import scopes
3
+ from .tree import * # noqa: F401,F403
4
+ from .differ import diff
5
+ from . import css
6
+ from .html_parser import (HtmlParser, HtmlElement, HtmlText, HtmlNode, HtmlCachedElement,
7
+ Chunk, ChunkCache, MarkupError)
8
+ from .layout_engine import LayoutEngine, LayoutResult, LayoutWarning
9
+ from .layout_host import (LayoutHost, LayoutSession, LayoutEvent, FilledMarkup,
10
+ PreparedRender, BACK_EVENT_KEY)
11
+ from .expander import Expander, ExpandedScreen, ExpandError
12
+ from .program import (NativeViewBase, Page, Screen, screen, state, view, go, go_root, go_back,
13
+ can_go_back, back, click, click_index, clicked, value, checked, set_value,
14
+ save, load, exists, delete)
@@ -0,0 +1,221 @@
1
+ """Parser of CSS declarations and values (port of Css.kt)."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import re
6
+ import threading
7
+ from dataclasses import dataclass
8
+ from typing import Callable
9
+
10
+ from .tree import AUTO, Dim, Length, Percent
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CssDeclaration:
15
+ name: str
16
+ value: str
17
+ line: int
18
+
19
+
20
+ def _round(x: float) -> int:
21
+ """Like Kotlin roundToInt: halves round up."""
22
+ return int(math.floor(x + 0.5))
23
+
24
+
25
+ def _to_float(s: str) -> float | None:
26
+ """Like toFloatOrNull: a finite decimal number, without '_'."""
27
+ if "_" in s or not s.strip() == s:
28
+ return None
29
+ try:
30
+ v = float(s)
31
+ except ValueError:
32
+ return None
33
+ return v if math.isfinite(v) else None
34
+
35
+
36
+ def _clamp(v, lo, hi):
37
+ return max(lo, min(hi, v))
38
+
39
+
40
+ def split_top_level(text: str, sep: str) -> list[str]:
41
+ parts: list[str] = []
42
+ cur: list[str] = []
43
+ depth = 0
44
+ quote: str | None = None
45
+ for c in text:
46
+ if quote is not None:
47
+ cur.append(c)
48
+ if c == quote:
49
+ quote = None
50
+ elif c == '"' or c == "'":
51
+ quote = c
52
+ cur.append(c)
53
+ elif c == "(":
54
+ depth += 1
55
+ cur.append(c)
56
+ elif c == ")":
57
+ if depth > 0:
58
+ depth -= 1
59
+ cur.append(c)
60
+ elif c == sep and depth == 0:
61
+ parts.append("".join(cur))
62
+ cur = []
63
+ else:
64
+ cur.append(c)
65
+ parts.append("".join(cur))
66
+ return parts
67
+
68
+
69
+ _IMPORTANT = re.compile(r"\s*!important\s*$", re.IGNORECASE)
70
+ _cache: dict[str, tuple[list[tuple[str, str]], list[str]]] = {}
71
+ _lock = threading.Lock()
72
+
73
+
74
+ def _parse(text: str):
75
+ items: list[tuple[str, str]] = []
76
+ warnings: list[str] = []
77
+ for part in split_top_level(text, ";"):
78
+ p = part.strip()
79
+ if not p:
80
+ continue
81
+ colon = p.find(":")
82
+ if colon <= 0:
83
+ warnings.append(f"CSS: declaration without ':' was ignored ('{p}')")
84
+ continue
85
+ name = p[:colon].strip().lower()
86
+ value = _IMPORTANT.sub("", p[colon + 1:].strip())
87
+ if not value:
88
+ warnings.append(f"CSS: property '{name}' has no value and was ignored")
89
+ continue
90
+ items.append((name, value))
91
+ return items, warnings
92
+
93
+
94
+ def declarations(text: str, line: int, warn: Callable[[str, int], None]) -> list[CssDeclaration]:
95
+ with _lock:
96
+ parsed = _cache.get(text)
97
+ if parsed is None:
98
+ parsed = _parse(text)
99
+ with _lock:
100
+ if len(_cache) > 512:
101
+ _cache.clear()
102
+ _cache[text] = parsed
103
+ items, warnings = parsed
104
+ for w in warnings:
105
+ warn(w, line)
106
+ return [CssDeclaration(n, v, line) for n, v in items]
107
+
108
+
109
+ _OPAQUE = 0xFF000000
110
+
111
+ _NAMES = {
112
+ "black": 0x000000, "white": 0xFFFFFF, "red": 0xFF0000, "green": 0x008000,
113
+ "blue": 0x0000FF, "yellow": 0xFFFF00, "orange": 0xFFA500, "purple": 0x800080,
114
+ "pink": 0xFFC0CB, "gray": 0x808080, "grey": 0x808080, "silver": 0xC0C0C0,
115
+ "maroon": 0x800000, "navy": 0x000080, "teal": 0x008080, "lime": 0x00FF00,
116
+ "aqua": 0x00FFFF, "cyan": 0x00FFFF, "magenta": 0xFF00FF, "fuchsia": 0xFF00FF,
117
+ "olive": 0x808000, "brown": 0xA52A2A, "gold": 0xFFD700, "indigo": 0x4B0082,
118
+ "violet": 0xEE82EE, "coral": 0xFF7F50, "crimson": 0xDC143C, "tomato": 0xFF6347,
119
+ "salmon": 0xFA8072, "khaki": 0xF0E68C, "beige": 0xF5F5DC, "ivory": 0xFFFFF0,
120
+ "lavender": 0xE6E6FA, "turquoise": 0x40E0D0, "darkgray": 0xA9A9A9,
121
+ "darkgrey": 0xA9A9A9, "lightgray": 0xD3D3D3, "lightgrey": 0xD3D3D3,
122
+ "darkblue": 0x00008B, "darkgreen": 0x006400, "darkred": 0x8B0000,
123
+ "lightblue": 0xADD8E6, "lightgreen": 0x90EE90, "whitesmoke": 0xF5F5F5,
124
+ }
125
+
126
+ _REGEX_RGB = re.compile(r"^rgba?\(([^)]*)\)$")
127
+ _REGEX_LENGTH = re.compile(r"^(-?\d*\.?\d+)(px|rem)$", re.ASCII)
128
+ _HEXCHARS = set("0123456789abcdef")
129
+
130
+
131
+ def _pack(r: int, g: int, b: int, a: int) -> int:
132
+ return (a << 24) | (r << 16) | (g << 8) | b
133
+
134
+
135
+ def color(text: str) -> int | None:
136
+ t = text.strip().lower()
137
+ if not t:
138
+ return None
139
+ if t == "transparent":
140
+ return 0
141
+ if t in _NAMES:
142
+ return _NAMES[t] | _OPAQUE
143
+ if t.startswith("#"):
144
+ h = t[1:]
145
+ if not h or any(ch not in _HEXCHARS for ch in h):
146
+ return None
147
+ dbl = lambda p: int(h[p], 16) * 17
148
+ pair = lambda p: int(h[p:p + 2], 16)
149
+ n = len(h)
150
+ if n == 3:
151
+ return _pack(dbl(0), dbl(1), dbl(2), 255)
152
+ if n == 4:
153
+ return _pack(dbl(0), dbl(1), dbl(2), dbl(3))
154
+ if n == 6:
155
+ return _pack(pair(0), pair(2), pair(4), 255)
156
+ if n == 8:
157
+ return _pack(pair(0), pair(2), pair(4), pair(6))
158
+ return None
159
+
160
+ m = _REGEX_RGB.match(t)
161
+ if m is None:
162
+ return None
163
+ parts = [p.strip() for p in re.split(r"[, /]", m.group(1)) if p.strip()]
164
+ if not 3 <= len(parts) <= 4:
165
+ return None
166
+
167
+ def channel(s: str) -> int | None:
168
+ if s.endswith("%"):
169
+ v = _to_float(s[:-1])
170
+ return None if v is None else _round(v * 2.55)
171
+ v = _to_float(s)
172
+ return None if v is None else _round(v)
173
+
174
+ r, g, b = channel(parts[0]), channel(parts[1]), channel(parts[2])
175
+ if r is None or g is None or b is None:
176
+ return None
177
+ a = 255
178
+ if len(parts) == 4:
179
+ s = parts[3]
180
+ v = _to_float(s[:-1]) if s.endswith("%") else _to_float(s)
181
+ if v is None:
182
+ return None
183
+ if s.endswith("%"):
184
+ v = v / 100.0
185
+ a = _round(_clamp(v, 0.0, 1.0) * 255.0)
186
+ return _pack(_clamp(r, 0, 255), _clamp(g, 0, 255), _clamp(b, 0, 255), a)
187
+
188
+
189
+ def length(text: str) -> Length | None:
190
+ t = text.strip().lower()
191
+ if t == "0":
192
+ return Length.ZERO
193
+ m = _REGEX_LENGTH.match(t)
194
+ if m is None:
195
+ return None
196
+ n = _to_float(m.group(1))
197
+ if n is None:
198
+ return None
199
+ return Length.rem(n) if m.group(2) == "rem" else Length.px(n)
200
+
201
+
202
+ def fraction(text: str) -> float | None:
203
+ """A number from 0 to 1, or a percentage (opacity)."""
204
+ t = text.strip().lower()
205
+ if t.endswith("%"):
206
+ v = _to_float(t[:-1])
207
+ v = None if v is None else v / 100.0
208
+ else:
209
+ v = _to_float(t)
210
+ return None if v is None else _clamp(v, 0.0, 1.0)
211
+
212
+
213
+ def size(text: str):
214
+ t = text.strip().lower()
215
+ if t == "auto":
216
+ return AUTO
217
+ if t.endswith("%"):
218
+ v = _to_float(t[:-1])
219
+ return None if v is None else Percent(v)
220
+ ln = length(t)
221
+ return None if ln is None else Dim(ln)
@@ -0,0 +1,140 @@
1
+ """Diff between two native trees (port of LayoutDiffer.kt)."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ from .tree import (Native, NativeBox, NativeButton, NativeCheckbox, NativeField,
7
+ NativeImage, NativeItem, NativeStyle, NativeText, Position, Span,
8
+ native_children, same_as)
9
+
10
+
11
+ class Change:
12
+ pass
13
+
14
+
15
+ @dataclass
16
+ class SetRoot(Change):
17
+ node: Native
18
+
19
+
20
+ @dataclass
21
+ class Create(Change):
22
+ parent_key: str
23
+ position: int
24
+ node: Native
25
+
26
+
27
+ @dataclass
28
+ class Remove(Change):
29
+ key: str
30
+
31
+
32
+ @dataclass
33
+ class Replace(Change):
34
+ key: str
35
+ node: Native
36
+
37
+
38
+ @dataclass
39
+ class UpdateText(Change):
40
+ key: str
41
+ spans: list[Span]
42
+
43
+
44
+ @dataclass
45
+ class UpdateStyle(Change):
46
+ key: str
47
+ style: NativeStyle
48
+
49
+
50
+ @dataclass
51
+ class UpdateAttributes(Change):
52
+ key: str
53
+ node: Native
54
+
55
+
56
+ @dataclass
57
+ class SetItems(Change):
58
+ key: str
59
+ items: list[Native]
60
+
61
+
62
+ def diff(previous: Native | None, nxt: Native) -> list[Change]:
63
+ if previous is None or previous.key != nxt.key or type(previous) is not type(nxt):
64
+ return [SetRoot(nxt)]
65
+ removed: list[Change] = []
66
+ rest: list[Change] = []
67
+ _compare(previous, nxt, removed, rest)
68
+ return removed + rest
69
+
70
+
71
+ def _compare(a: Native, n: Native, removed: list, rest: list) -> None:
72
+ if isinstance(a, NativeBox) and isinstance(n, NativeBox):
73
+ if (a.virtual != n.virtual or a.overlay != n.overlay
74
+ or a.header != n.header or a.footer != n.footer
75
+ or a.has_absolute != n.has_absolute):
76
+ rest.append(Replace(n.key, n))
77
+ return
78
+ if n.virtual:
79
+ if a.style != n.style:
80
+ rest.append(UpdateStyle(n.key, n.style))
81
+ if _attributes_differ(a, n):
82
+ rest.append(UpdateAttributes(n.key, n))
83
+ if not _same_children(a.children, n.children):
84
+ rest.append(SetItems(n.key, n.children))
85
+ return
86
+ if a.style != n.style:
87
+ rest.append(UpdateStyle(n.key, n.style))
88
+ if _attributes_differ(a, n):
89
+ rest.append(UpdateAttributes(n.key, n))
90
+ if isinstance(a, NativeText) and isinstance(n, NativeText) and a.spans != n.spans:
91
+ rest.append(UpdateText(n.key, n.spans))
92
+ ca, cn = native_children(a), native_children(n)
93
+ if not ca and not cn:
94
+ return
95
+ _compare_children(n, ca, cn, removed, rest)
96
+
97
+
98
+ def _same_children(a: list, b: list) -> bool:
99
+ return len(a) == len(b) and all(same_as(x, y) for x, y in zip(a, b))
100
+
101
+
102
+ def _attributes_differ(a: Native, n: Native) -> bool:
103
+ if a.classes != n.classes:
104
+ return True
105
+ if isinstance(a, NativeButton) and isinstance(n, NativeButton):
106
+ return a.label != n.label or a.disabled != n.disabled
107
+ if isinstance(a, NativeField) and isinstance(n, NativeField):
108
+ return (a.type != n.type or a.hint != n.hint or a.disabled != n.disabled
109
+ or a.read_only != n.read_only or a.rows != n.rows)
110
+ if isinstance(a, NativeCheckbox) and isinstance(n, NativeCheckbox):
111
+ return a.disabled != n.disabled
112
+ if isinstance(a, NativeImage) and isinstance(n, NativeImage):
113
+ return a.source != n.source or a.description != n.description
114
+ if isinstance(a, NativeItem) and isinstance(n, NativeItem):
115
+ return a.marker != n.marker
116
+ return False
117
+
118
+
119
+ def _compare_children(parent: Native, prev: list, nxt: list, removed: list, rest: list) -> None:
120
+ new_keys = {c.key for c in nxt}
121
+ old_by_key = {c.key: c for c in prev}
122
+ kept_old = [c.key for c in prev if c.key in new_keys]
123
+ kept_new = [c.key for c in nxt if c.key in old_by_key]
124
+ if kept_old != kept_new:
125
+ rest.append(Replace(parent.key, parent))
126
+ return
127
+ for a in prev:
128
+ if a.key not in new_keys:
129
+ removed.append(Remove(a.key))
130
+ for i, n in enumerate(nxt):
131
+ a = old_by_key.get(n.key)
132
+ if a is None:
133
+ # an absolute child lives apart from the flow: its position counts only the absolute ones
134
+ absolute = n.style.position == Position.ABSOLUTE
135
+ before = sum(1 for c in nxt[:i] if (c.style.position == Position.ABSOLUTE) == absolute)
136
+ rest.append(Create(parent.key, before, n))
137
+ elif type(a) is not type(n):
138
+ rest.append(Replace(n.key, n))
139
+ else:
140
+ _compare(a, n, removed, rest)
@@ -0,0 +1,25 @@
1
+ """Errors of the core (port of NativeLayoutError.kt)."""
2
+ from __future__ import annotations
3
+
4
+
5
+ class NativeLayoutError(Exception):
6
+ def __init__(self, message: str, line: int, context: str,
7
+ word: str | None = None, suggestion: str | None = None):
8
+ super().__init__(message)
9
+ self.message = message
10
+ self.line = line
11
+ self.context = context
12
+ self.word = word
13
+ self.suggestion = suggestion
14
+
15
+ def format(self) -> str:
16
+ out = f"Error: line {self.line}\n{self.context}\n\n{self.message}"
17
+ if self.suggestion is not None:
18
+ out += f"\n\nSuggestion: {self.suggestion}"
19
+ return out
20
+
21
+
22
+ def extract_line(source: str, line_number: int) -> str:
23
+ lines = source.split("\n")
24
+ i = line_number - 1
25
+ return lines[i].strip() if 0 <= i < len(lines) else ""