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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +883 -0
- data/README.md +1 -1
- data/lib/tina4/auth.rb +166 -87
- data/lib/tina4/auto_crud.rb +29 -32
- data/lib/tina4/cache_backends/base_backend.rb +19 -0
- data/lib/tina4/cache_backends/database_backend.rb +29 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +124 -13
- data/lib/tina4/cache_backends/memory_backend.rb +15 -0
- data/lib/tina4/cache_backends/redis_backend.rb +173 -52
- data/lib/tina4/cache_backends.rb +10 -1
- data/lib/tina4/cli.rb +35 -43
- data/lib/tina4/cors.rb +186 -30
- data/lib/tina4/database/sqlite3_adapter.rb +4 -1
- data/lib/tina4/database.rb +458 -48
- data/lib/tina4/database_adapter.rb +178 -0
- data/lib/tina4/database_result.rb +63 -17
- data/lib/tina4/database_url.rb +363 -0
- data/lib/tina4/dev.rb +0 -1
- data/lib/tina4/dev_admin.rb +118 -20
- data/lib/tina4/dev_mailbox.rb +5 -1
- data/lib/tina4/dispatch_pipeline.rb +605 -0
- data/lib/tina4/docstore.rb +274 -60
- data/lib/tina4/drivers/firebird_driver.rb +118 -4
- data/lib/tina4/drivers/mongodb_driver.rb +19 -4
- data/lib/tina4/drivers/mssql_driver.rb +73 -10
- data/lib/tina4/drivers/mysql_driver.rb +71 -4
- data/lib/tina4/drivers/odbc_driver.rb +40 -4
- data/lib/tina4/drivers/postgres_driver.rb +97 -10
- data/lib/tina4/drivers/sqlite_driver.rb +25 -3
- data/lib/tina4/env.rb +176 -34
- data/lib/tina4/field_types.rb +12 -0
- data/lib/tina4/frond.rb +102 -10
- data/lib/tina4/health.rb +30 -14
- data/lib/tina4/job.rb +15 -5
- data/lib/tina4/log.rb +236 -32
- data/lib/tina4/mcp.rb +11 -5
- data/lib/tina4/messenger.rb +317 -82
- data/lib/tina4/metrics.rb +179 -891
- data/lib/tina4/middleware.rb +191 -56
- data/lib/tina4/migration.rb +17 -1
- data/lib/tina4/orm.rb +114 -17
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/queue.rb +154 -9
- data/lib/tina4/queue_backends/kafka_backend.rb +191 -2
- data/lib/tina4/queue_backends/lite_backend.rb +121 -25
- data/lib/tina4/queue_backends/mongo_backend.rb +146 -10
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +194 -1
- data/lib/tina4/rack_app.rb +94 -316
- data/lib/tina4/request.rb +48 -8
- data/lib/tina4/response.rb +42 -1
- data/lib/tina4/response_cache.rb +142 -24
- data/lib/tina4/router.rb +141 -12
- data/lib/tina4/session.rb +243 -29
- data/lib/tina4/session_handlers/database_handler.rb +185 -20
- data/lib/tina4/session_handlers/file_handler.rb +113 -21
- data/lib/tina4/session_handlers/memcached_handler.rb +183 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +232 -15
- data/lib/tina4/session_handlers/mongo_wire_client.rb +300 -0
- data/lib/tina4/session_handlers/redis_handler.rb +20 -6
- data/lib/tina4/session_handlers/valkey_handler.rb +18 -4
- data/lib/tina4/shutdown.rb +180 -30
- data/lib/tina4/sql_translator.rb +110 -0
- data/lib/tina4/swagger.rb +50 -18
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -6
- data/lib/tina4.rb +301 -38
- metadata +35 -17
- data/lib/tina4/scss_compiler.rb +0 -349
data/lib/tina4/scss_compiler.rb
DELETED
|
@@ -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
|