tina4ruby 3.13.93 → 3.13.96

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +883 -0
  3. data/README.md +1 -1
  4. data/lib/tina4/auth.rb +166 -87
  5. data/lib/tina4/auto_crud.rb +29 -32
  6. data/lib/tina4/cache_backends/base_backend.rb +19 -0
  7. data/lib/tina4/cache_backends/database_backend.rb +29 -0
  8. data/lib/tina4/cache_backends/memcached_backend.rb +124 -13
  9. data/lib/tina4/cache_backends/memory_backend.rb +15 -0
  10. data/lib/tina4/cache_backends/redis_backend.rb +173 -52
  11. data/lib/tina4/cache_backends.rb +10 -1
  12. data/lib/tina4/cli.rb +35 -43
  13. data/lib/tina4/cors.rb +186 -30
  14. data/lib/tina4/database/sqlite3_adapter.rb +4 -1
  15. data/lib/tina4/database.rb +458 -48
  16. data/lib/tina4/database_adapter.rb +178 -0
  17. data/lib/tina4/database_result.rb +63 -17
  18. data/lib/tina4/database_url.rb +363 -0
  19. data/lib/tina4/dev.rb +0 -1
  20. data/lib/tina4/dev_admin.rb +118 -20
  21. data/lib/tina4/dev_mailbox.rb +5 -1
  22. data/lib/tina4/dispatch_pipeline.rb +605 -0
  23. data/lib/tina4/docstore.rb +274 -60
  24. data/lib/tina4/drivers/firebird_driver.rb +118 -4
  25. data/lib/tina4/drivers/mongodb_driver.rb +19 -4
  26. data/lib/tina4/drivers/mssql_driver.rb +73 -10
  27. data/lib/tina4/drivers/mysql_driver.rb +71 -4
  28. data/lib/tina4/drivers/odbc_driver.rb +40 -4
  29. data/lib/tina4/drivers/postgres_driver.rb +97 -10
  30. data/lib/tina4/drivers/sqlite_driver.rb +25 -3
  31. data/lib/tina4/env.rb +176 -34
  32. data/lib/tina4/field_types.rb +12 -0
  33. data/lib/tina4/frond.rb +102 -10
  34. data/lib/tina4/health.rb +30 -14
  35. data/lib/tina4/job.rb +15 -5
  36. data/lib/tina4/log.rb +236 -32
  37. data/lib/tina4/mcp.rb +11 -5
  38. data/lib/tina4/messenger.rb +317 -82
  39. data/lib/tina4/metrics.rb +179 -891
  40. data/lib/tina4/middleware.rb +191 -56
  41. data/lib/tina4/migration.rb +17 -1
  42. data/lib/tina4/orm.rb +114 -17
  43. data/lib/tina4/public/css/tina4.min.css +1 -1
  44. data/lib/tina4/queue.rb +154 -9
  45. data/lib/tina4/queue_backends/kafka_backend.rb +191 -2
  46. data/lib/tina4/queue_backends/lite_backend.rb +121 -25
  47. data/lib/tina4/queue_backends/mongo_backend.rb +146 -10
  48. data/lib/tina4/queue_backends/rabbitmq_backend.rb +194 -1
  49. data/lib/tina4/rack_app.rb +94 -316
  50. data/lib/tina4/request.rb +48 -8
  51. data/lib/tina4/response.rb +42 -1
  52. data/lib/tina4/response_cache.rb +142 -24
  53. data/lib/tina4/router.rb +141 -12
  54. data/lib/tina4/session.rb +243 -29
  55. data/lib/tina4/session_handlers/database_handler.rb +185 -20
  56. data/lib/tina4/session_handlers/file_handler.rb +113 -21
  57. data/lib/tina4/session_handlers/memcached_handler.rb +183 -0
  58. data/lib/tina4/session_handlers/mongo_handler.rb +232 -15
  59. data/lib/tina4/session_handlers/mongo_wire_client.rb +300 -0
  60. data/lib/tina4/session_handlers/redis_handler.rb +20 -6
  61. data/lib/tina4/session_handlers/valkey_handler.rb +18 -4
  62. data/lib/tina4/shutdown.rb +180 -30
  63. data/lib/tina4/sql_translator.rb +110 -0
  64. data/lib/tina4/swagger.rb +50 -18
  65. data/lib/tina4/version.rb +1 -1
  66. data/lib/tina4/webserver.rb +28 -6
  67. data/lib/tina4.rb +301 -38
  68. metadata +35 -17
  69. data/lib/tina4/scss_compiler.rb +0 -349
@@ -1,349 +0,0 @@
1
- # frozen_string_literal: true
2
- require "fileutils"
3
-
4
- module Tina4
5
- module ScssCompiler
6
- SCSS_DIRS = %w[src/scss scss src/styles styles].freeze
7
- CSS_OUTPUT = "src/public/css"
8
-
9
- # Flags that may trail a variable declaration's value. `!default` means
10
- # "assign only if this variable is not already set" -- the flag that makes a
11
- # variable themeable. `!global` is the scope flag. Both are compiler
12
- # directives: they are consumed at the declaration and must never reach the
13
- # CSS, because `padding: 1.5rem !default` is invalid CSS and browsers drop
14
- # the whole declaration. Sass flag names are case-SENSITIVE (`!DEFAULT` is an
15
- # error in Dart Sass), so the match is deliberately case-sensitive.
16
- VARIABLE_FLAG = /\s*!(default|global)\s*\z/
17
-
18
- # Module-level state for import paths and variables
19
- @import_paths = []
20
- @variables = {}
21
-
22
- class << self
23
- # Add a search path for @import resolution.
24
- def add_import_path(path)
25
- @import_paths ||= []
26
- @import_paths << path
27
- end
28
-
29
- # Set a variable that will be available during compilation.
30
- def set_variable(name, value)
31
- @variables ||= {}
32
- name = name.sub(/^\$/, "")
33
- @variables[name] = value
34
- end
35
-
36
- # Compile an SCSS string to CSS.
37
- def compile(source)
38
- @variables ||= {}
39
- @import_paths ||= []
40
- # Inject preset variables
41
- unless @variables.empty?
42
- var_block = @variables.map { |k, v| "$#{k}: #{v};" }.join("\n")
43
- source = "#{var_block}\n#{source}"
44
- end
45
- basic_compile(source, @import_paths.first || Dir.pwd)
46
- end
47
-
48
- # Compile all .scss files from known directories.
49
- def compile_all(root_dir = Dir.pwd)
50
- output_dir = File.join(root_dir, CSS_OUTPUT)
51
- FileUtils.mkdir_p(output_dir)
52
-
53
- SCSS_DIRS.each do |dir|
54
- scss_dir = File.join(root_dir, dir)
55
- next unless Dir.exist?(scss_dir)
56
-
57
- Dir.glob(File.join(scss_dir, "**/*.scss")).each do |scss_file|
58
- next if File.basename(scss_file).start_with?("_") # Skip partials
59
- compile_file(scss_file, output_dir, scss_dir)
60
- end
61
- end
62
- end
63
-
64
- # Compile a single SCSS file. If output_dir is provided, writes CSS there.
65
- # Always returns the compiled CSS string.
66
- def compile_file(scss_file, output_dir = nil, base_dir = nil)
67
- base_dir ||= File.dirname(scss_file)
68
- scss_content = File.read(scss_file)
69
- css_content = compile_scss(scss_content, File.dirname(scss_file))
70
-
71
- if output_dir
72
- relative = scss_file.sub(base_dir, "").sub(/\.scss$/, ".css")
73
- css_file = File.join(output_dir, relative)
74
- FileUtils.mkdir_p(File.dirname(css_file))
75
- existing = File.exist?(css_file) ? File.read(css_file) : nil
76
- if existing != css_content
77
- File.write(css_file, css_content)
78
- Tina4::Log.debug("Compiled SCSS: #{scss_file} -> #{css_file}")
79
- end
80
- end
81
-
82
- css_content
83
- rescue => e
84
- Tina4::Log.error("SCSS compilation failed: #{scss_file} - #{e.message}")
85
- ""
86
- end
87
-
88
- # Compile an SCSS content string with a base directory for import resolution.
89
- def compile_scss(content, base_dir)
90
- # Try sassc gem first
91
- begin
92
- require "sassc"
93
- return SassC::Engine.new(content, style: :expanded, load_paths: [base_dir]).render
94
- rescue LoadError
95
- # Fall through to basic compiler
96
- end
97
-
98
- # Basic SCSS to CSS conversion (handles common patterns)
99
- basic_compile(content, base_dir)
100
- end
101
-
102
- private
103
-
104
- # Split trailing !default / !global flags off a variable declaration value.
105
- # Returns [value_without_flags, declares_default].
106
- #
107
- # Only ever called on the value of a `$name: value;` declaration, so a
108
- # literal !default anywhere else -- inside a quoted string
109
- # (`content: "x !default y"`) or a function argument -- is left untouched,
110
- # exactly as Dart Sass leaves it. A blanket strip would corrupt real string
111
- # content, and would silently turn `rgba(#000 !default, 0.1)` (a syntax
112
- # error in Dart Sass) into valid-looking CSS that Sass would never emit.
113
- #
114
- # NOTE: this runs a regex, so it clobbers the caller's match globals --
115
- # callers must capture their own groups first.
116
- def strip_variable_flags(value)
117
- declares_default = false
118
- loop do
119
- match = VARIABLE_FLAG.match(value)
120
- break unless match
121
-
122
- declares_default = true if match[1] == "default"
123
- value = value[0, match.begin(0)]
124
- end
125
- [value.strip, declares_default]
126
- end
127
-
128
- def basic_compile(content, base_dir)
129
- # Handle @import
130
- content = process_imports(content, base_dir)
131
-
132
- # Handle variables. The /m flag makes the value span newlines, matching
133
- # the Python master's `[^;]+`: without it a MULTI-LINE declaration (a map
134
- # literal with its closing `) !default;` on its own line -- exactly what
135
- # tina4-css's _variables.scss uses) was never extracted, and the raw SCSS
136
- # was dumped verbatim into the CSS.
137
- variables = {}
138
- content = content.gsub(/\$([a-zA-Z_][\w-]*)\s*:\s*(.+?);/m) do
139
- # Capture BOTH groups before calling strip_variable_flags -- it runs a
140
- # regex, which clobbers the match globals this block relies on.
141
- name = Regexp.last_match(1)
142
- raw = Regexp.last_match(2).strip
143
- value, declares_default = strip_variable_flags(raw)
144
- # !default must not overwrite a value that is already set.
145
- next "" if declares_default && variables.fetch(name, "null") != "null"
146
-
147
- variables[name] = value
148
- ""
149
- end
150
-
151
- # Resolve #{ ... } interpolation (before $var substitution + nesting).
152
- content = resolve_interpolation(content, variables)
153
-
154
- # Replace variable references
155
- variables.each do |name, value|
156
- content = content.gsub("$#{name}", value)
157
- end
158
-
159
- # Resolve color functions (lighten/darken/rgba/rgb/mix) — after variable
160
- # substitution so a $colour arg is already a hex literal (issue #124).
161
- content = resolve_color_functions(content)
162
-
163
- # Handle nesting (basic single-level)
164
- content = flatten_nesting(content)
165
-
166
- # Handle & parent selector
167
- content = content.gsub(/&/, "")
168
-
169
- content
170
- end
171
-
172
- # Resolve lighten/darken/rgba/rgb/mix color functions. rgba(<hex>, a) is
173
- # the damaging case (issue #124): the functional rgba() notation cannot
174
- # take a hex, so rgba(#0f3460, 0.12) is invalid CSS and browsers drop the
175
- # whole declaration. Convert the hex to its r,g,b components. hex_to_rgb /
176
- # adjust_lightness are regex-free so they never clobber the match globals.
177
- def resolve_color_functions(content)
178
- content = content.gsub(/lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/) do
179
- adjust_lightness(Regexp.last_match(1).strip, Regexp.last_match(2).strip.chomp("%").to_f / 100)
180
- end
181
- content = content.gsub(/darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/) do
182
- adjust_lightness(Regexp.last_match(1).strip, -(Regexp.last_match(2).strip.chomp("%").to_f / 100))
183
- end
184
- # rgba(<hex>, <alpha>) — only the two-arg hex form; leave rgba(r,g,b,a).
185
- content = content.gsub(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/) do
186
- hex = Regexp.last_match(1)
187
- alpha = Regexp.last_match(2).strip
188
- whole = Regexp.last_match(0)
189
- rgb = hex_to_rgb(hex)
190
- rgb ? "rgba(#{rgb[0]}, #{rgb[1]}, #{rgb[2]}, #{alpha})" : whole
191
- end
192
- content = content.gsub(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/) do
193
- hex = Regexp.last_match(1)
194
- whole = Regexp.last_match(0)
195
- rgb = hex_to_rgb(hex)
196
- rgb ? "rgb(#{rgb[0]}, #{rgb[1]}, #{rgb[2]})" : whole
197
- end
198
- # mix(<c1>, <c2>[, <weight>]) — Sass weight is c1's proportion (default 50%).
199
- content.gsub(/mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/) do
200
- hex1 = Regexp.last_match(1)
201
- hex2 = Regexp.last_match(2)
202
- weight = Regexp.last_match(3)
203
- whole = Regexp.last_match(0)
204
- c1 = hex_to_rgb(hex1)
205
- c2 = hex_to_rgb(hex2)
206
- if c1 && c2
207
- w = weight ? weight.chomp("%").to_f / 100 : 0.5
208
- mixed = (0..2).map { |i| (c1[i] * w + c2[i] * (1 - w)).round }
209
- format("#%02x%02x%02x", *mixed)
210
- else
211
- whole
212
- end
213
- end
214
- end
215
-
216
- # Parse a #rgb / #rrggbb hex string into an [r, g, b] int array, or nil.
217
- # Regex-free (uses Integer()) so it never touches the match globals.
218
- def hex_to_rgb(color)
219
- c = color.strip.delete_prefix("#")
220
- c = c.chars.map { |ch| ch * 2 }.join if c.length == 3
221
- return nil unless c.length == 6
222
-
223
- [Integer(c[0, 2], 16), Integer(c[2, 2], 16), Integer(c[4, 2], 16)]
224
- rescue ArgumentError
225
- nil
226
- end
227
-
228
- # Adjust the HSL lightness of a hex color by `amount` (-1..1), return hex.
229
- # Truncates (to_i) to match the Python master's int(x*255) byte-for-byte.
230
- def adjust_lightness(color, amount)
231
- rgb = hex_to_rgb(color)
232
- return color if rgb.nil?
233
-
234
- r, g, b = rgb.map { |v| v / 255.0 }
235
- max = [r, g, b].max
236
- min = [r, g, b].min
237
- l = (max + min) / 2.0
238
- d = max - min
239
- h = 0.0
240
- s = 0.0
241
- if d != 0.0
242
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
243
- h = if max == r
244
- (g - b) / d + (g < b ? 6 : 0)
245
- elsif max == g
246
- (b - r) / d + 2
247
- else
248
- (r - g) / d + 4
249
- end
250
- h /= 6.0
251
- end
252
- l = [0.0, [1.0, l + amount].min].max
253
- if s.zero?
254
- r = g = b = l
255
- else
256
- q = l < 0.5 ? l * (1 + s) : l + s - l * s
257
- p = 2 * l - q
258
- r = _hue_to_rgb(p, q, h + 1.0 / 3)
259
- g = _hue_to_rgb(p, q, h)
260
- b = _hue_to_rgb(p, q, h - 1.0 / 3)
261
- end
262
- format("#%02x%02x%02x", (r * 255).to_i, (g * 255).to_i, (b * 255).to_i)
263
- end
264
-
265
- def _hue_to_rgb(p, q, t)
266
- t += 1 if t < 0
267
- t -= 1 if t > 1
268
- return p + (q - p) * 6 * t if t < 1.0 / 6
269
- return q if t < 1.0 / 2
270
- return p + (q - p) * (2.0 / 3 - t) * 6 if t < 2.0 / 3
271
-
272
- p
273
- end
274
-
275
- # Resolve SCSS #{ ... } interpolation. Each #{ expr } is replaced by its
276
- # resolved inner text: a $variable inside the braces resolves to its value,
277
- # anything else is inlined verbatim (trimmed). Lets a value carry a
278
- # variable inside a string context plain $var substitution can't reach --
279
- # e.g. calc(100% - #{$gap}) -> calc(100% - 20px) -- and lets a variable
280
- # appear in a selector (.icon-#{$name} -> .icon-home). Run BEFORE nesting
281
- # flatten so the literal braces never confuse the block matcher.
282
- def resolve_interpolation(content, variables)
283
- names = variables.keys.sort_by { |k| -k.length }
284
- content.gsub(/#\{([^{}]*)\}/) do
285
- inner = Regexp.last_match(1).strip
286
- names.each { |name| inner = inner.gsub("$#{name}", variables[name]) }
287
- inner
288
- end
289
- end
290
-
291
- def process_imports(content, base_dir)
292
- content.gsub(/@import\s+["'](.+?)["']\s*;/) do
293
- import_path = Regexp.last_match(1)
294
- candidates = [
295
- File.join(base_dir, "#{import_path}.scss"),
296
- File.join(base_dir, "_#{import_path}.scss"),
297
- File.join(base_dir, import_path)
298
- ]
299
- # Also search additional import paths
300
- (@import_paths || []).each do |search_path|
301
- candidates << File.join(search_path, "#{import_path}.scss")
302
- candidates << File.join(search_path, "_#{import_path}.scss")
303
- candidates << File.join(search_path, import_path)
304
- end
305
- found = candidates.find { |c| File.exist?(c) }
306
- if found
307
- imported = File.read(found)
308
- process_imports(imported, File.dirname(found))
309
- else
310
- "/* import not found: #{import_path} */"
311
- end
312
- end
313
- end
314
-
315
- def flatten_nesting(content)
316
- # Very basic nesting flattener - handles single level
317
- result = ""
318
- content.scan(/([^{]+)\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/m) do |selector, body|
319
- selector = selector.strip
320
- # Check for nested rules
321
- if body =~ /\{/
322
- # Has nested content
323
- props = ""
324
- nested = ""
325
- body.scan(/([^{;]+(?:\{[^}]*\}|;))/m) do |part_arr|
326
- part = part_arr[0].strip
327
- if part.include?("{")
328
- # Nested rule
329
- if part =~ /\A(.+?)\s*\{(.*?)\}\s*\z/m
330
- nested_sel = Regexp.last_match(1).strip
331
- nested_body = Regexp.last_match(2).strip
332
- full_sel = "#{selector} #{nested_sel}"
333
- nested += "#{full_sel} { #{nested_body} }\n"
334
- end
335
- else
336
- props += " #{part}\n" unless part.empty?
337
- end
338
- end
339
- result += "#{selector} {\n#{props}}\n" unless props.strip.empty?
340
- result += nested
341
- else
342
- result += "#{selector} {\n#{body}\n}\n"
343
- end
344
- end
345
- result.empty? ? content : result
346
- end
347
- end
348
- end
349
- end