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: 5274b86604ed917278cf1739cb4663c1d93246cd6bf82876876fea56c239c6c2
4
- data.tar.gz: 72291ac54ac2770b38b7ade5a8907751c9845926efbc54ff479b6ac8623d485e
3
+ metadata.gz: 8736c0cccd990adbc7f614480718ba0bab767f94ad281f508b0a01b9898deb3d
4
+ data.tar.gz: 93ab376b58edb586aed577d0d4a19011f7f84660c62eebffe418eccb4b9b1e73
5
5
  SHA512:
6
- metadata.gz: 1b134a748ab8322e8889dcd64e687632581ae5da768c5a5b26e0e2e52c032c309e0b39ebd9e0dd2a0645bdaf9ae53e60ed0450985611b220970a9a1ef802d682
7
- data.tar.gz: a309c63ca72694695797d1d5ad0edef41427664a2fd5e4d827776b42bd4a4e02c940f772f6aab2afb5b857fa1b49f31ee749ddd674893ca7207566035ea7eeb1
6
+ metadata.gz: 1c0d8a02384a2e1f958d2a0ce62f66fe8f63db8c87cbe709089e04ca9441e2d37a9a41b860170cdb2ca8f24467a79ce12b2587d1312e0d59dd22e75020429911
7
+ data.tar.gz: 7aae2b96e735993cfd583cdc98ea0ad6bbd49110ef63209b17f6f49e614bf7edc0d1948612f40522ed9aa6e1839fa6e02731a7f6764ef269b3b61c17cee5c2b3
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.3.0"
3
+ VERSION = "0.3.1"
4
4
  end
5
5
  end
@@ -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.0
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/