opal-vite 0.3.0 → 0.3.1
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8736c0cccd990adbc7f614480718ba0bab767f94ad281f508b0a01b9898deb3d
|
|
4
|
+
data.tar.gz: 93ab376b58edb586aed577d0d4a19011f7f84660c62eebffe418eccb4b9b1e73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c0d8a02384a2e1f958d2a0ce62f66fe8f63db8c87cbe709089e04ca9441e2d37a9a41b860170cdb2ca8f24467a79ce12b2587d1312e0d59dd22e75020429911
|
|
7
|
+
data.tar.gz: 7aae2b96e735993cfd583cdc98ea0ad6bbd49110ef63209b17f6f49e614bf7edc0d1948612f40522ed9aa6e1839fa6e02731a7f6764ef269b3b61c17cee5c2b3
|
data/lib/opal/vite/version.rb
CHANGED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
|
|
3
|
+
module OpalVite
|
|
4
|
+
module Concerns
|
|
5
|
+
module V1
|
|
6
|
+
# Base64Helpers - provides Base64 encoding/decoding utilities
|
|
7
|
+
#
|
|
8
|
+
# This module wraps JavaScript's btoa/atob and provides additional
|
|
9
|
+
# utilities for Base64 operations commonly needed in web applications.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# class MyController < StimulusController
|
|
13
|
+
# include OpalVite::Concerns::V1::Base64Helpers
|
|
14
|
+
#
|
|
15
|
+
# def connect
|
|
16
|
+
# encoded = base64_encode("Hello, World!")
|
|
17
|
+
# puts encoded # => "SGVsbG8sIFdvcmxkIQ=="
|
|
18
|
+
#
|
|
19
|
+
# decoded = base64_decode(encoded)
|
|
20
|
+
# puts decoded # => "Hello, World!"
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
module Base64Helpers
|
|
25
|
+
# ===== Basic Encoding/Decoding =====
|
|
26
|
+
|
|
27
|
+
# Encode a string to Base64
|
|
28
|
+
# @param str [String] String to encode
|
|
29
|
+
# @return [String] Base64 encoded string
|
|
30
|
+
def base64_encode(str)
|
|
31
|
+
`btoa(#{str})`
|
|
32
|
+
rescue
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Decode a Base64 string
|
|
37
|
+
# @param str [String] Base64 string to decode
|
|
38
|
+
# @return [String] Decoded string
|
|
39
|
+
def base64_decode(str)
|
|
40
|
+
`atob(#{str})`
|
|
41
|
+
rescue
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ===== URL-Safe Base64 =====
|
|
46
|
+
|
|
47
|
+
# Encode a string to URL-safe Base64
|
|
48
|
+
# @param str [String] String to encode
|
|
49
|
+
# @return [String] URL-safe Base64 encoded string
|
|
50
|
+
def base64_encode_urlsafe(str)
|
|
51
|
+
encoded = base64_encode(str)
|
|
52
|
+
return nil unless encoded
|
|
53
|
+
|
|
54
|
+
# Replace + with -, / with _, and remove =
|
|
55
|
+
encoded.gsub('+', '-').gsub('/', '_').gsub('=', '')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Decode a URL-safe Base64 string
|
|
59
|
+
# @param str [String] URL-safe Base64 string to decode
|
|
60
|
+
# @return [String] Decoded string
|
|
61
|
+
def base64_decode_urlsafe(str)
|
|
62
|
+
# Restore standard Base64 characters
|
|
63
|
+
standard = str.gsub('-', '+').gsub('_', '/')
|
|
64
|
+
|
|
65
|
+
# Add padding if needed
|
|
66
|
+
case standard.length % 4
|
|
67
|
+
when 2
|
|
68
|
+
standard += '=='
|
|
69
|
+
when 3
|
|
70
|
+
standard += '='
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
base64_decode(standard)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ===== Unicode Support =====
|
|
77
|
+
|
|
78
|
+
# Encode a Unicode string to Base64
|
|
79
|
+
# @param str [String] Unicode string to encode
|
|
80
|
+
# @return [String] Base64 encoded string
|
|
81
|
+
def base64_encode_unicode(str)
|
|
82
|
+
# Convert to UTF-8 bytes, then encode
|
|
83
|
+
`btoa(unescape(encodeURIComponent(#{str})))`
|
|
84
|
+
rescue
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Decode a Base64 string to Unicode
|
|
89
|
+
# @param str [String] Base64 string to decode
|
|
90
|
+
# @return [String] Decoded Unicode string
|
|
91
|
+
def base64_decode_unicode(str)
|
|
92
|
+
`decodeURIComponent(escape(atob(#{str})))`
|
|
93
|
+
rescue
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ===== Binary Data (ArrayBuffer/Uint8Array) =====
|
|
98
|
+
|
|
99
|
+
# Encode an ArrayBuffer or Uint8Array to Base64
|
|
100
|
+
# @param buffer [Native] ArrayBuffer or Uint8Array
|
|
101
|
+
# @return [String] Base64 encoded string
|
|
102
|
+
def base64_encode_buffer(buffer)
|
|
103
|
+
`
|
|
104
|
+
var bytes = new Uint8Array(#{buffer});
|
|
105
|
+
var binary = '';
|
|
106
|
+
for (var i = 0; i < bytes.byteLength; i++) {
|
|
107
|
+
binary += String.fromCharCode(bytes[i]);
|
|
108
|
+
}
|
|
109
|
+
return btoa(binary);
|
|
110
|
+
`
|
|
111
|
+
rescue
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Decode a Base64 string to Uint8Array
|
|
116
|
+
# @param str [String] Base64 string to decode
|
|
117
|
+
# @return [Native] Uint8Array
|
|
118
|
+
def base64_decode_to_buffer(str)
|
|
119
|
+
`
|
|
120
|
+
var binary = atob(#{str});
|
|
121
|
+
var bytes = new Uint8Array(binary.length);
|
|
122
|
+
for (var i = 0; i < binary.length; i++) {
|
|
123
|
+
bytes[i] = binary.charCodeAt(i);
|
|
124
|
+
}
|
|
125
|
+
return bytes;
|
|
126
|
+
`
|
|
127
|
+
rescue
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ===== Data URL Support =====
|
|
132
|
+
|
|
133
|
+
# Create a data URL from content
|
|
134
|
+
# @param content [String] Content to encode
|
|
135
|
+
# @param mime_type [String] MIME type (default: "text/plain")
|
|
136
|
+
# @return [String] Data URL
|
|
137
|
+
def to_data_url(content, mime_type = 'text/plain')
|
|
138
|
+
encoded = base64_encode_unicode(content)
|
|
139
|
+
return nil unless encoded
|
|
140
|
+
|
|
141
|
+
"data:#{mime_type};base64,#{encoded}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Parse a data URL
|
|
145
|
+
# @param data_url [String] Data URL
|
|
146
|
+
# @return [Hash, nil] Hash with :mime_type and :data keys, or nil
|
|
147
|
+
def parse_data_url(data_url)
|
|
148
|
+
return nil unless data_url.to_s.start_with?('data:')
|
|
149
|
+
|
|
150
|
+
match = `#{data_url}.match(/^data:([^;]+);base64,(.+)$/)`
|
|
151
|
+
return nil if `#{match} === null`
|
|
152
|
+
|
|
153
|
+
{
|
|
154
|
+
mime_type: `#{match}[1]`,
|
|
155
|
+
data: base64_decode_unicode(`#{match}[2]`)
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ===== Authentication Helpers =====
|
|
160
|
+
|
|
161
|
+
# Create a Basic Auth header value
|
|
162
|
+
# @param username [String] Username
|
|
163
|
+
# @param password [String] Password
|
|
164
|
+
# @return [String] Basic auth header value
|
|
165
|
+
def basic_auth_header(username, password)
|
|
166
|
+
credentials = "#{username}:#{password}"
|
|
167
|
+
encoded = base64_encode(credentials)
|
|
168
|
+
"Basic #{encoded}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Parse a Basic Auth header value
|
|
172
|
+
# @param header [String] Authorization header value
|
|
173
|
+
# @return [Hash, nil] Hash with :username and :password keys, or nil
|
|
174
|
+
def parse_basic_auth(header)
|
|
175
|
+
return nil unless header.to_s.start_with?('Basic ')
|
|
176
|
+
|
|
177
|
+
encoded = header[6..-1]
|
|
178
|
+
decoded = base64_decode(encoded)
|
|
179
|
+
return nil unless decoded
|
|
180
|
+
|
|
181
|
+
parts = decoded.split(':', 2)
|
|
182
|
+
return nil if parts.length != 2
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
username: parts[0],
|
|
186
|
+
password: parts[1]
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# ===== JWT Helpers =====
|
|
191
|
+
|
|
192
|
+
# Decode a JWT payload (without verification)
|
|
193
|
+
# @param token [String] JWT token
|
|
194
|
+
# @return [Hash, nil] Decoded payload or nil
|
|
195
|
+
# @note This does NOT verify the signature - use only for reading claims
|
|
196
|
+
def decode_jwt_payload(token)
|
|
197
|
+
parts = token.to_s.split('.')
|
|
198
|
+
return nil if parts.length != 3
|
|
199
|
+
|
|
200
|
+
payload_base64 = parts[1]
|
|
201
|
+
payload_json = base64_decode_urlsafe(payload_base64)
|
|
202
|
+
return nil unless payload_json
|
|
203
|
+
|
|
204
|
+
`JSON.parse(#{payload_json})`
|
|
205
|
+
rescue
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Check if a JWT is expired
|
|
210
|
+
# @param token [String] JWT token
|
|
211
|
+
# @return [Boolean] True if expired
|
|
212
|
+
def jwt_expired?(token)
|
|
213
|
+
payload = decode_jwt_payload(token)
|
|
214
|
+
return true unless payload
|
|
215
|
+
|
|
216
|
+
exp = `#{payload}.exp`
|
|
217
|
+
return false if `#{exp} === undefined || #{exp} === null`
|
|
218
|
+
|
|
219
|
+
now = `Math.floor(Date.now() / 1000)`
|
|
220
|
+
`#{exp} < #{now}`
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Get JWT expiration time
|
|
224
|
+
# @param token [String] JWT token
|
|
225
|
+
# @return [Time, nil] Expiration time or nil
|
|
226
|
+
def jwt_expires_at(token)
|
|
227
|
+
payload = decode_jwt_payload(token)
|
|
228
|
+
return nil unless payload
|
|
229
|
+
|
|
230
|
+
exp = `#{payload}.exp`
|
|
231
|
+
return nil if `#{exp} === undefined || #{exp} === null`
|
|
232
|
+
|
|
233
|
+
# Convert Unix timestamp to Ruby-like time representation
|
|
234
|
+
`new Date(#{exp} * 1000)`
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# ===== Utility Methods =====
|
|
238
|
+
|
|
239
|
+
# Check if a string is valid Base64
|
|
240
|
+
# @param str [String] String to check
|
|
241
|
+
# @return [Boolean] True if valid Base64
|
|
242
|
+
def valid_base64?(str)
|
|
243
|
+
return false if str.nil? || str.empty?
|
|
244
|
+
|
|
245
|
+
# Check for valid Base64 characters
|
|
246
|
+
`
|
|
247
|
+
var regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
248
|
+
if (!regex.test(#{str})) return false;
|
|
249
|
+
if (#{str}.length % 4 !== 0) return false;
|
|
250
|
+
try {
|
|
251
|
+
atob(#{str});
|
|
252
|
+
return true;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
`
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Get the decoded length of a Base64 string
|
|
260
|
+
# @param str [String] Base64 string
|
|
261
|
+
# @return [Integer] Decoded length in bytes
|
|
262
|
+
def base64_decoded_length(str)
|
|
263
|
+
return 0 if str.nil? || str.empty?
|
|
264
|
+
|
|
265
|
+
len = str.length
|
|
266
|
+
padding = str.end_with?('==') ? 2 : (str.end_with?('=') ? 1 : 0)
|
|
267
|
+
(len * 3 / 4) - padding
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Alias for convenience
|
|
275
|
+
Base64Helpers = OpalVite::Concerns::V1::Base64Helpers
|
|
@@ -1645,6 +1645,300 @@ module OpalVite
|
|
|
1645
1645
|
`this.element.contains(#{el})`
|
|
1646
1646
|
end
|
|
1647
1647
|
|
|
1648
|
+
# ===== Debounce/Throttle Utilities =====
|
|
1649
|
+
|
|
1650
|
+
# Create a debounced version of a block
|
|
1651
|
+
# @param wait [Integer] Milliseconds to wait
|
|
1652
|
+
# @yield Block to debounce
|
|
1653
|
+
# @return [Native] Debounced function
|
|
1654
|
+
def debounce(wait, &block)
|
|
1655
|
+
`
|
|
1656
|
+
var timeout;
|
|
1657
|
+
return function(...args) {
|
|
1658
|
+
clearTimeout(timeout);
|
|
1659
|
+
timeout = setTimeout(() => #{block.call}, wait);
|
|
1660
|
+
};
|
|
1661
|
+
`
|
|
1662
|
+
end
|
|
1663
|
+
|
|
1664
|
+
# Create a throttled version of a block
|
|
1665
|
+
# @param wait [Integer] Milliseconds between calls
|
|
1666
|
+
# @yield Block to throttle
|
|
1667
|
+
# @return [Native] Throttled function
|
|
1668
|
+
def throttle(wait, &block)
|
|
1669
|
+
`
|
|
1670
|
+
var lastTime = 0;
|
|
1671
|
+
return function(...args) {
|
|
1672
|
+
var now = Date.now();
|
|
1673
|
+
if (now - lastTime >= #{wait}) {
|
|
1674
|
+
lastTime = now;
|
|
1675
|
+
#{block.call};
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
`
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
# Debounce a method call
|
|
1682
|
+
# @param wait [Integer] Milliseconds to wait
|
|
1683
|
+
# @param key [String] Unique key for this debounce
|
|
1684
|
+
# @yield Block to execute after debounce
|
|
1685
|
+
def debounced(wait, key = 'default', &block)
|
|
1686
|
+
@_debounce_timers ||= {}
|
|
1687
|
+
timer_key = "_debounce_#{key}"
|
|
1688
|
+
|
|
1689
|
+
`clearTimeout(#{@_debounce_timers[timer_key]})`
|
|
1690
|
+
@_debounce_timers[timer_key] = `setTimeout(function() { #{block.call} }, #{wait})`
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
# Throttle a method call
|
|
1694
|
+
# @param wait [Integer] Milliseconds between calls
|
|
1695
|
+
# @param key [String] Unique key for this throttle
|
|
1696
|
+
# @yield Block to execute if not throttled
|
|
1697
|
+
def throttled(wait, key = 'default', &block)
|
|
1698
|
+
@_throttle_times ||= {}
|
|
1699
|
+
time_key = "_throttle_#{key}"
|
|
1700
|
+
|
|
1701
|
+
now = `Date.now()`
|
|
1702
|
+
last_time = @_throttle_times[time_key] || 0
|
|
1703
|
+
|
|
1704
|
+
if `#{now} - #{last_time} >= #{wait}`
|
|
1705
|
+
@_throttle_times[time_key] = now
|
|
1706
|
+
block.call
|
|
1707
|
+
end
|
|
1708
|
+
end
|
|
1709
|
+
|
|
1710
|
+
# ===== Clipboard Utilities =====
|
|
1711
|
+
|
|
1712
|
+
# Copy text to clipboard
|
|
1713
|
+
# @param text [String] Text to copy
|
|
1714
|
+
# @yield Optional callback on success
|
|
1715
|
+
def copy_to_clipboard(text, &on_success)
|
|
1716
|
+
`navigator.clipboard.writeText(#{text}).then(function() {
|
|
1717
|
+
#{on_success.call if on_success}
|
|
1718
|
+
})`
|
|
1719
|
+
end
|
|
1720
|
+
|
|
1721
|
+
# Read text from clipboard
|
|
1722
|
+
# @yield Block receiving clipboard text
|
|
1723
|
+
def read_from_clipboard(&block)
|
|
1724
|
+
`navigator.clipboard.readText().then(function(text) {
|
|
1725
|
+
#{block.call(`text`)}
|
|
1726
|
+
})`
|
|
1727
|
+
end
|
|
1728
|
+
|
|
1729
|
+
# ===== Object Utilities =====
|
|
1730
|
+
|
|
1731
|
+
# Deep clone an object
|
|
1732
|
+
# @param obj [Object] Object to clone
|
|
1733
|
+
# @return [Object] Cloned object
|
|
1734
|
+
def deep_clone(obj)
|
|
1735
|
+
`JSON.parse(JSON.stringify(#{obj.to_n}))`
|
|
1736
|
+
end
|
|
1737
|
+
|
|
1738
|
+
# Deep merge two objects
|
|
1739
|
+
# @param target [Hash] Target object
|
|
1740
|
+
# @param source [Hash] Source object
|
|
1741
|
+
# @return [Hash] Merged object
|
|
1742
|
+
def deep_merge(target, source)
|
|
1743
|
+
`
|
|
1744
|
+
function deepMerge(target, source) {
|
|
1745
|
+
var result = Object.assign({}, target);
|
|
1746
|
+
for (var key in source) {
|
|
1747
|
+
if (source.hasOwnProperty(key)) {
|
|
1748
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
1749
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
1750
|
+
} else {
|
|
1751
|
+
result[key] = source[key];
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return result;
|
|
1756
|
+
}
|
|
1757
|
+
return deepMerge(#{target.to_n}, #{source.to_n});
|
|
1758
|
+
`
|
|
1759
|
+
end
|
|
1760
|
+
|
|
1761
|
+
# Pick specific keys from an object
|
|
1762
|
+
# @param obj [Hash] Source object
|
|
1763
|
+
# @param keys [Array<String, Symbol>] Keys to pick
|
|
1764
|
+
# @return [Hash] Object with only specified keys
|
|
1765
|
+
def pick(obj, *keys)
|
|
1766
|
+
result = {}
|
|
1767
|
+
keys.flatten.each do |key|
|
|
1768
|
+
key_s = key.to_s
|
|
1769
|
+
native_obj = obj.to_n
|
|
1770
|
+
if `#{native_obj}.hasOwnProperty(#{key_s})`
|
|
1771
|
+
result[key_s] = `#{native_obj}[#{key_s}]`
|
|
1772
|
+
end
|
|
1773
|
+
end
|
|
1774
|
+
result
|
|
1775
|
+
end
|
|
1776
|
+
|
|
1777
|
+
# Omit specific keys from an object
|
|
1778
|
+
# @param obj [Hash] Source object
|
|
1779
|
+
# @param keys [Array<String, Symbol>] Keys to omit
|
|
1780
|
+
# @return [Hash] Object without specified keys
|
|
1781
|
+
def omit(obj, *keys)
|
|
1782
|
+
keys_to_omit = keys.flatten.map(&:to_s)
|
|
1783
|
+
result = {}
|
|
1784
|
+
native_obj = obj.to_n
|
|
1785
|
+
`Object.keys(#{native_obj}).forEach(function(key) {
|
|
1786
|
+
if (!#{keys_to_omit}.includes(key)) {
|
|
1787
|
+
#{result[`key`] = `#{native_obj}[key]`}
|
|
1788
|
+
}
|
|
1789
|
+
})`
|
|
1790
|
+
result
|
|
1791
|
+
end
|
|
1792
|
+
|
|
1793
|
+
# ===== Set Utilities =====
|
|
1794
|
+
|
|
1795
|
+
# Create a unique array (like Set)
|
|
1796
|
+
# @param arr [Array] Array with possible duplicates
|
|
1797
|
+
# @return [Array] Array with unique values
|
|
1798
|
+
def unique(arr)
|
|
1799
|
+
`[...new Set(#{arr.to_n})]`
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1802
|
+
# Get intersection of two arrays
|
|
1803
|
+
# @param arr1 [Array] First array
|
|
1804
|
+
# @param arr2 [Array] Second array
|
|
1805
|
+
# @return [Array] Intersection
|
|
1806
|
+
def intersection(arr1, arr2)
|
|
1807
|
+
set2 = `new Set(#{arr2.to_n})`
|
|
1808
|
+
`#{arr1.to_n}.filter(x => #{set2}.has(x))`
|
|
1809
|
+
end
|
|
1810
|
+
|
|
1811
|
+
# Get difference of two arrays (arr1 - arr2)
|
|
1812
|
+
# @param arr1 [Array] First array
|
|
1813
|
+
# @param arr2 [Array] Second array
|
|
1814
|
+
# @return [Array] Difference
|
|
1815
|
+
def difference(arr1, arr2)
|
|
1816
|
+
set2 = `new Set(#{arr2.to_n})`
|
|
1817
|
+
`#{arr1.to_n}.filter(x => !#{set2}.has(x))`
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
# Get union of two arrays
|
|
1821
|
+
# @param arr1 [Array] First array
|
|
1822
|
+
# @param arr2 [Array] Second array
|
|
1823
|
+
# @return [Array] Union (unique values)
|
|
1824
|
+
def union(arr1, arr2)
|
|
1825
|
+
`[...new Set([...#{arr1.to_n}, ...#{arr2.to_n}])]`
|
|
1826
|
+
end
|
|
1827
|
+
|
|
1828
|
+
# ===== Validation Utilities =====
|
|
1829
|
+
|
|
1830
|
+
# Validate an email address
|
|
1831
|
+
# @param email [String] Email to validate
|
|
1832
|
+
# @return [Boolean] True if valid
|
|
1833
|
+
def valid_email?(email)
|
|
1834
|
+
`/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(#{email})`
|
|
1835
|
+
end
|
|
1836
|
+
|
|
1837
|
+
# Validate a URL
|
|
1838
|
+
# @param url [String] URL to validate
|
|
1839
|
+
# @return [Boolean] True if valid
|
|
1840
|
+
def valid_url?(url)
|
|
1841
|
+
`
|
|
1842
|
+
try {
|
|
1843
|
+
new URL(#{url});
|
|
1844
|
+
return true;
|
|
1845
|
+
} catch (e) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
`
|
|
1849
|
+
end
|
|
1850
|
+
|
|
1851
|
+
# Validate a phone number (basic)
|
|
1852
|
+
# @param phone [String] Phone number to validate
|
|
1853
|
+
# @return [Boolean] True if valid
|
|
1854
|
+
def valid_phone?(phone)
|
|
1855
|
+
# Remove common separators and check for digits
|
|
1856
|
+
cleaned = `#{phone}.replace(/[\s\-\(\)\.]/g, '')`
|
|
1857
|
+
`
|
|
1858
|
+
var pattern = /^[\+]?[0-9]{10,15}$/;
|
|
1859
|
+
return pattern.test(#{cleaned});
|
|
1860
|
+
`
|
|
1861
|
+
end
|
|
1862
|
+
|
|
1863
|
+
# Check if a value is blank (nil, empty string, or whitespace only)
|
|
1864
|
+
# @param value [Object] Value to check
|
|
1865
|
+
# @return [Boolean] True if blank
|
|
1866
|
+
def blank?(value)
|
|
1867
|
+
return true if value.nil?
|
|
1868
|
+
return `#{value}.trim() === ''` if `typeof #{value} === 'string'`
|
|
1869
|
+
return `#{value}.length === 0` if `Array.isArray(#{value})`
|
|
1870
|
+
false
|
|
1871
|
+
end
|
|
1872
|
+
|
|
1873
|
+
# Check if a value is present (not blank)
|
|
1874
|
+
# @param value [Object] Value to check
|
|
1875
|
+
# @return [Boolean] True if present
|
|
1876
|
+
def present?(value)
|
|
1877
|
+
!blank?(value)
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1880
|
+
# Validate minimum length
|
|
1881
|
+
# @param value [String] Value to check
|
|
1882
|
+
# @param min [Integer] Minimum length
|
|
1883
|
+
# @return [Boolean] True if valid
|
|
1884
|
+
def min_length?(value, min)
|
|
1885
|
+
return false if value.nil?
|
|
1886
|
+
`#{value}.length >= #{min}`
|
|
1887
|
+
end
|
|
1888
|
+
|
|
1889
|
+
# Validate maximum length
|
|
1890
|
+
# @param value [String] Value to check
|
|
1891
|
+
# @param max [Integer] Maximum length
|
|
1892
|
+
# @return [Boolean] True if valid
|
|
1893
|
+
def max_length?(value, max)
|
|
1894
|
+
return false if value.nil?
|
|
1895
|
+
`#{value}.length <= #{max}`
|
|
1896
|
+
end
|
|
1897
|
+
|
|
1898
|
+
# Validate a value matches a pattern
|
|
1899
|
+
# @param value [String] Value to check
|
|
1900
|
+
# @param pattern [String] Regular expression pattern
|
|
1901
|
+
# @return [Boolean] True if matches
|
|
1902
|
+
def matches_pattern?(value, pattern)
|
|
1903
|
+
return false if value.nil?
|
|
1904
|
+
`new RegExp(#{pattern}).test(#{value})`
|
|
1905
|
+
end
|
|
1906
|
+
|
|
1907
|
+
# ===== Console Helpers =====
|
|
1908
|
+
|
|
1909
|
+
# Log a styled message to console
|
|
1910
|
+
# @param message [String] Message to log
|
|
1911
|
+
# @param style [String] CSS style string
|
|
1912
|
+
def console_styled(message, style = 'color: blue; font-weight: bold;')
|
|
1913
|
+
`console.log('%c' + #{message}, #{style})`
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
# Log a grouped set of messages
|
|
1917
|
+
# @param label [String] Group label
|
|
1918
|
+
# @yield Block that logs messages
|
|
1919
|
+
def console_group(label, collapsed: false, &block)
|
|
1920
|
+
if collapsed
|
|
1921
|
+
`console.groupCollapsed(#{label})`
|
|
1922
|
+
else
|
|
1923
|
+
`console.group(#{label})`
|
|
1924
|
+
end
|
|
1925
|
+
block.call
|
|
1926
|
+
`console.groupEnd()`
|
|
1927
|
+
end
|
|
1928
|
+
|
|
1929
|
+
# Log a table
|
|
1930
|
+
# @param data [Array, Hash] Data to display as table
|
|
1931
|
+
def console_table(data)
|
|
1932
|
+
`console.table(#{data.to_n})`
|
|
1933
|
+
end
|
|
1934
|
+
|
|
1935
|
+
# Log with timestamp
|
|
1936
|
+
# @param message [String] Message to log
|
|
1937
|
+
def console_time(message)
|
|
1938
|
+
timestamp = `new Date().toISOString()`
|
|
1939
|
+
`console.log('[' + #{timestamp} + '] ' + #{message})`
|
|
1940
|
+
end
|
|
1941
|
+
|
|
1648
1942
|
private
|
|
1649
1943
|
|
|
1650
1944
|
# Convert snake_case to camelCase, preserving existing camelCase
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
|
|
3
|
+
module OpalVite
|
|
4
|
+
module Concerns
|
|
5
|
+
module V1
|
|
6
|
+
# URIHelpers - provides URL parsing and manipulation utilities
|
|
7
|
+
#
|
|
8
|
+
# This module wraps JavaScript's URL and URLSearchParams APIs
|
|
9
|
+
# to provide Ruby-friendly methods for URL operations.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# class MyController < StimulusController
|
|
13
|
+
# include OpalVite::Concerns::V1::URIHelpers
|
|
14
|
+
#
|
|
15
|
+
# def connect
|
|
16
|
+
# url = parse_url("https://example.com/path?foo=bar")
|
|
17
|
+
# puts url_hostname(url) # => "example.com"
|
|
18
|
+
# puts url_param(url, "foo") # => "bar"
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
module URIHelpers
|
|
23
|
+
# ===== URL Parsing =====
|
|
24
|
+
|
|
25
|
+
# Parse a URL string into a URL object
|
|
26
|
+
# @param url_string [String] URL string to parse
|
|
27
|
+
# @return [Native] JavaScript URL object
|
|
28
|
+
def parse_url(url_string)
|
|
29
|
+
`new URL(#{url_string})`
|
|
30
|
+
rescue
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Parse a URL with a base URL
|
|
35
|
+
# @param url_string [String] Relative or absolute URL
|
|
36
|
+
# @param base [String] Base URL
|
|
37
|
+
# @return [Native] JavaScript URL object
|
|
38
|
+
def parse_url_with_base(url_string, base)
|
|
39
|
+
`new URL(#{url_string}, #{base})`
|
|
40
|
+
rescue
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get the current page URL
|
|
45
|
+
# @return [Native] JavaScript URL object
|
|
46
|
+
def current_url
|
|
47
|
+
`new URL(window.location.href)`
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ===== URL Components =====
|
|
51
|
+
|
|
52
|
+
# Get the protocol (scheme) of a URL
|
|
53
|
+
# @param url [Native] URL object
|
|
54
|
+
# @return [String] Protocol (e.g., "https:")
|
|
55
|
+
def url_protocol(url)
|
|
56
|
+
`#{url}.protocol`
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the hostname of a URL
|
|
60
|
+
# @param url [Native] URL object
|
|
61
|
+
# @return [String] Hostname (e.g., "example.com")
|
|
62
|
+
def url_hostname(url)
|
|
63
|
+
`#{url}.hostname`
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get the host (hostname + port) of a URL
|
|
67
|
+
# @param url [Native] URL object
|
|
68
|
+
# @return [String] Host (e.g., "example.com:8080")
|
|
69
|
+
def url_host(url)
|
|
70
|
+
`#{url}.host`
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get the port of a URL
|
|
74
|
+
# @param url [Native] URL object
|
|
75
|
+
# @return [String] Port number or empty string
|
|
76
|
+
def url_port(url)
|
|
77
|
+
`#{url}.port`
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get the pathname of a URL
|
|
81
|
+
# @param url [Native] URL object
|
|
82
|
+
# @return [String] Pathname (e.g., "/path/to/page")
|
|
83
|
+
def url_pathname(url)
|
|
84
|
+
`#{url}.pathname`
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get the search string (query string with ?)
|
|
88
|
+
# @param url [Native] URL object
|
|
89
|
+
# @return [String] Search string (e.g., "?foo=bar")
|
|
90
|
+
def url_search(url)
|
|
91
|
+
`#{url}.search`
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get the hash (fragment) of a URL
|
|
95
|
+
# @param url [Native] URL object
|
|
96
|
+
# @return [String] Hash (e.g., "#section")
|
|
97
|
+
def url_hash(url)
|
|
98
|
+
`#{url}.hash`
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get the origin of a URL
|
|
102
|
+
# @param url [Native] URL object
|
|
103
|
+
# @return [String] Origin (e.g., "https://example.com")
|
|
104
|
+
def url_origin(url)
|
|
105
|
+
`#{url}.origin`
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get the full URL as a string
|
|
109
|
+
# @param url [Native] URL object
|
|
110
|
+
# @return [String] Full URL string
|
|
111
|
+
def url_to_string(url)
|
|
112
|
+
`#{url}.href`
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ===== Query Parameters =====
|
|
116
|
+
|
|
117
|
+
# Get a query parameter value
|
|
118
|
+
# @param url [Native] URL object
|
|
119
|
+
# @param name [String] Parameter name
|
|
120
|
+
# @return [String, nil] Parameter value or nil
|
|
121
|
+
def url_param(url, name)
|
|
122
|
+
result = `#{url}.searchParams.get(#{name})`
|
|
123
|
+
`#{result} === null` ? nil : result
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get all values for a query parameter
|
|
127
|
+
# @param url [Native] URL object
|
|
128
|
+
# @param name [String] Parameter name
|
|
129
|
+
# @return [Array<String>] Array of values
|
|
130
|
+
def url_params(url, name)
|
|
131
|
+
`Array.from(#{url}.searchParams.getAll(#{name}))`
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Check if a query parameter exists
|
|
135
|
+
# @param url [Native] URL object
|
|
136
|
+
# @param name [String] Parameter name
|
|
137
|
+
# @return [Boolean] True if parameter exists
|
|
138
|
+
def url_has_param?(url, name)
|
|
139
|
+
`#{url}.searchParams.has(#{name})`
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get all query parameters as a Hash
|
|
143
|
+
# @param url [Native] URL object
|
|
144
|
+
# @return [Hash] Hash of parameter names to values
|
|
145
|
+
def url_all_params(url)
|
|
146
|
+
result = {}
|
|
147
|
+
`#{url}.searchParams.forEach((value, key) => {
|
|
148
|
+
#{result[`key`] = `value`}
|
|
149
|
+
})`
|
|
150
|
+
result
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Set a query parameter (mutates the URL object)
|
|
154
|
+
# @param url [Native] URL object
|
|
155
|
+
# @param name [String] Parameter name
|
|
156
|
+
# @param value [String] Parameter value
|
|
157
|
+
def url_set_param(url, name, value)
|
|
158
|
+
`#{url}.searchParams.set(#{name}, #{value})`
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Append a query parameter (allows duplicates)
|
|
162
|
+
# @param url [Native] URL object
|
|
163
|
+
# @param name [String] Parameter name
|
|
164
|
+
# @param value [String] Parameter value
|
|
165
|
+
def url_append_param(url, name, value)
|
|
166
|
+
`#{url}.searchParams.append(#{name}, #{value})`
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Delete a query parameter
|
|
170
|
+
# @param url [Native] URL object
|
|
171
|
+
# @param name [String] Parameter name
|
|
172
|
+
def url_delete_param(url, name)
|
|
173
|
+
`#{url}.searchParams.delete(#{name})`
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# ===== URL Building =====
|
|
177
|
+
|
|
178
|
+
# Build a URL from components
|
|
179
|
+
# @param options [Hash] URL components
|
|
180
|
+
# @option options [String] :protocol Protocol (default: "https:")
|
|
181
|
+
# @option options [String] :hostname Hostname (required)
|
|
182
|
+
# @option options [String] :port Port number
|
|
183
|
+
# @option options [String] :pathname Path
|
|
184
|
+
# @option options [Hash] :params Query parameters
|
|
185
|
+
# @option options [String] :hash Fragment
|
|
186
|
+
# @return [String] Built URL string
|
|
187
|
+
def build_url(options = {})
|
|
188
|
+
protocol = options[:protocol] || 'https:'
|
|
189
|
+
hostname = options[:hostname] || 'localhost'
|
|
190
|
+
port = options[:port]
|
|
191
|
+
pathname = options[:pathname] || '/'
|
|
192
|
+
params = options[:params] || {}
|
|
193
|
+
hash = options[:hash]
|
|
194
|
+
|
|
195
|
+
# Build base URL
|
|
196
|
+
base = "#{protocol}//#{hostname}"
|
|
197
|
+
base += ":#{port}" if port && !port.to_s.empty?
|
|
198
|
+
base += pathname
|
|
199
|
+
|
|
200
|
+
url = parse_url(base)
|
|
201
|
+
return nil unless url
|
|
202
|
+
|
|
203
|
+
# Add query parameters
|
|
204
|
+
params.each do |key, value|
|
|
205
|
+
url_set_param(url, key.to_s, value.to_s)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Add hash
|
|
209
|
+
`#{url}.hash = #{hash}` if hash
|
|
210
|
+
|
|
211
|
+
url_to_string(url)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# ===== URL Encoding =====
|
|
215
|
+
|
|
216
|
+
# Encode a URI component
|
|
217
|
+
# @param str [String] String to encode
|
|
218
|
+
# @return [String] Encoded string
|
|
219
|
+
def encode_uri_component(str)
|
|
220
|
+
`encodeURIComponent(#{str})`
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Decode a URI component
|
|
224
|
+
# @param str [String] String to decode
|
|
225
|
+
# @return [String] Decoded string
|
|
226
|
+
def decode_uri_component(str)
|
|
227
|
+
`decodeURIComponent(#{str})`
|
|
228
|
+
rescue
|
|
229
|
+
str
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Encode a full URI
|
|
233
|
+
# @param str [String] URI to encode
|
|
234
|
+
# @return [String] Encoded URI
|
|
235
|
+
def encode_uri(str)
|
|
236
|
+
`encodeURI(#{str})`
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Decode a full URI
|
|
240
|
+
# @param str [String] URI to decode
|
|
241
|
+
# @return [String] Decoded URI
|
|
242
|
+
def decode_uri(str)
|
|
243
|
+
`decodeURI(#{str})`
|
|
244
|
+
rescue
|
|
245
|
+
str
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# ===== Query String Utilities =====
|
|
249
|
+
|
|
250
|
+
# Parse a query string into a Hash
|
|
251
|
+
# @param query_string [String] Query string (with or without leading ?)
|
|
252
|
+
# @return [Hash] Parsed parameters
|
|
253
|
+
def parse_query_string(query_string)
|
|
254
|
+
# Remove leading ? if present
|
|
255
|
+
qs = query_string.to_s
|
|
256
|
+
qs = qs[1..-1] if qs.start_with?('?')
|
|
257
|
+
|
|
258
|
+
result = {}
|
|
259
|
+
params = `new URLSearchParams(#{qs})`
|
|
260
|
+
`#{params}.forEach((value, key) => {
|
|
261
|
+
#{result[`key`] = `value`}
|
|
262
|
+
})`
|
|
263
|
+
result
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Build a query string from a Hash
|
|
267
|
+
# @param params [Hash] Parameters
|
|
268
|
+
# @return [String] Query string (without leading ?)
|
|
269
|
+
def build_query_string(params)
|
|
270
|
+
search_params = `new URLSearchParams()`
|
|
271
|
+
params.each do |key, value|
|
|
272
|
+
`#{search_params}.append(#{key.to_s}, #{value.to_s})`
|
|
273
|
+
end
|
|
274
|
+
`#{search_params}.toString()`
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# ===== Path Utilities =====
|
|
278
|
+
|
|
279
|
+
# Join path segments
|
|
280
|
+
# @param segments [Array<String>] Path segments
|
|
281
|
+
# @return [String] Joined path
|
|
282
|
+
def join_path(*segments)
|
|
283
|
+
segments.map { |s| s.to_s.gsub(%r{^/|/$}, '') }.reject(&:empty?).join('/')
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Get the filename from a path
|
|
287
|
+
# @param path [String] Path
|
|
288
|
+
# @return [String] Filename
|
|
289
|
+
def path_basename(path)
|
|
290
|
+
path.to_s.split('/').last || ''
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Get the directory from a path
|
|
294
|
+
# @param path [String] Path
|
|
295
|
+
# @return [String] Directory path
|
|
296
|
+
def path_dirname(path)
|
|
297
|
+
parts = path.to_s.split('/')
|
|
298
|
+
parts.pop
|
|
299
|
+
parts.join('/') || '/'
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Get the file extension from a path
|
|
303
|
+
# @param path [String] Path
|
|
304
|
+
# @return [String] Extension (with dot) or empty string
|
|
305
|
+
def path_extname(path)
|
|
306
|
+
basename = path_basename(path)
|
|
307
|
+
idx = basename.rindex('.')
|
|
308
|
+
idx ? basename[idx..-1] : ''
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Alias for convenience
|
|
316
|
+
URIHelpers = OpalVite::Concerns::V1::URIHelpers
|
|
@@ -6,3 +6,5 @@ require 'opal_vite/concerns/v1/storable'
|
|
|
6
6
|
require 'opal_vite/concerns/v1/stimulus_helpers'
|
|
7
7
|
require 'opal_vite/concerns/v1/vue_helpers'
|
|
8
8
|
require 'opal_vite/concerns/v1/react_helpers'
|
|
9
|
+
require 'opal_vite/concerns/v1/uri_helpers'
|
|
10
|
+
require 'opal_vite/concerns/v1/base64_helpers'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opal-vite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- stofu1234
|
|
@@ -120,12 +120,14 @@ files:
|
|
|
120
120
|
- opal/opal_vite/concerns/storable.rb
|
|
121
121
|
- opal/opal_vite/concerns/toastable.rb
|
|
122
122
|
- opal/opal_vite/concerns/v1.rb
|
|
123
|
+
- opal/opal_vite/concerns/v1/base64_helpers.rb
|
|
123
124
|
- opal/opal_vite/concerns/v1/dom_helpers.rb
|
|
124
125
|
- opal/opal_vite/concerns/v1/js_proxy_ex.rb
|
|
125
126
|
- opal/opal_vite/concerns/v1/react_helpers.rb
|
|
126
127
|
- opal/opal_vite/concerns/v1/stimulus_helpers.rb
|
|
127
128
|
- opal/opal_vite/concerns/v1/storable.rb
|
|
128
129
|
- opal/opal_vite/concerns/v1/toastable.rb
|
|
130
|
+
- opal/opal_vite/concerns/v1/uri_helpers.rb
|
|
129
131
|
- opal/opal_vite/concerns/v1/vue_helpers.rb
|
|
130
132
|
- opal/opal_vite/concerns/vue_helpers.rb
|
|
131
133
|
homepage: https://stofu1234.github.io/opal-vite/
|