tina4ruby 3.13.46 → 3.13.48
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/localization.rb +155 -49
- data/lib/tina4/migration.rb +113 -38
- data/lib/tina4/scss_compiler.rb +19 -0
- data/lib/tina4/version.rb +1 -1
- 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: 9a325d3de2855893935edf93852d2ebc796def59bbb68dd668420da07de78f28
|
|
4
|
+
data.tar.gz: be1f417f6fe10f8e68c038a16839eb0a036bb8b1a15b58f67764ef1e1d31988d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c5da9a717c386c9e36a5fd5cb1d347d7bf5baa660c8efaee3f564dc37acadb3398f98a81c73d90885b71e1571e9edc93896cf1921584953b80372ccd3dca02d
|
|
7
|
+
data.tar.gz: 5ccc156283bcdb8fc2e176b7342358cc43adde6c68191bf605e2e8e7774fd8d7c574798a589d14647ea75fb82d7a9d81ab01e1ed0ed9d8bf07e7bb60340e8317
|
data/lib/tina4/localization.rb
CHANGED
|
@@ -24,38 +24,19 @@ module Tina4
|
|
|
24
24
|
@current_locale = locale.to_s
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
locale_dir = File.expand_path(dir, root_dir)
|
|
33
|
-
next unless Dir.exist?(locale_dir)
|
|
27
|
+
# The fallback locale used when a key is missing in the current locale.
|
|
28
|
+
# Mirrors the Python master (TINA4_LOCALE, else "en").
|
|
29
|
+
def default_locale
|
|
30
|
+
ENV["TINA4_LOCALE"] || "en"
|
|
31
|
+
end
|
|
34
32
|
|
|
33
|
+
def load(root_dir = Dir.pwd)
|
|
34
|
+
locale_search_dirs(root_dir).each do |locale_dir|
|
|
35
35
|
Dir.glob(File.join(locale_dir, "*.json")).each do |file|
|
|
36
|
-
|
|
37
|
-
data = JSON.parse(File.read(file))
|
|
38
|
-
translations[locale] ||= {}
|
|
39
|
-
translations[locale].merge!(data)
|
|
40
|
-
# Build leaf-key aliases from the loaded data
|
|
41
|
-
build_leaf_aliases(locale, data)
|
|
42
|
-
Tina4::Log.debug("Loaded locale: #{locale} from #{file}")
|
|
36
|
+
ingest_json_file(File.basename(file, ".json"), file)
|
|
43
37
|
end
|
|
44
|
-
|
|
45
|
-
# Also support YAML
|
|
46
38
|
Dir.glob(File.join(locale_dir, "*.{yml,yaml}")).each do |file|
|
|
47
|
-
|
|
48
|
-
require "yaml"
|
|
49
|
-
locale = File.basename(file, File.extname(file))
|
|
50
|
-
data = YAML.safe_load(File.read(file))
|
|
51
|
-
if data.is_a?(Hash)
|
|
52
|
-
translations[locale] ||= {}
|
|
53
|
-
translations[locale].merge!(data)
|
|
54
|
-
build_leaf_aliases(locale, data)
|
|
55
|
-
end
|
|
56
|
-
rescue LoadError
|
|
57
|
-
Tina4::Log.warning("YAML support requires the 'yaml' gem")
|
|
58
|
-
end
|
|
39
|
+
ingest_yaml_file(File.basename(file, File.extname(file)), file)
|
|
59
40
|
end
|
|
60
41
|
end
|
|
61
42
|
end
|
|
@@ -64,22 +45,53 @@ module Tina4
|
|
|
64
45
|
lang = locale || current_locale
|
|
65
46
|
value = lookup(lang, key)
|
|
66
47
|
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
# Fallback: current locale -> default/fallback locale -> the key itself.
|
|
49
|
+
if value.nil? && lang != default_locale
|
|
50
|
+
value = lookup(default_locale, key)
|
|
69
51
|
end
|
|
70
52
|
|
|
71
53
|
value = default || key if value.nil?
|
|
72
54
|
|
|
73
|
-
# Interpolation: "Hello
|
|
74
|
-
interpolations
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
value
|
|
55
|
+
# Interpolation: "Hello {name}" => "Hello World". PARTIAL — each {name}
|
|
56
|
+
# token present in interpolations is replaced; a missing param's
|
|
57
|
+
# placeholder is left literal, and a malformed placeholder ({x.y} with a
|
|
58
|
+
# dot, {n:d} with a spec, a lone unmatched brace) is left literal too.
|
|
59
|
+
# Never raises — a bad template must not crash t().
|
|
60
|
+
interpolate(value, interpolations)
|
|
79
61
|
end
|
|
80
62
|
|
|
81
63
|
def set_locale(locale)
|
|
82
64
|
self.current_locale = locale.to_s
|
|
65
|
+
# Lazily load the new locale's file if it hasn't been loaded yet, so a
|
|
66
|
+
# switch to an unloaded locale resolves its keys (parity with Python/PHP/
|
|
67
|
+
# Node, where setting the locale triggers a per-locale load).
|
|
68
|
+
load_locale(locale.to_s) unless translations.key?(locale.to_s)
|
|
69
|
+
current_locale
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Load a SINGLE locale's file (JSON, then YAML) from the configured search
|
|
73
|
+
# dirs if it is not already loaded. Boot-safe (a malformed file is skipped,
|
|
74
|
+
# never raised) — mirrors Python's _load_locale.
|
|
75
|
+
def load_locale(locale, root_dir = Dir.pwd)
|
|
76
|
+
locale = locale.to_s
|
|
77
|
+
return if translations.key?(locale)
|
|
78
|
+
|
|
79
|
+
locale_search_dirs(root_dir).each do |locale_dir|
|
|
80
|
+
json = File.join(locale_dir, "#{locale}.json")
|
|
81
|
+
if File.file?(json)
|
|
82
|
+
ingest_json_file(locale, json)
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
%w[yml yaml].each do |ext|
|
|
86
|
+
yaml = File.join(locale_dir, "#{locale}.#{ext}")
|
|
87
|
+
if File.file?(yaml)
|
|
88
|
+
ingest_yaml_file(locale, yaml)
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
# No file found — cache an empty table so lookups fall back to the key.
|
|
94
|
+
translations[locale] ||= {}
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
def get_locale
|
|
@@ -117,12 +129,102 @@ module Tina4
|
|
|
117
129
|
end
|
|
118
130
|
end
|
|
119
131
|
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
# List available locale codes by scanning the configured locale dir(s) for
|
|
133
|
+
# *.json / *.yml / *.yaml file stems, sorted. When no dir exists/has files,
|
|
134
|
+
# returns a [default_locale] floor (parity with Python/PHP/Node — never an
|
|
135
|
+
# empty list when a default locale is known).
|
|
136
|
+
def available_locales(root_dir = Dir.pwd)
|
|
137
|
+
stems = []
|
|
138
|
+
locale_search_dirs(root_dir).each do |locale_dir|
|
|
139
|
+
%w[*.json *.yml *.yaml].each do |pattern|
|
|
140
|
+
Dir.glob(File.join(locale_dir, pattern)).each do |file|
|
|
141
|
+
stems << File.basename(file, File.extname(file))
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
stems.uniq!
|
|
146
|
+
stems.empty? ? [default_locale] : stems.sort
|
|
122
147
|
end
|
|
123
148
|
|
|
124
149
|
private
|
|
125
150
|
|
|
151
|
+
# The locale directories to scan, in order: TINA4_LOCALE_DIR override (when
|
|
152
|
+
# set) else the built-in LOCALE_DIRS list, each resolved against root_dir,
|
|
153
|
+
# filtered to those that actually exist.
|
|
154
|
+
def locale_search_dirs(root_dir = Dir.pwd)
|
|
155
|
+
override = ENV["TINA4_LOCALE_DIR"]
|
|
156
|
+
dirs = override && !override.empty? ? [override] : LOCALE_DIRS
|
|
157
|
+
dirs.map { |dir| File.expand_path(dir, root_dir) }.select { |d| Dir.exist?(d) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Parse one JSON locale file into translations + leaf aliases. Boot-safe:
|
|
161
|
+
# a malformed/unreadable file is logged, skipped, and cached empty so a
|
|
162
|
+
# bad file never crashes boot and is never retried into a partial state.
|
|
163
|
+
def ingest_json_file(locale, file)
|
|
164
|
+
data = JSON.parse(File.read(file))
|
|
165
|
+
translations[locale] ||= {}
|
|
166
|
+
translations[locale].merge!(data)
|
|
167
|
+
build_leaf_aliases(locale, data)
|
|
168
|
+
Tina4::Log.debug("Loaded locale: #{locale} from #{file}")
|
|
169
|
+
rescue JSON::ParserError, IOError, SystemCallError, StandardError => e
|
|
170
|
+
Tina4::Log.error("Skipping malformed locale file #{file}: #{e.class}: #{e.message}")
|
|
171
|
+
translations[locale] ||= {}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Parse one YAML locale file into translations + leaf aliases. Same
|
|
175
|
+
# boot-safe policy as ingest_json_file (Psych::SyntaxError is the YAML
|
|
176
|
+
# parse error; the broad StandardError is a final safety net).
|
|
177
|
+
def ingest_yaml_file(locale, file)
|
|
178
|
+
require "yaml"
|
|
179
|
+
data = YAML.safe_load(File.read(file))
|
|
180
|
+
if data.is_a?(Hash)
|
|
181
|
+
translations[locale] ||= {}
|
|
182
|
+
translations[locale].merge!(data)
|
|
183
|
+
build_leaf_aliases(locale, data)
|
|
184
|
+
end
|
|
185
|
+
rescue LoadError
|
|
186
|
+
Tina4::Log.warning("YAML support requires the 'yaml' gem")
|
|
187
|
+
rescue Psych::SyntaxError, IOError, SystemCallError, StandardError => e
|
|
188
|
+
Tina4::Log.error("Skipping malformed locale file #{file}: #{e.class}: #{e.message}")
|
|
189
|
+
translations[locale] ||= {}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# {name} placeholder. Matches Python's _PLACEHOLDER = \{(\w+)\}, so only a
|
|
193
|
+
# bare word inside braces is a candidate — {x.y} (a dot) and {n:d} (a spec)
|
|
194
|
+
# never match and are left literal, and a lone unmatched brace is untouched.
|
|
195
|
+
PLACEHOLDER = /\{(\w+)\}/.freeze
|
|
196
|
+
|
|
197
|
+
# Substitute {name} placeholders from params. PARTIAL + literal-leftover:
|
|
198
|
+
# a placeholder present in params is replaced; a missing or malformed
|
|
199
|
+
# placeholder is left untouched. Never raises (a bad template must not
|
|
200
|
+
# crash t()). Params may be keyed by symbol (kwargs) or string.
|
|
201
|
+
def interpolate(template, params)
|
|
202
|
+
return template unless template.is_a?(String)
|
|
203
|
+
return template if params.nil? || params.empty?
|
|
204
|
+
|
|
205
|
+
template.gsub(PLACEHOLDER) do
|
|
206
|
+
name = Regexp.last_match(1)
|
|
207
|
+
if params.key?(name.to_sym)
|
|
208
|
+
params[name.to_sym].to_s
|
|
209
|
+
elsif params.key?(name)
|
|
210
|
+
params[name].to_s
|
|
211
|
+
else
|
|
212
|
+
Regexp.last_match(0)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Render a non-string locale scalar JSON-natively: boolean true/false ->
|
|
218
|
+
# "true"/"false", null/nil -> "null", numbers as their plain form ("42").
|
|
219
|
+
def coerce_scalar(value)
|
|
220
|
+
case value
|
|
221
|
+
when true then "true"
|
|
222
|
+
when false then "false"
|
|
223
|
+
when nil then "null"
|
|
224
|
+
else value.to_s
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
126
228
|
# Recursively walk a nested hash and register leaf-key aliases.
|
|
127
229
|
# First-wins: if a leaf key already exists, it is NOT overwritten.
|
|
128
230
|
def build_leaf_aliases(locale, hash, prefix = nil)
|
|
@@ -132,30 +234,34 @@ module Tina4
|
|
|
132
234
|
if value.is_a?(Hash)
|
|
133
235
|
build_leaf_aliases(locale, value, full_key)
|
|
134
236
|
else
|
|
135
|
-
# Store the leaf key as an alias (first-wins)
|
|
136
|
-
|
|
237
|
+
# Store the leaf key as an alias (first-wins). Coerce non-string
|
|
238
|
+
# JSON-native scalars (true/false/null/number) to their string form.
|
|
239
|
+
flat_aliases[locale.to_s][key.to_s] ||= coerce_scalar(value)
|
|
137
240
|
end
|
|
138
241
|
end
|
|
139
242
|
end
|
|
140
243
|
|
|
141
244
|
def lookup(locale, key)
|
|
142
245
|
keys = key.to_s.split(".")
|
|
143
|
-
|
|
144
|
-
return nil unless
|
|
246
|
+
node = translations[locale]
|
|
247
|
+
return nil unless node
|
|
145
248
|
|
|
146
|
-
# Try dot-path traversal first
|
|
147
|
-
|
|
249
|
+
# Try dot-path traversal first. Track found-ness explicitly so a leaf
|
|
250
|
+
# whose value is false/nil is still recognised as resolved.
|
|
251
|
+
found = true
|
|
148
252
|
keys.each do |k|
|
|
149
|
-
if
|
|
150
|
-
|
|
253
|
+
if node.is_a?(Hash) && (node.key?(k) || node.key?(k.to_sym))
|
|
254
|
+
node = node.key?(k) ? node[k] : node[k.to_sym]
|
|
151
255
|
else
|
|
152
|
-
|
|
256
|
+
found = false
|
|
153
257
|
break
|
|
154
258
|
end
|
|
155
259
|
end
|
|
156
|
-
|
|
260
|
+
# A resolved leaf (string OR JSON-native scalar) wins. An intermediate
|
|
261
|
+
# Hash is not a value — fall through to the leaf alias instead.
|
|
262
|
+
return coerce_scalar(node) if found && !node.is_a?(Hash)
|
|
157
263
|
|
|
158
|
-
# Fall back to leaf-key alias (
|
|
264
|
+
# Fall back to leaf-key alias (stored already-coerced as a string).
|
|
159
265
|
if flat_aliases[locale]
|
|
160
266
|
alias_val = flat_aliases[locale][key.to_s]
|
|
161
267
|
return alias_val if alias_val.is_a?(String)
|
data/lib/tina4/migration.rb
CHANGED
|
@@ -366,59 +366,134 @@ module Tina4
|
|
|
366
366
|
sql.gsub(SMART_QUOTE_RE, SMART_QUOTES)
|
|
367
367
|
end
|
|
368
368
|
|
|
369
|
-
# Split SQL into individual statements,
|
|
370
|
-
# -
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
#
|
|
369
|
+
# Split SQL into individual statements with a single-pass, quote- and
|
|
370
|
+
# comment-aware scanner. The split decision is made character by character so
|
|
371
|
+
# the delimiter only ever fires in real statement position.
|
|
372
|
+
#
|
|
373
|
+
# This is the fix for issue #54: the old implementation split on +delimiter+
|
|
374
|
+
# BEFORE stripping +-- …+ line comments, so a +;+ inside a line comment
|
|
375
|
+
# fragmented one statement into several broken pieces. A scanner that knows
|
|
376
|
+
# where it is (code / comment / string) cannot make that mistake.
|
|
377
|
+
#
|
|
378
|
+
# Handled, in priority order, only when NOT already inside a stored-proc block:
|
|
379
|
+
# - $$ … $$ and // … // stored-proc blocks are kept intact (inner ; never
|
|
380
|
+
# splits). A // preceded by ':' is a URL scheme (https://…), not a delimiter.
|
|
381
|
+
# - /* … */ block comments are stripped.
|
|
382
|
+
# - -- … line comments are stripped to end of line (the newline is kept).
|
|
383
|
+
# - '…' single-quoted strings and "…" double-quoted identifiers are copied
|
|
384
|
+
# verbatim, honouring the SQL doubled-quote escape ('' / ""); a ;, -- or /*
|
|
385
|
+
# inside a literal is data, not a delimiter or comment.
|
|
386
|
+
# Mirrors the tina4-python _split_statements / tina4-php scanner (parity).
|
|
375
387
|
def split_sql_statements(sql, delimiter = ";")
|
|
376
388
|
# Normalize smart/curly quotes to straight ASCII first, so SQL pasted from
|
|
377
389
|
# an editor/doc (which converts " → “ ” and ' → ‘ ’) actually runs.
|
|
378
390
|
sql = normalize_quotes(sql)
|
|
379
391
|
|
|
380
|
-
|
|
392
|
+
statements = []
|
|
393
|
+
current = +""
|
|
394
|
+
n = sql.length
|
|
395
|
+
dlen = delimiter.length
|
|
396
|
+
i = 0
|
|
397
|
+
in_dollar_block = false
|
|
398
|
+
in_slash_block = false
|
|
399
|
+
|
|
400
|
+
while i < n
|
|
401
|
+
ch = sql[i]
|
|
402
|
+
|
|
403
|
+
# $$ … $$ stored-proc block (toggle).
|
|
404
|
+
if !in_slash_block && ch == "$" && i + 1 < n && sql[i + 1] == "$"
|
|
405
|
+
current << "$$"
|
|
406
|
+
i += 2
|
|
407
|
+
in_dollar_block = !in_dollar_block
|
|
408
|
+
next
|
|
409
|
+
end
|
|
381
410
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
411
|
+
# // … // stored-proc block (toggle) — but NOT a `://` URL scheme.
|
|
412
|
+
if !in_dollar_block && ch == "/" && i + 1 < n && sql[i + 1] == "/" &&
|
|
413
|
+
!(i.positive? && sql[i - 1] == ":")
|
|
414
|
+
current << "//"
|
|
415
|
+
i += 2
|
|
416
|
+
in_slash_block = !in_slash_block
|
|
417
|
+
next
|
|
418
|
+
end
|
|
387
419
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
420
|
+
# Inside a stored-proc block: consume verbatim (inner ; never splits).
|
|
421
|
+
if in_dollar_block || in_slash_block
|
|
422
|
+
current << ch
|
|
423
|
+
i += 1
|
|
424
|
+
next
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Block comment /* … */ — stripped.
|
|
428
|
+
if ch == "/" && i + 1 < n && sql[i + 1] == "*"
|
|
429
|
+
endpos = sql.index("*/", i + 2)
|
|
430
|
+
i = endpos ? endpos + 2 : n
|
|
431
|
+
next
|
|
432
|
+
end
|
|
397
433
|
|
|
398
|
-
|
|
399
|
-
|
|
434
|
+
# Line comment -- … — stripped to end of line; the newline is left for the
|
|
435
|
+
# next iteration so line structure (and NEXT-line boundaries) survive.
|
|
436
|
+
if ch == "-" && i + 1 < n && sql[i + 1] == "-"
|
|
437
|
+
endpos = sql.index("\n", i + 2)
|
|
438
|
+
i = endpos || n
|
|
439
|
+
next
|
|
440
|
+
end
|
|
400
441
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
442
|
+
# Single-quoted string literal — '' escapes a quote. Copied verbatim.
|
|
443
|
+
if ch == "'"
|
|
444
|
+
current << "'"
|
|
445
|
+
i += 1
|
|
446
|
+
while i < n
|
|
447
|
+
if sql[i] == "'" && i + 1 < n && sql[i + 1] == "'"
|
|
448
|
+
current << "''"
|
|
449
|
+
i += 2
|
|
450
|
+
elsif sql[i] == "'"
|
|
451
|
+
current << "'"
|
|
452
|
+
i += 1
|
|
453
|
+
break
|
|
454
|
+
else
|
|
455
|
+
current << sql[i]
|
|
456
|
+
i += 1
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
next
|
|
411
460
|
end
|
|
412
|
-
cleaned = lines.join("\n").strip
|
|
413
461
|
|
|
414
|
-
#
|
|
415
|
-
|
|
416
|
-
|
|
462
|
+
# Double-quoted identifier — "" escapes a quote. Same verbatim handling.
|
|
463
|
+
if ch == '"'
|
|
464
|
+
current << '"'
|
|
465
|
+
i += 1
|
|
466
|
+
while i < n
|
|
467
|
+
if sql[i] == '"' && i + 1 < n && sql[i + 1] == '"'
|
|
468
|
+
current << '""'
|
|
469
|
+
i += 2
|
|
470
|
+
elsif sql[i] == '"'
|
|
471
|
+
current << '"'
|
|
472
|
+
i += 1
|
|
473
|
+
break
|
|
474
|
+
else
|
|
475
|
+
current << sql[i]
|
|
476
|
+
i += 1
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
next
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Statement delimiter — only reached outside blocks/comments/strings.
|
|
483
|
+
if !delimiter.empty? && sql[i, dlen] == delimiter
|
|
484
|
+
stmt = current.strip
|
|
485
|
+
statements << stmt unless stmt.empty?
|
|
486
|
+
current = +""
|
|
487
|
+
i += dlen
|
|
488
|
+
next
|
|
417
489
|
end
|
|
418
490
|
|
|
419
|
-
|
|
491
|
+
current << ch
|
|
492
|
+
i += 1
|
|
420
493
|
end
|
|
421
494
|
|
|
495
|
+
stmt = current.strip
|
|
496
|
+
statements << stmt unless stmt.empty?
|
|
422
497
|
statements
|
|
423
498
|
end
|
|
424
499
|
|
data/lib/tina4/scss_compiler.rb
CHANGED
|
@@ -103,6 +103,9 @@ module Tina4
|
|
|
103
103
|
""
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
+
# Resolve #{ ... } interpolation (before $var substitution + nesting).
|
|
107
|
+
content = resolve_interpolation(content, variables)
|
|
108
|
+
|
|
106
109
|
# Replace variable references
|
|
107
110
|
variables.each do |name, value|
|
|
108
111
|
content = content.gsub("$#{name}", value)
|
|
@@ -117,6 +120,22 @@ module Tina4
|
|
|
117
120
|
content
|
|
118
121
|
end
|
|
119
122
|
|
|
123
|
+
# Resolve SCSS #{ ... } interpolation. Each #{ expr } is replaced by its
|
|
124
|
+
# resolved inner text: a $variable inside the braces resolves to its value,
|
|
125
|
+
# anything else is inlined verbatim (trimmed). Lets a value carry a
|
|
126
|
+
# variable inside a string context plain $var substitution can't reach --
|
|
127
|
+
# e.g. calc(100% - #{$gap}) -> calc(100% - 20px) -- and lets a variable
|
|
128
|
+
# appear in a selector (.icon-#{$name} -> .icon-home). Run BEFORE nesting
|
|
129
|
+
# flatten so the literal braces never confuse the block matcher.
|
|
130
|
+
def resolve_interpolation(content, variables)
|
|
131
|
+
names = variables.keys.sort_by { |k| -k.length }
|
|
132
|
+
content.gsub(/#\{([^{}]*)\}/) do
|
|
133
|
+
inner = Regexp.last_match(1).strip
|
|
134
|
+
names.each { |name| inner = inner.gsub("$#{name}", variables[name]) }
|
|
135
|
+
inner
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
120
139
|
def process_imports(content, base_dir)
|
|
121
140
|
content.gsub(/@import\s+["'](.+?)["']\s*;/) do
|
|
122
141
|
import_path = Regexp.last_match(1)
|
data/lib/tina4/version.rb
CHANGED
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.48
|
|
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-06-
|
|
11
|
+
date: 2026-06-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|