tina4ruby 3.13.51 → 3.13.52
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.
- checksums.yaml +4 -4
- data/lib/tina4/cli.rb +4 -0
- data/lib/tina4/database.rb +1 -0
- data/lib/tina4/frond.rb +210 -1
- data/lib/tina4/public/js/frond.js +138 -1
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/rack_app.rb +4 -1
- data/lib/tina4/scss_compiler.rb +107 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +10 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6a859f05cd2ac97b5e38ab9ee678cd8419f91be1aca0dedaed1e1d63b268968
|
|
4
|
+
data.tar.gz: 4454362a269ef77b5992a0ec4d2e30e6dd64134ed64265d1e8fd6f9432fcbc0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 595d84b72e3b75a263f54ff93859a9f92f52fc76434b7633d149d11a2fa8a172a72478a13b9aa649ea0189a1f975d0a98daf7afa7cbceeb5bec9636433c95dad
|
|
7
|
+
data.tar.gz: 66634157b45125bb796277f54a9b93d12ab721c237b0dec39450d78d69ba4f3162972a178ef742607d45e4cc33d572c3460d7d336e0133e7c29ea902e21b68ae
|
data/lib/tina4/cli.rb
CHANGED
|
@@ -200,6 +200,10 @@ module Tina4
|
|
|
200
200
|
# Register health check endpoint
|
|
201
201
|
Tina4::Health.register!
|
|
202
202
|
|
|
203
|
+
# Register the always-on Frond {% live %} refresh endpoint
|
|
204
|
+
# (GET /__frond/live/{name}) so server-rendered live blocks can poll/SSE.
|
|
205
|
+
Tina4::Frond.register_live_endpoint!
|
|
206
|
+
|
|
203
207
|
# Load route files
|
|
204
208
|
load_routes(root_dir)
|
|
205
209
|
|
data/lib/tina4/database.rb
CHANGED
|
@@ -111,6 +111,7 @@ module Tina4
|
|
|
111
111
|
"sqlite3" => "Tina4::Drivers::SqliteDriver",
|
|
112
112
|
"postgres" => "Tina4::Drivers::PostgresDriver",
|
|
113
113
|
"postgresql" => "Tina4::Drivers::PostgresDriver",
|
|
114
|
+
"pgsql" => "Tina4::Drivers::PostgresDriver",
|
|
114
115
|
"mysql" => "Tina4::Drivers::MysqlDriver",
|
|
115
116
|
"mssql" => "Tina4::Drivers::MssqlDriver",
|
|
116
117
|
"sqlserver" => "Tina4::Drivers::MssqlDriver",
|
data/lib/tina4/frond.rb
CHANGED
|
@@ -39,7 +39,23 @@ module Tina4
|
|
|
39
39
|
@@class_globals = {}
|
|
40
40
|
@@class_tests = {}
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# -- Live-block registries (server-rendered {% live %} regions) -----------
|
|
43
|
+
# A {% live %} block registers three things when its page first renders:
|
|
44
|
+
# * class_live_fragments[name] -> the raw body source, re-rendered on
|
|
45
|
+
# every refresh by the /__frond/live/<name>
|
|
46
|
+
# endpoint or push_live
|
|
47
|
+
# * class_live_sources[name] -> an optional data provider (live_source)
|
|
48
|
+
# that re-runs with the LIVE request each
|
|
49
|
+
# refresh, so auth re-applies (IDOR guard)
|
|
50
|
+
# * class_live_ws_paths[name] -> the ws path a `ws "path"` block declared,
|
|
51
|
+
# used as the push_live broadcast target
|
|
52
|
+
# These persist across requests in the long-lived server (parity with the
|
|
53
|
+
# Python master's class-level dicts and PHP's static registries).
|
|
54
|
+
@@class_live_fragments = {}
|
|
55
|
+
@@class_live_sources = {}
|
|
56
|
+
@@class_live_ws_paths = {}
|
|
57
|
+
|
|
58
|
+
# Clear the class-level globals/filters/tests/live registries.
|
|
43
59
|
#
|
|
44
60
|
# Useful in test fixtures to prevent leaking state between tests. Does
|
|
45
61
|
# NOT affect built-in filters or globals — only user-registered ones.
|
|
@@ -47,6 +63,9 @@ module Tina4
|
|
|
47
63
|
@@class_filters = {}
|
|
48
64
|
@@class_globals = {}
|
|
49
65
|
@@class_tests = {}
|
|
66
|
+
@@class_live_fragments = {}
|
|
67
|
+
@@class_live_sources = {}
|
|
68
|
+
@@class_live_ws_paths = {}
|
|
50
69
|
end
|
|
51
70
|
|
|
52
71
|
# -- Token types ----------------------------------------------------------
|
|
@@ -91,6 +110,18 @@ module Tina4
|
|
|
91
110
|
SET_RE = /\Aset\s+(\w+)\s*=\s*(.+)\z/m
|
|
92
111
|
INCLUDE_RE = /\Ainclude\s+["'](.+?)["'](?:\s+with\s+(.+))?\z/
|
|
93
112
|
MACRO_RE = /\Amacro\s+(\w+)\s*\(([^)]*)\)/
|
|
113
|
+
# {% live "name" poll N | sse | ws "path" [src "url"] %}
|
|
114
|
+
LIVE_RE = /\Alive\s+["']([^"']+)["'](.*)\z/m
|
|
115
|
+
LIVE_WS_RE = /ws\s+["']([^"']+)["']/
|
|
116
|
+
LIVE_SRC_RE = /src\s+["']([^"']+)["']/
|
|
117
|
+
|
|
118
|
+
# Escape a value for use inside an HTML attribute on a live marker.
|
|
119
|
+
# Byte-identical order to the Python master / PHP liveAttr so the emitted
|
|
120
|
+
# marker element matches across all four frameworks.
|
|
121
|
+
def self.live_attr(value)
|
|
122
|
+
value.to_s.gsub("&", "&").gsub('"', """)
|
|
123
|
+
.gsub("<", "<").gsub(">", ">")
|
|
124
|
+
end
|
|
94
125
|
FROM_IMPORT_RE = /\Afrom\s+["'](.+?)["']\s+import\s+(.+)/
|
|
95
126
|
CACHE_RE = /\Acache\s+["'](.+?)["']\s*(\d+)?/
|
|
96
127
|
SPACELESS_RE = />\s+</
|
|
@@ -587,6 +618,9 @@ module Tina4
|
|
|
587
618
|
when "cache"
|
|
588
619
|
result, i = handle_cache(tokens, i, context)
|
|
589
620
|
output << result
|
|
621
|
+
when "live"
|
|
622
|
+
result, i = handle_live(tokens, i, context)
|
|
623
|
+
output << result
|
|
590
624
|
when "spaceless"
|
|
591
625
|
result, i = handle_spaceless(tokens, i, context)
|
|
592
626
|
output << result
|
|
@@ -1794,6 +1828,181 @@ module Tina4
|
|
|
1794
1828
|
[rendered, i]
|
|
1795
1829
|
end
|
|
1796
1830
|
|
|
1831
|
+
# Handle {% live "name" poll N | sse | ws "path" [src "url"] %}...{% endlive %}.
|
|
1832
|
+
#
|
|
1833
|
+
# Server-rendered live region. The body renders once for first paint, is
|
|
1834
|
+
# registered under <name> so the /__frond/live/<name> endpoint (or a
|
|
1835
|
+
# live_source provider) can re-render it, and is wrapped in a marker element
|
|
1836
|
+
# that frond.js wires to the chosen transport (poll / sse / ws). Mirrors the
|
|
1837
|
+
# Python master's _handle_live and PHP's handleLive.
|
|
1838
|
+
def handle_live(tokens, start, context)
|
|
1839
|
+
content, _, _ = strip_tag(tokens[start][1])
|
|
1840
|
+
m = LIVE_RE.match(content)
|
|
1841
|
+
raise 'live: expected {% live "name" poll N | sse | ws "path" %}' unless m
|
|
1842
|
+
|
|
1843
|
+
name = m[1]
|
|
1844
|
+
rest = (m[2] || "").strip
|
|
1845
|
+
parts = rest.split
|
|
1846
|
+
mode = parts[0] || ""
|
|
1847
|
+
|
|
1848
|
+
src = nil
|
|
1849
|
+
if (sm = LIVE_SRC_RE.match(rest))
|
|
1850
|
+
src = sm[1]
|
|
1851
|
+
end
|
|
1852
|
+
if src && (src.start_with?("http://") || src.start_with?("https://") || src.start_with?("//"))
|
|
1853
|
+
raise "live: src must be a same-origin path, not an absolute URL"
|
|
1854
|
+
end
|
|
1855
|
+
|
|
1856
|
+
interval = nil
|
|
1857
|
+
ws_path = nil
|
|
1858
|
+
case mode
|
|
1859
|
+
when "poll"
|
|
1860
|
+
raise 'live: poll requires seconds, e.g. {% live "x" poll 5 %}' unless parts[1]&.match?(/\A\d+\z/)
|
|
1861
|
+
interval = parts[1].to_i
|
|
1862
|
+
when "sse"
|
|
1863
|
+
# no extra config
|
|
1864
|
+
when "ws"
|
|
1865
|
+
wm = LIVE_WS_RE.match(rest)
|
|
1866
|
+
raise 'live: ws requires a path, e.g. {% live "x" ws "/ws/x" %}' unless wm
|
|
1867
|
+
ws_path = wm[1]
|
|
1868
|
+
else
|
|
1869
|
+
raise %(live: unknown transport "#{mode}" (use poll N, sse, or ws "path"))
|
|
1870
|
+
end
|
|
1871
|
+
|
|
1872
|
+
# Collect body tokens up to {% endlive %}. Nested live is unsupported.
|
|
1873
|
+
body_tokens = []
|
|
1874
|
+
i = start + 1
|
|
1875
|
+
while i < tokens.length
|
|
1876
|
+
if tokens[i][0] == BLOCK
|
|
1877
|
+
tc, _, _ = strip_tag(tokens[i][1])
|
|
1878
|
+
tag = tc.split[0] || ""
|
|
1879
|
+
raise "live: nested live blocks are not supported" if tag == "live"
|
|
1880
|
+
if tag == "endlive"
|
|
1881
|
+
i += 1
|
|
1882
|
+
break
|
|
1883
|
+
end
|
|
1884
|
+
body_tokens << tokens[i]
|
|
1885
|
+
else
|
|
1886
|
+
body_tokens << tokens[i]
|
|
1887
|
+
end
|
|
1888
|
+
i += 1
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
# Register the raw body source so the auto endpoint can re-render it.
|
|
1892
|
+
@@class_live_fragments[name] = body_tokens.map { |t| t[1] }.join
|
|
1893
|
+
|
|
1894
|
+
endpoint = src || ("/__frond/live/" + name)
|
|
1895
|
+
attrs = [%(data-frond-live="#{Frond.live_attr(name)}"), %(id="live-#{Frond.live_attr(name)}")]
|
|
1896
|
+
case mode
|
|
1897
|
+
when "poll"
|
|
1898
|
+
attrs << 'data-mode="poll"' << %(data-interval="#{interval}") << %(data-src="#{Frond.live_attr(endpoint)}")
|
|
1899
|
+
when "sse"
|
|
1900
|
+
attrs << 'data-mode="sse"' << %(data-src="#{Frond.live_attr(endpoint)}")
|
|
1901
|
+
when "ws"
|
|
1902
|
+
@@class_live_ws_paths[name] = ws_path
|
|
1903
|
+
attrs << 'data-mode="ws"' << %(data-ws="#{Frond.live_attr(ws_path)}")
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
first_paint = render_tokens(body_tokens.dup, context)
|
|
1907
|
+
[%(<div #{attrs.join(' ')}>#{first_paint}</div>), i]
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
# -- Live-block class API (mirrors Python master + PHP static facade) -----
|
|
1911
|
+
|
|
1912
|
+
# Re-render a registered {% live %} fragment by name with fresh data.
|
|
1913
|
+
# Returns the rendered HTML, or nil if no fragment is registered under that
|
|
1914
|
+
# name yet (its page has not rendered). The /__frond/live/<name> endpoint
|
|
1915
|
+
# calls this after resolving the provider data.
|
|
1916
|
+
def self.render_live(name, data = {})
|
|
1917
|
+
source = @@class_live_fragments[name]
|
|
1918
|
+
return nil if source.nil?
|
|
1919
|
+
|
|
1920
|
+
new.render_string(source, data || {})
|
|
1921
|
+
end
|
|
1922
|
+
|
|
1923
|
+
# Register a data provider for a {% live %} block. Accepts a block OR a
|
|
1924
|
+
# callable (proc/lambda); it is invoked with the live request on every
|
|
1925
|
+
# refresh so auth re-applies (IDOR guard). Mirrors Python's @live_source.
|
|
1926
|
+
def self.live_source(name, callable = nil, &blk)
|
|
1927
|
+
@@class_live_sources[name] = callable || blk
|
|
1928
|
+
end
|
|
1929
|
+
|
|
1930
|
+
# The provider registered for a live block, or nil.
|
|
1931
|
+
def self.get_live_source(name)
|
|
1932
|
+
@@class_live_sources[name]
|
|
1933
|
+
end
|
|
1934
|
+
|
|
1935
|
+
# Whether a live fragment has been registered (its page rendered).
|
|
1936
|
+
def self.has_live_fragment?(name)
|
|
1937
|
+
@@class_live_fragments.key?(name)
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
# The ws path a live block declared (data-ws), or nil.
|
|
1941
|
+
def self.get_live_ws_path(name)
|
|
1942
|
+
@@class_live_ws_paths[name]
|
|
1943
|
+
end
|
|
1944
|
+
|
|
1945
|
+
# Handle GET /__frond/live/{name}: resolve the provider, run it with the
|
|
1946
|
+
# live request (auth re-applies), re-render the fragment, return via the
|
|
1947
|
+
# response callable. 404 for unknown name / unrendered fragment. Mirrors
|
|
1948
|
+
# Python's live_endpoint and PHP's respondLive.
|
|
1949
|
+
def self.respond_live(request, response, name)
|
|
1950
|
+
provider = @@class_live_sources[name]
|
|
1951
|
+
if !@@class_live_fragments.key?(name) && provider.nil?
|
|
1952
|
+
return response.call("live block not found: #{name}", 404)
|
|
1953
|
+
end
|
|
1954
|
+
|
|
1955
|
+
context = {}
|
|
1956
|
+
unless provider.nil?
|
|
1957
|
+
result = provider.call(request)
|
|
1958
|
+
context = result.is_a?(Hash) ? result : {}
|
|
1959
|
+
end
|
|
1960
|
+
|
|
1961
|
+
html = render_live(name, context)
|
|
1962
|
+
return response.call("live fragment not registered yet: #{name}", 404) if html.nil?
|
|
1963
|
+
|
|
1964
|
+
response.call(html)
|
|
1965
|
+
end
|
|
1966
|
+
|
|
1967
|
+
# Re-render the '<name>' live fragment and push it to connected clients.
|
|
1968
|
+
# Broadcasts a {type,name,html} envelope over WebSocket to the block's
|
|
1969
|
+
# declared data-ws path (else a room named <name>). Returns the rendered
|
|
1970
|
+
# HTML, or nil if the fragment is not registered. Mirrors Python push_live
|
|
1971
|
+
# / PHP pushLive. The broadcast is best-effort — a missing/failed WS engine
|
|
1972
|
+
# never raises into the caller.
|
|
1973
|
+
def self.push_live(name, data = {})
|
|
1974
|
+
html = render_live(name, data)
|
|
1975
|
+
return nil if html.nil?
|
|
1976
|
+
|
|
1977
|
+
envelope = { "type" => "live", "name" => name, "html" => html }.to_json
|
|
1978
|
+
engine = (Tina4::WebSocket.current if defined?(Tina4::WebSocket))
|
|
1979
|
+
if engine
|
|
1980
|
+
begin
|
|
1981
|
+
ws_path = get_live_ws_path(name)
|
|
1982
|
+
if ws_path
|
|
1983
|
+
engine.broadcast(envelope, path: ws_path)
|
|
1984
|
+
else
|
|
1985
|
+
engine.broadcast_to_room(name, envelope)
|
|
1986
|
+
end
|
|
1987
|
+
rescue StandardError => e
|
|
1988
|
+
Tina4::Log.error("push_live(#{name}) broadcast failed: #{e.message}") if defined?(Tina4::Log)
|
|
1989
|
+
end
|
|
1990
|
+
end
|
|
1991
|
+
html
|
|
1992
|
+
end
|
|
1993
|
+
|
|
1994
|
+
# Register the always-on GET /__frond/live/{name} endpoint that re-renders
|
|
1995
|
+
# a live block on demand. Idempotent — guarded against a re-register after a
|
|
1996
|
+
# Router.clear! (specs / hot-reload rescans). Mirrors PHP App::registerLiveEndpoint.
|
|
1997
|
+
def self.register_live_endpoint!
|
|
1998
|
+
return if Tina4::Router.find_route("GET", "/__frond/live/live-probe")
|
|
1999
|
+
|
|
2000
|
+
Tina4::Router.add(
|
|
2001
|
+
"GET", "/__frond/live/{name}",
|
|
2002
|
+
lambda { |request, response, name| Tina4::Frond.respond_live(request, response, name) }
|
|
2003
|
+
)
|
|
2004
|
+
end
|
|
2005
|
+
|
|
1797
2006
|
def handle_spaceless(tokens, start, context)
|
|
1798
2007
|
body_tokens = []
|
|
1799
2008
|
i = start + 1
|
|
@@ -558,6 +558,132 @@ var _frondModule = (() => {
|
|
|
558
558
|
}
|
|
559
559
|
});
|
|
560
560
|
}
|
|
561
|
+
function _liveKey(el) {
|
|
562
|
+
const d = el.dataset;
|
|
563
|
+
return d && d.key ? d.key : null;
|
|
564
|
+
}
|
|
565
|
+
function _liveSyncAttrs(oldNode, newNode) {
|
|
566
|
+
const na = newNode.attributes;
|
|
567
|
+
for (let i = 0; i < na.length; i++) {
|
|
568
|
+
const a = na[i];
|
|
569
|
+
if (oldNode.getAttribute(a.name) !== a.value) oldNode.setAttribute(a.name, a.value);
|
|
570
|
+
}
|
|
571
|
+
const oa = Array.prototype.slice.call(oldNode.attributes);
|
|
572
|
+
oa.forEach(function(a) {
|
|
573
|
+
if (!newNode.hasAttribute(a.name)) oldNode.removeAttribute(a.name);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function _liveMorphNode(oldNode, newNode) {
|
|
577
|
+
const tag = newNode.tagName;
|
|
578
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
|
|
579
|
+
_liveSyncAttrs(oldNode, newNode);
|
|
580
|
+
if (oldNode.children.length || newNode.children.length) {
|
|
581
|
+
_liveReconcile(oldNode, newNode);
|
|
582
|
+
} else if (oldNode.innerHTML !== newNode.innerHTML) {
|
|
583
|
+
oldNode.innerHTML = newNode.innerHTML;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function _liveReconcile(parent, next) {
|
|
587
|
+
const oldKids = Array.prototype.slice.call(parent.children);
|
|
588
|
+
const newKids = Array.prototype.slice.call(next.children);
|
|
589
|
+
const oldByKey = {};
|
|
590
|
+
oldKids.forEach(function(c) {
|
|
591
|
+
const k = _liveKey(c);
|
|
592
|
+
if (k) oldByKey[k] = c;
|
|
593
|
+
});
|
|
594
|
+
const order = [];
|
|
595
|
+
for (let i = 0; i < newKids.length; i++) {
|
|
596
|
+
const nk = newKids[i];
|
|
597
|
+
const k = _liveKey(nk);
|
|
598
|
+
let match = null;
|
|
599
|
+
if (k && oldByKey[k]) {
|
|
600
|
+
match = oldByKey[k];
|
|
601
|
+
} else if (!k && oldKids[i] && !_liveKey(oldKids[i]) && oldKids[i].tagName === nk.tagName) {
|
|
602
|
+
match = oldKids[i];
|
|
603
|
+
}
|
|
604
|
+
if (match && match.tagName === nk.tagName) {
|
|
605
|
+
_liveMorphNode(match, nk);
|
|
606
|
+
reused.push(match);
|
|
607
|
+
order.push(match);
|
|
608
|
+
} else {
|
|
609
|
+
order.push(nk);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
let cursor = parent.firstElementChild;
|
|
613
|
+
for (let i = 0; i < order.length; i++) {
|
|
614
|
+
const node = order[i];
|
|
615
|
+
if (node === cursor) {
|
|
616
|
+
cursor = cursor.nextElementSibling;
|
|
617
|
+
} else {
|
|
618
|
+
parent.insertBefore(node, cursor);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
oldKids.forEach(function(c) {
|
|
622
|
+
if (order.indexOf(c) === -1 && c.parentNode === parent) parent.removeChild(c);
|
|
623
|
+
});
|
|
624
|
+
void reused;
|
|
625
|
+
}
|
|
626
|
+
function _liveSwap(container, html) {
|
|
627
|
+
const tmp = document.createElement("div");
|
|
628
|
+
tmp.innerHTML = html;
|
|
629
|
+
if (!tmp.children.length || !container.children.length) {
|
|
630
|
+
container.innerHTML = html;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
_liveReconcile(container, tmp);
|
|
634
|
+
}
|
|
635
|
+
function _liveWsUrl(path) {
|
|
636
|
+
if (/^wss?:\/\//.test(path)) return path;
|
|
637
|
+
const proto = typeof location !== "undefined" && location.protocol === "https:" ? "wss" : "ws";
|
|
638
|
+
return proto + "://" + location.host + path;
|
|
639
|
+
}
|
|
640
|
+
function _liveExtract(msg, name) {
|
|
641
|
+
if (msg && typeof msg === "object") {
|
|
642
|
+
if (msg.type === "live") {
|
|
643
|
+
if (name && msg.name && msg.name !== name) return null;
|
|
644
|
+
return msg.html != null ? String(msg.html) : null;
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
return typeof msg === "string" ? msg : null;
|
|
649
|
+
}
|
|
650
|
+
function liveInit(root) {
|
|
651
|
+
if (typeof document === "undefined") return;
|
|
652
|
+
const scope = root || document;
|
|
653
|
+
const blocks = scope.querySelectorAll("[data-frond-live]");
|
|
654
|
+
Array.prototype.slice.call(blocks).forEach(function(el) {
|
|
655
|
+
if (el.__frondLive) return;
|
|
656
|
+
el.__frondLive = true;
|
|
657
|
+
const mode = el.getAttribute("data-mode");
|
|
658
|
+
const name = el.getAttribute("data-frond-live");
|
|
659
|
+
if (mode === "poll") {
|
|
660
|
+
const src = el.getAttribute("data-src");
|
|
661
|
+
const interval = (parseInt(el.getAttribute("data-interval"), 10) || 5) * 1e3;
|
|
662
|
+
const timer = setInterval(function() {
|
|
663
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
664
|
+
request(src, { method: "GET", onSuccess: function(data) {
|
|
665
|
+
_liveSwap(el, typeof data === "string" ? data : String(data));
|
|
666
|
+
} });
|
|
667
|
+
}, interval);
|
|
668
|
+
el.__frondLiveStop = function() {
|
|
669
|
+
clearInterval(timer);
|
|
670
|
+
};
|
|
671
|
+
} else if (mode === "ws") {
|
|
672
|
+
const sock = wsConnect(_liveWsUrl(el.getAttribute("data-ws")));
|
|
673
|
+
sock.on("message", function(msg) {
|
|
674
|
+
const h = _liveExtract(msg, name);
|
|
675
|
+
if (h !== null) _liveSwap(el, h);
|
|
676
|
+
});
|
|
677
|
+
el.__frondLiveStop = function() {
|
|
678
|
+
sock.close();
|
|
679
|
+
};
|
|
680
|
+
} else if (mode === "sse") {
|
|
681
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
682
|
+
console.warn("[frond.live] sse transport is not wired yet (v1 supports poll and ws); block '" + name + "' shows first paint only. Use poll or ws.");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
561
687
|
var frond = {
|
|
562
688
|
/** Core HTTP request. */
|
|
563
689
|
request,
|
|
@@ -573,6 +699,8 @@ var _frondModule = (() => {
|
|
|
573
699
|
ws: wsConnect,
|
|
574
700
|
/** Server-Sent Events with auto-reconnect. */
|
|
575
701
|
sse: sseConnect,
|
|
702
|
+
/** Wire {% live %} blocks (poll/ws) with keyed morph. Auto-runs on DOMContentLoaded. */
|
|
703
|
+
live: liveInit,
|
|
576
704
|
/** Cookie helpers: get, set, remove. */
|
|
577
705
|
cookie,
|
|
578
706
|
/** Display alert message in #message element. */
|
|
@@ -593,8 +721,17 @@ var _frondModule = (() => {
|
|
|
593
721
|
};
|
|
594
722
|
if (typeof window !== "undefined") {
|
|
595
723
|
window.frond = frond;
|
|
724
|
+
if (typeof document !== "undefined") {
|
|
725
|
+
if (document.readyState === "loading") {
|
|
726
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
727
|
+
liveInit();
|
|
728
|
+
});
|
|
729
|
+
} else {
|
|
730
|
+
liveInit();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
596
733
|
}
|
|
597
734
|
return __toCommonJS(frond_exports);
|
|
598
735
|
})();
|
|
599
|
-
/* Frond v2.
|
|
736
|
+
/* Frond v2.2.0 - tina4.com */
|
|
600
737
|
//# sourceMappingURL=frond.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var _frondModule=(()=>{var b=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var O=(o,s)=>{for(var e in s)b(o,e,{get:s[e],enumerable:!0})},M=(o,s,e,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let n of x(s))!C.call(o,n)&&n!==e&&b(o,n,{get:()=>s[n],enumerable:!(t=k(s,n))||t.enumerable});return o};var q=o=>M(b({},"__esModule",{value:!0}),o);var j={};O(j,{frond:()=>R});var g=null;function w(o,s){let e;typeof s=="function"?e={onSuccess:s}:e=s||{};let t=(e.method||"GET").toUpperCase(),n=new XMLHttpRequest;if(n.open(t,o,!0),g!==null&&n.setRequestHeader("Authorization","Bearer "+g),e.headers)for(let r in e.headers)Object.prototype.hasOwnProperty.call(e.headers,r)&&n.setRequestHeader(r,e.headers[r]);let i=null;e.body!==void 0&&e.body!==null&&(e.body instanceof FormData?i=e.body:typeof e.body=="object"?(i=JSON.stringify(e.body),n.setRequestHeader("Content-Type","application/json; charset=UTF-8")):typeof e.body=="string"&&(i=e.body,n.setRequestHeader("Content-Type","text/plain; charset=UTF-8"))),n.onload=function(){let r=n.getResponseHeader("FreshToken");r&&r!==""&&(g=r);let u=n.response;try{u=JSON.parse(u)}catch{}if(n.responseURL){let c=new URL(o,window.location.href).href;if(n.responseURL!==c){window.location.href=n.responseURL;return}}n.status>=200&&n.status<400?e.onSuccess&&e.onSuccess(u,n.status,n):e.onError&&e.onError(n.status,n)},n.onerror=function(){e.onError&&e.onError(n.status,n)},n.send(i)}function h(o,s){if(!o)return"";let e=new DOMParser,t=o.includes("<html>")?o:"<body>"+o+"</body></html>",i=e.parseFromString(t,"text/html").querySelector("body"),r=i.querySelectorAll("script");if(r.forEach(function(u){u.remove()}),s!==null){let u=document.getElementById(s);return u&&(i.children.length>0?u.replaceChildren.apply(u,Array.from(i.children)):u.innerHTML=i.innerHTML,r.forEach(function(c){let d=document.createElement("script");d.type="text/javascript",d.async=!0,c.src?d.src=c.src:d.textContent=c.textContent,u.appendChild(d)})),""}return r.forEach(function(u){let c=document.createElement("script");c.type="text/javascript",c.async=!0,c.textContent=u.textContent,document.body.appendChild(c)}),i.innerHTML}function H(o,s,e){let t=s||"content";w(o,{method:"GET",onSuccess:function(n,i){if(document.getElementById(t)){let r=h(n,t);e&&e(r,n)}else e&&e(n)}})}function S(o,s,e,t){let n=e||"content";w(o,{method:"POST",body:s,onSuccess:function(i){let r="";if(i&&i.message!==void 0)r=h(i.message,n);else if(document.getElementById(n))r=h(i,n);else{t&&t(i);return}t&&t(r,i)}})}var T={collect:function(o){let s=new FormData,e=document.querySelectorAll("#"+o+" select, #"+o+" input, #"+o+" textarea");for(let t=0;t<e.length;t++){let n=e[t];if(n.name==="formToken"&&g!==null&&(n.value=g),!!n.name)if(n.type==="file"){let i=n.files;if(i)for(let r=0;r<i.length;r++){let u=i[r];if(u!==void 0){let c=n.name;i.length>1&&!c.includes("[")&&(c=c+"[]"),s.append(c,u,u.name)}}}else n.type==="checkbox"||n.type==="radio"?n.checked?s.append(n.name,n.value):n.type!=="radio"&&s.append(n.name,"0"):s.append(n.name,n.value===""?"":n.value)}return s},submit:function(o,s,e,t){let n=T.collect(o);S(s,n,e||"message",t)},show:function(o,s,e,t){let n=o.toUpperCase();(o==="create"||o==="edit")&&(n="GET"),o==="delete"&&(n="DELETE");let i=e||"form";w(s,{method:n,onSuccess:function(r){let u="";if(r&&r.message!==void 0)u=h(r.message,i);else if(document.getElementById(i))u=h(r,i);else{t&&t(r);return}t&&t(u)}})}};function L(o,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,protocols:[],onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},t=null,n=!1,i=e.reconnectDelay,r=0,u=null,c={message:[],open:[],close:[],error:[]},d={status:"connecting",send:function(l){if(!t||t.readyState!==WebSocket.OPEN)throw new Error("[frond] WebSocket is not connected");t.send(typeof l=="string"?l:JSON.stringify(l))},on:function(l,a){return c[l]||(c[l]=[]),c[l].push(a),function(){let f=c[l],m=f.indexOf(a);m>=0&&f.splice(m,1)}},close:function(l,a){n=!0,u&&(clearTimeout(u),u=null),t&&t.close(l||1e3,a||""),d.status="closed"}};function y(l){if(typeof l!="string")return l;try{return JSON.parse(l)}catch{return l}}function p(){!e.reconnect||r>=e.maxReconnectAttempts||(r++,d.status="reconnecting",u=setTimeout(function(){u=null,v()},i),i=Math.min(i*2,e.maxReconnectDelay))}function v(){d.status=r>0?"reconnecting":"connecting";try{t=new WebSocket(o,e.protocols)}catch{d.status="closed";return}t.onopen=function(){d.status="open",r=0,i=e.reconnectDelay,e.onOpen();for(let l of c.open)l()},t.onmessage=function(l){let a=y(l.data);for(let f of c.message)f(a)},t.onclose=function(l){d.status="closed",e.onClose(l.code,l.reason);for(let a of c.close)a(l.code,l.reason);n||p()},t.onerror=function(l){e.onError(l);for(let a of c.error)a(l)}}return v(),d}function D(o,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,events:[],json:!0,onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},t=null,n=!1,i=e.reconnectDelay,r=0,u=null,c={message:[],open:[],close:[],error:[]},d={status:"connecting",on:function(a,f){return c[a]||(c[a]=[]),c[a].push(f),function(){let m=c[a],E=m.indexOf(f);E>=0&&m.splice(E,1)}},close:function(){n=!0,u&&(clearTimeout(u),u=null),t&&(t.close(),t=null),d.status="closed"}};function y(a){if(!e.json)return a;try{return JSON.parse(a)}catch{return a}}function p(a,f){for(let m of c.message)m(a,f||void 0)}function v(){!e.reconnect||r>=e.maxReconnectAttempts||(r++,d.status="reconnecting",u=setTimeout(function(){u=null,l()},i),i=Math.min(i*2,e.maxReconnectDelay))}function l(){d.status=r>0?"reconnecting":"connecting";try{t=new EventSource(o)}catch{d.status="closed";return}t.onopen=function(){d.status="open",r=0,i=e.reconnectDelay,e.onOpen();for(let a of c.open)a(null)},t.onmessage=function(a){p(y(a.data),null)};for(let a of e.events)t.addEventListener(a,function(f){p(y(f.data),a)});t.onerror=function(a){e.onError(a);for(let f of c.error)f(a);if(t&&t.readyState===2){t=null,d.status="closed",e.onClose();for(let f of c.close)f(null);n||v()}}}return l(),d}var W={set:function(o,s,e){let t="";if(e){let n=new Date;n.setTime(n.getTime()+e*24*60*60*1e3),t="; expires="+n.toUTCString()}document.cookie=o+"="+(s||"")+t+"; path=/"},get:function(o){let s=o+"=",e=document.cookie.split(";");for(let t=0;t<e.length;t++){let n=e[t];for(;n.charAt(0)===" ";)n=n.substring(1);if(n.indexOf(s)===0)return n.substring(s.length)}return null},remove:function(o){document.cookie=o+"=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"}};function A(o,s){let e=document.getElementById("message");if(!e)return;let t=s||"info";e.innerHTML='<div class="alert alert-'+t+' alert-dismissible">'+o+'<button type="button" class="btn-close" data-t4-dismiss="alert">×</button></div>'}function I(o,s,e,t){let n=window.screenLeft!==void 0?window.screenLeft:window.screenX,i=window.screenTop!==void 0?window.screenTop:window.screenY,r=window.innerWidth||document.documentElement.clientWidth||screen.width,u=window.innerHeight||document.documentElement.clientHeight||screen.height,c=r/window.screen.availWidth,d=(r-e)/2/c+n,y=(u-t)/2/c+i,p=window.open(o,s,"directories=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width="+e/c+",height="+t/c+",top="+y+",left="+d);return window.focus&&p&&p.focus(),p}function N(o){if(o.indexOf("No data available")>=0){window.alert("No data available for this report.");return}window.open(o,"_blank","toolbar=no,scrollbars=yes,resizable=yes,width=800,height=600,top=0,left=0")}function U(o,s,e,t){w(o,{method:"POST",body:{query:s,variables:e||{}},onSuccess:function(n){t&&t(n.data||null,n.errors||void 0)},onError:function(n){t&&t(null,[{message:"GraphQL request failed with status "+n}])}})}var R={request:w,load:H,post:S,inject:h,form:T,ws:L,sse:D,cookie:W,message:A,popup:I,report:N,graphql:U,get token(){return g},set token(o){g=o}};typeof window<"u"&&(window.frond=R);return q(j);})();
|
|
2
|
-
/* Frond v2.
|
|
1
|
+
var _frondModule=(()=>{var E=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var q=(t,s)=>{for(var e in s)E(t,e,{get:s[e],enumerable:!0})},D=(t,s,e,o)=>{if(s&&typeof s=="object"||typeof s=="function")for(let n of A(s))!H.call(t,n)&&n!==e&&E(t,n,{get:()=>s[n],enumerable:!(o=O(s,n))||o.enumerable});return t};var W=t=>D(E({},"__esModule",{value:!0}),t);var z={};q(z,{frond:()=>L});var y=null;function v(t,s){let e;typeof s=="function"?e={onSuccess:s}:e=s||{};let o=(e.method||"GET").toUpperCase(),n=new XMLHttpRequest;if(n.open(o,t,!0),y!==null&&n.setRequestHeader("Authorization","Bearer "+y),e.headers)for(let u in e.headers)Object.prototype.hasOwnProperty.call(e.headers,u)&&n.setRequestHeader(u,e.headers[u]);let c=null;e.body!==void 0&&e.body!==null&&(e.body instanceof FormData?c=e.body:typeof e.body=="object"?(c=JSON.stringify(e.body),n.setRequestHeader("Content-Type","application/json; charset=UTF-8")):typeof e.body=="string"&&(c=e.body,n.setRequestHeader("Content-Type","text/plain; charset=UTF-8"))),n.onload=function(){let u=n.getResponseHeader("FreshToken");u&&u!==""&&(y=u);let r=n.response;try{r=JSON.parse(r)}catch{}if(n.responseURL){let i=new URL(t,window.location.href).href;if(n.responseURL!==i){window.location.href=n.responseURL;return}}n.status>=200&&n.status<400?e.onSuccess&&e.onSuccess(r,n.status,n):e.onError&&e.onError(n.status,n)},n.onerror=function(){e.onError&&e.onError(n.status,n)},n.send(c)}function h(t,s){if(!t)return"";let e=new DOMParser,o=t.includes("<html>")?t:"<body>"+t+"</body></html>",c=e.parseFromString(o,"text/html").querySelector("body"),u=c.querySelectorAll("script");if(u.forEach(function(r){r.remove()}),s!==null){let r=document.getElementById(s);return r&&(c.children.length>0?r.replaceChildren.apply(r,Array.from(c.children)):r.innerHTML=c.innerHTML,u.forEach(function(i){let l=document.createElement("script");l.type="text/javascript",l.async=!0,i.src?l.src=i.src:l.textContent=i.textContent,r.appendChild(l)})),""}return u.forEach(function(r){let i=document.createElement("script");i.type="text/javascript",i.async=!0,i.textContent=r.textContent,document.body.appendChild(i)}),c.innerHTML}function _(t,s,e){let o=s||"content";v(t,{method:"GET",onSuccess:function(n,c){if(document.getElementById(o)){let u=h(n,o);e&&e(u,n)}else e&&e(n)}})}function R(t,s,e,o){let n=e||"content";v(t,{method:"POST",body:s,onSuccess:function(c){let u="";if(c&&c.message!==void 0)u=h(c.message,n);else if(document.getElementById(n))u=h(c,n);else{o&&o(c);return}o&&o(u,c)}})}var x={collect:function(t){let s=new FormData,e=document.querySelectorAll("#"+t+" select, #"+t+" input, #"+t+" textarea");for(let o=0;o<e.length;o++){let n=e[o];if(n.name==="formToken"&&y!==null&&(n.value=y),!!n.name)if(n.type==="file"){let c=n.files;if(c)for(let u=0;u<c.length;u++){let r=c[u];if(r!==void 0){let i=n.name;c.length>1&&!i.includes("[")&&(i=i+"[]"),s.append(i,r,r.name)}}}else n.type==="checkbox"||n.type==="radio"?n.checked?s.append(n.name,n.value):n.type!=="radio"&&s.append(n.name,"0"):s.append(n.name,n.value===""?"":n.value)}return s},submit:function(t,s,e,o){let n=x.collect(t);R(s,n,e||"message",o)},show:function(t,s,e,o){let n=t.toUpperCase();(t==="create"||t==="edit")&&(n="GET"),t==="delete"&&(n="DELETE");let c=e||"form";v(s,{method:n,onSuccess:function(u){let r="";if(u&&u.message!==void 0)r=h(u.message,c);else if(document.getElementById(c))r=h(u,c);else{o&&o(u);return}o&&o(r)}})}};function M(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,protocols:[],onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",send:function(f){if(!o||o.readyState!==WebSocket.OPEN)throw new Error("[frond] WebSocket is not connected");o.send(typeof f=="string"?f:JSON.stringify(f))},on:function(f,a){return i[f]||(i[f]=[]),i[f].push(a),function(){let d=i[f],g=d.indexOf(a);g>=0&&d.splice(g,1)}},close:function(f,a){n=!0,r&&(clearTimeout(r),r=null),o&&o.close(f||1e3,a||""),l.status="closed"}};function p(f){if(typeof f!="string")return f;try{return JSON.parse(f)}catch{return f}}function m(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,w()},c),c=Math.min(c*2,e.maxReconnectDelay))}function w(){l.status=u>0?"reconnecting":"connecting";try{o=new WebSocket(t,e.protocols)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let f of i.open)f()},o.onmessage=function(f){let a=p(f.data);for(let d of i.message)d(a)},o.onclose=function(f){l.status="closed",e.onClose(f.code,f.reason);for(let a of i.close)a(f.code,f.reason);n||m()},o.onerror=function(f){e.onError(f);for(let a of i.error)a(f)}}return w(),l}function I(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,events:[],json:!0,onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",on:function(a,d){return i[a]||(i[a]=[]),i[a].push(d),function(){let g=i[a],T=g.indexOf(d);T>=0&&g.splice(T,1)}},close:function(){n=!0,r&&(clearTimeout(r),r=null),o&&(o.close(),o=null),l.status="closed"}};function p(a){if(!e.json)return a;try{return JSON.parse(a)}catch{return a}}function m(a,d){for(let g of i.message)g(a,d||void 0)}function w(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,f()},c),c=Math.min(c*2,e.maxReconnectDelay))}function f(){l.status=u>0?"reconnecting":"connecting";try{o=new EventSource(t)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let a of i.open)a(null)},o.onmessage=function(a){m(p(a.data),null)};for(let a of e.events)o.addEventListener(a,function(d){m(p(d.data),a)});o.onerror=function(a){e.onError(a);for(let d of i.error)d(a);if(o&&o.readyState===2){o=null,l.status="closed",e.onClose();for(let d of i.close)d(null);n||w()}}}return f(),l}var U={set:function(t,s,e){let o="";if(e){let n=new Date;n.setTime(n.getTime()+e*24*60*60*1e3),o="; expires="+n.toUTCString()}document.cookie=t+"="+(s||"")+o+"; path=/"},get:function(t){let s=t+"=",e=document.cookie.split(";");for(let o=0;o<e.length;o++){let n=e[o];for(;n.charAt(0)===" ";)n=n.substring(1);if(n.indexOf(s)===0)return n.substring(s.length)}return null},remove:function(t){document.cookie=t+"=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"}};function j(t,s){let e=document.getElementById("message");if(!e)return;let o=s||"info";e.innerHTML='<div class="alert alert-'+o+' alert-dismissible">'+t+'<button type="button" class="btn-close" data-t4-dismiss="alert">×</button></div>'}function B(t,s,e,o){let n=window.screenLeft!==void 0?window.screenLeft:window.screenX,c=window.screenTop!==void 0?window.screenTop:window.screenY,u=window.innerWidth||document.documentElement.clientWidth||screen.width,r=window.innerHeight||document.documentElement.clientHeight||screen.height,i=u/window.screen.availWidth,l=(u-e)/2/i+n,p=(r-o)/2/i+c,m=window.open(t,s,"directories=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width="+e/i+",height="+o/i+",top="+p+",left="+l);return window.focus&&m&&m.focus(),m}function P(t){if(t.indexOf("No data available")>=0){window.alert("No data available for this report.");return}window.open(t,"_blank","toolbar=no,scrollbars=yes,resizable=yes,width=800,height=600,top=0,left=0")}function F(t,s,e,o){v(t,{method:"POST",body:{query:s,variables:e||{}},onSuccess:function(n){o&&o(n.data||null,n.errors||void 0)},onError:function(n){o&&o(null,[{message:"GraphQL request failed with status "+n}])}})}function b(t){let s=t.dataset;return s&&s.key?s.key:null}function X(t,s){let e=s.attributes;for(let n=0;n<e.length;n++){let c=e[n];t.getAttribute(c.name)!==c.value&&t.setAttribute(c.name,c.value)}Array.prototype.slice.call(t.attributes).forEach(function(n){s.hasAttribute(n.name)||t.removeAttribute(n.name)})}function G(t,s){let e=s.tagName;e==="INPUT"||e==="TEXTAREA"||e==="SELECT"||(X(t,s),t.children.length||s.children.length?C(t,s):t.innerHTML!==s.innerHTML&&(t.innerHTML=s.innerHTML))}function C(t,s){let e=Array.prototype.slice.call(t.children),o=Array.prototype.slice.call(s.children),n={};e.forEach(function(r){let i=b(r);i&&(n[i]=r)});let c=[];for(let r=0;r<o.length;r++){let i=o[r],l=b(i),p=null;l&&n[l]?p=n[l]:!l&&e[r]&&!b(e[r])&&e[r].tagName===i.tagName&&(p=e[r]),p&&p.tagName===i.tagName?(G(p,i),reused.push(p),c.push(p)):c.push(i)}let u=t.firstElementChild;for(let r=0;r<c.length;r++){let i=c[r];i===u?u=u.nextElementSibling:t.insertBefore(i,u)}e.forEach(function(r){c.indexOf(r)===-1&&r.parentNode===t&&t.removeChild(r)}),reused}function k(t,s){let e=document.createElement("div");if(e.innerHTML=s,!e.children.length||!t.children.length){t.innerHTML=s;return}C(t,e)}function J(t){return/^wss?:\/\//.test(t)?t:(typeof location<"u"&&location.protocol==="https:"?"wss":"ws")+"://"+location.host+t}function N(t,s){return t&&typeof t=="object"?t.type==="live"?s&&t.name&&t.name!==s?null:t.html!=null?String(t.html):null:null:typeof t=="string"?t:null}function S(t){if(typeof document>"u")return;let e=(t||document).querySelectorAll("[data-frond-live]");Array.prototype.slice.call(e).forEach(function(o){if(o.__frondLive)return;o.__frondLive=!0;let n=o.getAttribute("data-mode"),c=o.getAttribute("data-frond-live");if(n==="poll"){let u=o.getAttribute("data-src"),r=(parseInt(o.getAttribute("data-interval"),10)||5)*1e3,i=setInterval(function(){typeof document<"u"&&document.hidden||v(u,{method:"GET",onSuccess:function(l){k(o,typeof l=="string"?l:String(l))}})},r);o.__frondLiveStop=function(){clearInterval(i)}}else if(n==="ws"){let u=M(J(o.getAttribute("data-ws")));u.on("message",function(r){let i=N(r,c);i!==null&&k(o,i)}),o.__frondLiveStop=function(){u.close()}}else n==="sse"&&typeof console<"u"&&console.warn&&console.warn("[frond.live] sse transport is not wired yet (v1 supports poll and ws); block '"+c+"' shows first paint only. Use poll or ws.")})}var L={request:v,load:_,post:R,inject:h,form:x,ws:M,sse:I,live:S,cookie:U,message:j,popup:B,report:P,graphql:F,get token(){return y},set token(t){y=t}};typeof window<"u"&&(window.frond=L,typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){S()}):S()));return W(z);})();
|
|
2
|
+
/* Frond v2.2.0 - tina4.com */
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -34,8 +34,11 @@ module Tina4
|
|
|
34
34
|
fallback = Dir.exist?(FRAMEWORK_PUBLIC_DIR) ? [FRAMEWORK_PUBLIC_DIR] : []
|
|
35
35
|
@static_roots = (project_roots + fallback).freeze
|
|
36
36
|
|
|
37
|
-
# Shared WebSocket engine for route-based WS handling
|
|
37
|
+
# Shared WebSocket engine for route-based WS handling. Publish it as the
|
|
38
|
+
# process-wide "current" engine so Frond.push_live (and other framework
|
|
39
|
+
# code) can broadcast live-block updates without a threaded reference.
|
|
38
40
|
@websocket_engine = Tina4::WebSocket.new
|
|
41
|
+
Tina4::WebSocket.current = @websocket_engine
|
|
39
42
|
|
|
40
43
|
# Register the dev-reload WebSocket route (debug mode only) so a browser
|
|
41
44
|
# handshake to /__dev_reload is accepted and held open by the connection
|
data/lib/tina4/scss_compiler.rb
CHANGED
|
@@ -111,6 +111,10 @@ module Tina4
|
|
|
111
111
|
content = content.gsub("$#{name}", value)
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
# Resolve color functions (lighten/darken/rgba/rgb/mix) — after variable
|
|
115
|
+
# substitution so a $colour arg is already a hex literal (issue #124).
|
|
116
|
+
content = resolve_color_functions(content)
|
|
117
|
+
|
|
114
118
|
# Handle nesting (basic single-level)
|
|
115
119
|
content = flatten_nesting(content)
|
|
116
120
|
|
|
@@ -120,6 +124,109 @@ module Tina4
|
|
|
120
124
|
content
|
|
121
125
|
end
|
|
122
126
|
|
|
127
|
+
# Resolve lighten/darken/rgba/rgb/mix color functions. rgba(<hex>, a) is
|
|
128
|
+
# the damaging case (issue #124): the functional rgba() notation cannot
|
|
129
|
+
# take a hex, so rgba(#0f3460, 0.12) is invalid CSS and browsers drop the
|
|
130
|
+
# whole declaration. Convert the hex to its r,g,b components. hex_to_rgb /
|
|
131
|
+
# adjust_lightness are regex-free so they never clobber the match globals.
|
|
132
|
+
def resolve_color_functions(content)
|
|
133
|
+
content = content.gsub(/lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/) do
|
|
134
|
+
adjust_lightness(Regexp.last_match(1).strip, Regexp.last_match(2).strip.chomp("%").to_f / 100)
|
|
135
|
+
end
|
|
136
|
+
content = content.gsub(/darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/) do
|
|
137
|
+
adjust_lightness(Regexp.last_match(1).strip, -(Regexp.last_match(2).strip.chomp("%").to_f / 100))
|
|
138
|
+
end
|
|
139
|
+
# rgba(<hex>, <alpha>) — only the two-arg hex form; leave rgba(r,g,b,a).
|
|
140
|
+
content = content.gsub(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/) do
|
|
141
|
+
hex = Regexp.last_match(1)
|
|
142
|
+
alpha = Regexp.last_match(2).strip
|
|
143
|
+
whole = Regexp.last_match(0)
|
|
144
|
+
rgb = hex_to_rgb(hex)
|
|
145
|
+
rgb ? "rgba(#{rgb[0]}, #{rgb[1]}, #{rgb[2]}, #{alpha})" : whole
|
|
146
|
+
end
|
|
147
|
+
content = content.gsub(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/) do
|
|
148
|
+
hex = Regexp.last_match(1)
|
|
149
|
+
whole = Regexp.last_match(0)
|
|
150
|
+
rgb = hex_to_rgb(hex)
|
|
151
|
+
rgb ? "rgb(#{rgb[0]}, #{rgb[1]}, #{rgb[2]})" : whole
|
|
152
|
+
end
|
|
153
|
+
# mix(<c1>, <c2>[, <weight>]) — Sass weight is c1's proportion (default 50%).
|
|
154
|
+
content.gsub(/mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/) do
|
|
155
|
+
hex1 = Regexp.last_match(1)
|
|
156
|
+
hex2 = Regexp.last_match(2)
|
|
157
|
+
weight = Regexp.last_match(3)
|
|
158
|
+
whole = Regexp.last_match(0)
|
|
159
|
+
c1 = hex_to_rgb(hex1)
|
|
160
|
+
c2 = hex_to_rgb(hex2)
|
|
161
|
+
if c1 && c2
|
|
162
|
+
w = weight ? weight.chomp("%").to_f / 100 : 0.5
|
|
163
|
+
mixed = (0..2).map { |i| (c1[i] * w + c2[i] * (1 - w)).round }
|
|
164
|
+
format("#%02x%02x%02x", *mixed)
|
|
165
|
+
else
|
|
166
|
+
whole
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Parse a #rgb / #rrggbb hex string into an [r, g, b] int array, or nil.
|
|
172
|
+
# Regex-free (uses Integer()) so it never touches the match globals.
|
|
173
|
+
def hex_to_rgb(color)
|
|
174
|
+
c = color.strip.delete_prefix("#")
|
|
175
|
+
c = c.chars.map { |ch| ch * 2 }.join if c.length == 3
|
|
176
|
+
return nil unless c.length == 6
|
|
177
|
+
|
|
178
|
+
[Integer(c[0, 2], 16), Integer(c[2, 2], 16), Integer(c[4, 2], 16)]
|
|
179
|
+
rescue ArgumentError
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Adjust the HSL lightness of a hex color by `amount` (-1..1), return hex.
|
|
184
|
+
# Truncates (to_i) to match the Python master's int(x*255) byte-for-byte.
|
|
185
|
+
def adjust_lightness(color, amount)
|
|
186
|
+
rgb = hex_to_rgb(color)
|
|
187
|
+
return color if rgb.nil?
|
|
188
|
+
|
|
189
|
+
r, g, b = rgb.map { |v| v / 255.0 }
|
|
190
|
+
max = [r, g, b].max
|
|
191
|
+
min = [r, g, b].min
|
|
192
|
+
l = (max + min) / 2.0
|
|
193
|
+
d = max - min
|
|
194
|
+
h = 0.0
|
|
195
|
+
s = 0.0
|
|
196
|
+
if d != 0.0
|
|
197
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
198
|
+
h = if max == r
|
|
199
|
+
(g - b) / d + (g < b ? 6 : 0)
|
|
200
|
+
elsif max == g
|
|
201
|
+
(b - r) / d + 2
|
|
202
|
+
else
|
|
203
|
+
(r - g) / d + 4
|
|
204
|
+
end
|
|
205
|
+
h /= 6.0
|
|
206
|
+
end
|
|
207
|
+
l = [0.0, [1.0, l + amount].min].max
|
|
208
|
+
if s.zero?
|
|
209
|
+
r = g = b = l
|
|
210
|
+
else
|
|
211
|
+
q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
|
212
|
+
p = 2 * l - q
|
|
213
|
+
r = _hue_to_rgb(p, q, h + 1.0 / 3)
|
|
214
|
+
g = _hue_to_rgb(p, q, h)
|
|
215
|
+
b = _hue_to_rgb(p, q, h - 1.0 / 3)
|
|
216
|
+
end
|
|
217
|
+
format("#%02x%02x%02x", (r * 255).to_i, (g * 255).to_i, (b * 255).to_i)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def _hue_to_rgb(p, q, t)
|
|
221
|
+
t += 1 if t < 0
|
|
222
|
+
t -= 1 if t > 1
|
|
223
|
+
return p + (q - p) * 6 * t if t < 1.0 / 6
|
|
224
|
+
return q if t < 1.0 / 2
|
|
225
|
+
return p + (q - p) * (2.0 / 3 - t) * 6 if t < 2.0 / 3
|
|
226
|
+
|
|
227
|
+
p
|
|
228
|
+
end
|
|
229
|
+
|
|
123
230
|
# Resolve SCSS #{ ... } interpolation. Each #{ expr } is replaced by its
|
|
124
231
|
# resolved inner text: a $variable inside the braces resolves to its value,
|
|
125
232
|
# anything else is inlined verbatim (trimmed). Lets a value carry a
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/websocket.rb
CHANGED
|
@@ -135,6 +135,16 @@ module Tina4
|
|
|
135
135
|
class WebSocket
|
|
136
136
|
GUID = WEBSOCKET_GUID
|
|
137
137
|
|
|
138
|
+
# Process-wide handle to the live route-serving WebSocket engine. RackApp
|
|
139
|
+
# sets this on construction so framework code (Frond.push_live, background
|
|
140
|
+
# tasks) can broadcast without threading an engine reference through every
|
|
141
|
+
# call site. Mirrors PHP's \Tina4\Server::getInstance(). Nil until a RackApp
|
|
142
|
+
# is built (e.g. in unit specs) — best-effort callers degrade silently.
|
|
143
|
+
@current = nil
|
|
144
|
+
class << self
|
|
145
|
+
attr_accessor :current
|
|
146
|
+
end
|
|
147
|
+
|
|
138
148
|
attr_reader :connections
|
|
139
149
|
|
|
140
150
|
def initialize
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.52
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|