tina4ruby 3.11.13 → 3.11.15
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 +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
// Tina4 CSS Framework - CSS Custom Properties
|
|
2
|
-
// Exposes the design tokens as CSS custom properties for runtime use
|
|
3
|
-
|
|
4
|
-
@import 'variables';
|
|
5
|
-
|
|
6
|
-
:root {
|
|
7
|
-
// Theme colors
|
|
8
|
-
--t4-primary: #{$primary};
|
|
9
|
-
--t4-secondary: #{$secondary};
|
|
10
|
-
--t4-success: #{$success};
|
|
11
|
-
--t4-danger: #{$danger};
|
|
12
|
-
--t4-warning: #{$warning};
|
|
13
|
-
--t4-info: #{$info};
|
|
14
|
-
--t4-light: #{$light};
|
|
15
|
-
--t4-dark: #{$dark};
|
|
16
|
-
--t4-white: #{$white};
|
|
17
|
-
--t4-black: #{$black};
|
|
18
|
-
--t4-muted: #{$muted};
|
|
19
|
-
|
|
20
|
-
// Body
|
|
21
|
-
--t4-body-bg: #{$body-bg};
|
|
22
|
-
--t4-body-color: #{$body-color};
|
|
23
|
-
|
|
24
|
-
// Typography
|
|
25
|
-
--t4-font-family: #{$font-family-base};
|
|
26
|
-
--t4-font-family-mono: #{$font-family-mono};
|
|
27
|
-
--t4-font-size-base: #{$font-size-base};
|
|
28
|
-
--t4-line-height-base: #{$line-height-base};
|
|
29
|
-
|
|
30
|
-
// Spacing
|
|
31
|
-
--t4-spacer: #{$spacer};
|
|
32
|
-
|
|
33
|
-
// Border radius
|
|
34
|
-
--t4-border-radius: #{$border-radius};
|
|
35
|
-
--t4-border-radius-lg: #{$border-radius-lg};
|
|
36
|
-
--t4-border-radius-xl: #{$border-radius-xl};
|
|
37
|
-
|
|
38
|
-
// Shadows
|
|
39
|
-
--t4-shadow-sm: #{$shadow-sm};
|
|
40
|
-
--t4-shadow: #{$shadow};
|
|
41
|
-
--t4-shadow-lg: #{$shadow-lg};
|
|
42
|
-
|
|
43
|
-
// Transitions
|
|
44
|
-
--t4-transition: #{$transition-base};
|
|
45
|
-
|
|
46
|
-
// Link
|
|
47
|
-
--t4-link-color: #{$link-color};
|
|
48
|
-
}
|
|
1
|
+
// Tina4 CSS Framework - CSS Custom Properties
|
|
2
|
+
// Exposes the design tokens as CSS custom properties for runtime use
|
|
3
|
+
|
|
4
|
+
@import 'variables';
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
// Theme colors
|
|
8
|
+
--t4-primary: #{$primary};
|
|
9
|
+
--t4-secondary: #{$secondary};
|
|
10
|
+
--t4-success: #{$success};
|
|
11
|
+
--t4-danger: #{$danger};
|
|
12
|
+
--t4-warning: #{$warning};
|
|
13
|
+
--t4-info: #{$info};
|
|
14
|
+
--t4-light: #{$light};
|
|
15
|
+
--t4-dark: #{$dark};
|
|
16
|
+
--t4-white: #{$white};
|
|
17
|
+
--t4-black: #{$black};
|
|
18
|
+
--t4-muted: #{$muted};
|
|
19
|
+
|
|
20
|
+
// Body
|
|
21
|
+
--t4-body-bg: #{$body-bg};
|
|
22
|
+
--t4-body-color: #{$body-color};
|
|
23
|
+
|
|
24
|
+
// Typography
|
|
25
|
+
--t4-font-family: #{$font-family-base};
|
|
26
|
+
--t4-font-family-mono: #{$font-family-mono};
|
|
27
|
+
--t4-font-size-base: #{$font-size-base};
|
|
28
|
+
--t4-line-height-base: #{$line-height-base};
|
|
29
|
+
|
|
30
|
+
// Spacing
|
|
31
|
+
--t4-spacer: #{$spacer};
|
|
32
|
+
|
|
33
|
+
// Border radius
|
|
34
|
+
--t4-border-radius: #{$border-radius};
|
|
35
|
+
--t4-border-radius-lg: #{$border-radius-lg};
|
|
36
|
+
--t4-border-radius-xl: #{$border-radius-xl};
|
|
37
|
+
|
|
38
|
+
// Shadows
|
|
39
|
+
--t4-shadow-sm: #{$shadow-sm};
|
|
40
|
+
--t4-shadow: #{$shadow};
|
|
41
|
+
--t4-shadow-lg: #{$shadow-lg};
|
|
42
|
+
|
|
43
|
+
// Transitions
|
|
44
|
+
--t4-transition: #{$transition-base};
|
|
45
|
+
|
|
46
|
+
// Link
|
|
47
|
+
--t4-link-color: #{$link-color};
|
|
48
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// Tina4 CSS Framework
|
|
2
|
-
// A lightweight, modern, responsive CSS framework
|
|
3
|
-
// ================================================
|
|
4
|
-
|
|
5
|
-
@import 'variables';
|
|
6
|
-
@import 'reset';
|
|
7
|
-
@import 'typography';
|
|
8
|
-
@import 'grid';
|
|
9
|
-
@import 'buttons';
|
|
10
|
-
@import 'forms';
|
|
11
|
-
@import 'cards';
|
|
12
|
-
@import 'nav';
|
|
13
|
-
@import 'modals';
|
|
14
|
-
@import 'alerts';
|
|
15
|
-
@import 'tables';
|
|
16
|
-
@import 'badges';
|
|
17
|
-
@import 'utilities';
|
|
1
|
+
// Tina4 CSS Framework
|
|
2
|
+
// A lightweight, modern, responsive CSS framework
|
|
3
|
+
// ================================================
|
|
4
|
+
|
|
5
|
+
@import 'variables';
|
|
6
|
+
@import 'reset';
|
|
7
|
+
@import 'typography';
|
|
8
|
+
@import 'grid';
|
|
9
|
+
@import 'buttons';
|
|
10
|
+
@import 'forms';
|
|
11
|
+
@import 'cards';
|
|
12
|
+
@import 'nav';
|
|
13
|
+
@import 'modals';
|
|
14
|
+
@import 'alerts';
|
|
15
|
+
@import 'tables';
|
|
16
|
+
@import 'badges';
|
|
17
|
+
@import 'utilities';
|
data/lib/tina4/scss_compiler.rb
CHANGED
|
@@ -1,178 +1,178 @@
|
|
|
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
|
-
# Module-level state for import paths and variables
|
|
10
|
-
@import_paths = []
|
|
11
|
-
@variables = {}
|
|
12
|
-
|
|
13
|
-
class << self
|
|
14
|
-
# Add a search path for @import resolution.
|
|
15
|
-
def add_import_path(path)
|
|
16
|
-
@import_paths ||= []
|
|
17
|
-
@import_paths << path
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Set a variable that will be available during compilation.
|
|
21
|
-
def set_variable(name, value)
|
|
22
|
-
@variables ||= {}
|
|
23
|
-
name = name.sub(/^\$/, "")
|
|
24
|
-
@variables[name] = value
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Compile an SCSS string to CSS.
|
|
28
|
-
def compile(source)
|
|
29
|
-
@variables ||= {}
|
|
30
|
-
@import_paths ||= []
|
|
31
|
-
# Inject preset variables
|
|
32
|
-
unless @variables.empty?
|
|
33
|
-
var_block = @variables.map { |k, v| "$#{k}: #{v};" }.join("\n")
|
|
34
|
-
source = "#{var_block}\n#{source}"
|
|
35
|
-
end
|
|
36
|
-
basic_compile(source, @import_paths.first || Dir.pwd)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Compile all .scss files from known directories.
|
|
40
|
-
def compile_all(root_dir = Dir.pwd)
|
|
41
|
-
output_dir = File.join(root_dir, CSS_OUTPUT)
|
|
42
|
-
FileUtils.mkdir_p(output_dir)
|
|
43
|
-
|
|
44
|
-
SCSS_DIRS.each do |dir|
|
|
45
|
-
scss_dir = File.join(root_dir, dir)
|
|
46
|
-
next unless Dir.exist?(scss_dir)
|
|
47
|
-
|
|
48
|
-
Dir.glob(File.join(scss_dir, "**/*.scss")).each do |scss_file|
|
|
49
|
-
next if File.basename(scss_file).start_with?("_") # Skip partials
|
|
50
|
-
compile_file(scss_file, output_dir, scss_dir)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Compile a single SCSS file. If output_dir is provided, writes CSS there.
|
|
56
|
-
# Always returns the compiled CSS string.
|
|
57
|
-
def compile_file(scss_file, output_dir = nil, base_dir = nil)
|
|
58
|
-
base_dir ||= File.dirname(scss_file)
|
|
59
|
-
scss_content = File.read(scss_file)
|
|
60
|
-
css_content = compile_scss(scss_content, File.dirname(scss_file))
|
|
61
|
-
|
|
62
|
-
if output_dir
|
|
63
|
-
relative = scss_file.sub(base_dir, "").sub(/\.scss$/, ".css")
|
|
64
|
-
css_file = File.join(output_dir, relative)
|
|
65
|
-
FileUtils.mkdir_p(File.dirname(css_file))
|
|
66
|
-
existing = File.exist?(css_file) ? File.read(css_file) : nil
|
|
67
|
-
if existing != css_content
|
|
68
|
-
File.write(css_file, css_content)
|
|
69
|
-
Tina4::Log.debug("Compiled SCSS: #{scss_file} -> #{css_file}")
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
css_content
|
|
74
|
-
rescue => e
|
|
75
|
-
Tina4::Log.error("SCSS compilation failed: #{scss_file} - #{e.message}")
|
|
76
|
-
""
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Compile an SCSS content string with a base directory for import resolution.
|
|
80
|
-
def compile_scss(content, base_dir)
|
|
81
|
-
# Try sassc gem first
|
|
82
|
-
begin
|
|
83
|
-
require "sassc"
|
|
84
|
-
return SassC::Engine.new(content, style: :expanded, load_paths: [base_dir]).render
|
|
85
|
-
rescue LoadError
|
|
86
|
-
# Fall through to basic compiler
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Basic SCSS to CSS conversion (handles common patterns)
|
|
90
|
-
basic_compile(content, base_dir)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
def basic_compile(content, base_dir)
|
|
96
|
-
# Handle @import
|
|
97
|
-
content = process_imports(content, base_dir)
|
|
98
|
-
|
|
99
|
-
# Handle variables
|
|
100
|
-
variables = {}
|
|
101
|
-
content = content.gsub(/\$([a-zA-Z_][\w-]*)\s*:\s*(.+?);/) do
|
|
102
|
-
variables[Regexp.last_match(1)] = Regexp.last_match(2).strip
|
|
103
|
-
""
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Replace variable references
|
|
107
|
-
variables.each do |name, value|
|
|
108
|
-
content = content.gsub("$#{name}", value)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Handle nesting (basic single-level)
|
|
112
|
-
content = flatten_nesting(content)
|
|
113
|
-
|
|
114
|
-
# Handle & parent selector
|
|
115
|
-
content = content.gsub(/&/, "")
|
|
116
|
-
|
|
117
|
-
content
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def process_imports(content, base_dir)
|
|
121
|
-
content.gsub(/@import\s+["'](.+?)["']\s*;/) do
|
|
122
|
-
import_path = Regexp.last_match(1)
|
|
123
|
-
candidates = [
|
|
124
|
-
File.join(base_dir, "#{import_path}.scss"),
|
|
125
|
-
File.join(base_dir, "_#{import_path}.scss"),
|
|
126
|
-
File.join(base_dir, import_path)
|
|
127
|
-
]
|
|
128
|
-
# Also search additional import paths
|
|
129
|
-
(@import_paths || []).each do |search_path|
|
|
130
|
-
candidates << File.join(search_path, "#{import_path}.scss")
|
|
131
|
-
candidates << File.join(search_path, "_#{import_path}.scss")
|
|
132
|
-
candidates << File.join(search_path, import_path)
|
|
133
|
-
end
|
|
134
|
-
found = candidates.find { |c| File.exist?(c) }
|
|
135
|
-
if found
|
|
136
|
-
imported = File.read(found)
|
|
137
|
-
process_imports(imported, File.dirname(found))
|
|
138
|
-
else
|
|
139
|
-
"/* import not found: #{import_path} */"
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def flatten_nesting(content)
|
|
145
|
-
# Very basic nesting flattener - handles single level
|
|
146
|
-
result = ""
|
|
147
|
-
content.scan(/([^{]+)\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/m) do |selector, body|
|
|
148
|
-
selector = selector.strip
|
|
149
|
-
# Check for nested rules
|
|
150
|
-
if body =~ /\{/
|
|
151
|
-
# Has nested content
|
|
152
|
-
props = ""
|
|
153
|
-
nested = ""
|
|
154
|
-
body.scan(/([^{;]+(?:\{[^}]*\}|;))/m) do |part_arr|
|
|
155
|
-
part = part_arr[0].strip
|
|
156
|
-
if part.include?("{")
|
|
157
|
-
# Nested rule
|
|
158
|
-
if part =~ /\A(.+?)\s*\{(.*?)\}\s*\z/m
|
|
159
|
-
nested_sel = Regexp.last_match(1).strip
|
|
160
|
-
nested_body = Regexp.last_match(2).strip
|
|
161
|
-
full_sel = "#{selector} #{nested_sel}"
|
|
162
|
-
nested += "#{full_sel} { #{nested_body} }\n"
|
|
163
|
-
end
|
|
164
|
-
else
|
|
165
|
-
props += " #{part}\n" unless part.empty?
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
result += "#{selector} {\n#{props}}\n" unless props.strip.empty?
|
|
169
|
-
result += nested
|
|
170
|
-
else
|
|
171
|
-
result += "#{selector} {\n#{body}\n}\n"
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
result.empty? ? content : result
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
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
|
+
# Module-level state for import paths and variables
|
|
10
|
+
@import_paths = []
|
|
11
|
+
@variables = {}
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Add a search path for @import resolution.
|
|
15
|
+
def add_import_path(path)
|
|
16
|
+
@import_paths ||= []
|
|
17
|
+
@import_paths << path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Set a variable that will be available during compilation.
|
|
21
|
+
def set_variable(name, value)
|
|
22
|
+
@variables ||= {}
|
|
23
|
+
name = name.sub(/^\$/, "")
|
|
24
|
+
@variables[name] = value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Compile an SCSS string to CSS.
|
|
28
|
+
def compile(source)
|
|
29
|
+
@variables ||= {}
|
|
30
|
+
@import_paths ||= []
|
|
31
|
+
# Inject preset variables
|
|
32
|
+
unless @variables.empty?
|
|
33
|
+
var_block = @variables.map { |k, v| "$#{k}: #{v};" }.join("\n")
|
|
34
|
+
source = "#{var_block}\n#{source}"
|
|
35
|
+
end
|
|
36
|
+
basic_compile(source, @import_paths.first || Dir.pwd)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Compile all .scss files from known directories.
|
|
40
|
+
def compile_all(root_dir = Dir.pwd)
|
|
41
|
+
output_dir = File.join(root_dir, CSS_OUTPUT)
|
|
42
|
+
FileUtils.mkdir_p(output_dir)
|
|
43
|
+
|
|
44
|
+
SCSS_DIRS.each do |dir|
|
|
45
|
+
scss_dir = File.join(root_dir, dir)
|
|
46
|
+
next unless Dir.exist?(scss_dir)
|
|
47
|
+
|
|
48
|
+
Dir.glob(File.join(scss_dir, "**/*.scss")).each do |scss_file|
|
|
49
|
+
next if File.basename(scss_file).start_with?("_") # Skip partials
|
|
50
|
+
compile_file(scss_file, output_dir, scss_dir)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Compile a single SCSS file. If output_dir is provided, writes CSS there.
|
|
56
|
+
# Always returns the compiled CSS string.
|
|
57
|
+
def compile_file(scss_file, output_dir = nil, base_dir = nil)
|
|
58
|
+
base_dir ||= File.dirname(scss_file)
|
|
59
|
+
scss_content = File.read(scss_file)
|
|
60
|
+
css_content = compile_scss(scss_content, File.dirname(scss_file))
|
|
61
|
+
|
|
62
|
+
if output_dir
|
|
63
|
+
relative = scss_file.sub(base_dir, "").sub(/\.scss$/, ".css")
|
|
64
|
+
css_file = File.join(output_dir, relative)
|
|
65
|
+
FileUtils.mkdir_p(File.dirname(css_file))
|
|
66
|
+
existing = File.exist?(css_file) ? File.read(css_file) : nil
|
|
67
|
+
if existing != css_content
|
|
68
|
+
File.write(css_file, css_content)
|
|
69
|
+
Tina4::Log.debug("Compiled SCSS: #{scss_file} -> #{css_file}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
css_content
|
|
74
|
+
rescue => e
|
|
75
|
+
Tina4::Log.error("SCSS compilation failed: #{scss_file} - #{e.message}")
|
|
76
|
+
""
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Compile an SCSS content string with a base directory for import resolution.
|
|
80
|
+
def compile_scss(content, base_dir)
|
|
81
|
+
# Try sassc gem first
|
|
82
|
+
begin
|
|
83
|
+
require "sassc"
|
|
84
|
+
return SassC::Engine.new(content, style: :expanded, load_paths: [base_dir]).render
|
|
85
|
+
rescue LoadError
|
|
86
|
+
# Fall through to basic compiler
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Basic SCSS to CSS conversion (handles common patterns)
|
|
90
|
+
basic_compile(content, base_dir)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def basic_compile(content, base_dir)
|
|
96
|
+
# Handle @import
|
|
97
|
+
content = process_imports(content, base_dir)
|
|
98
|
+
|
|
99
|
+
# Handle variables
|
|
100
|
+
variables = {}
|
|
101
|
+
content = content.gsub(/\$([a-zA-Z_][\w-]*)\s*:\s*(.+?);/) do
|
|
102
|
+
variables[Regexp.last_match(1)] = Regexp.last_match(2).strip
|
|
103
|
+
""
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Replace variable references
|
|
107
|
+
variables.each do |name, value|
|
|
108
|
+
content = content.gsub("$#{name}", value)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Handle nesting (basic single-level)
|
|
112
|
+
content = flatten_nesting(content)
|
|
113
|
+
|
|
114
|
+
# Handle & parent selector
|
|
115
|
+
content = content.gsub(/&/, "")
|
|
116
|
+
|
|
117
|
+
content
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_imports(content, base_dir)
|
|
121
|
+
content.gsub(/@import\s+["'](.+?)["']\s*;/) do
|
|
122
|
+
import_path = Regexp.last_match(1)
|
|
123
|
+
candidates = [
|
|
124
|
+
File.join(base_dir, "#{import_path}.scss"),
|
|
125
|
+
File.join(base_dir, "_#{import_path}.scss"),
|
|
126
|
+
File.join(base_dir, import_path)
|
|
127
|
+
]
|
|
128
|
+
# Also search additional import paths
|
|
129
|
+
(@import_paths || []).each do |search_path|
|
|
130
|
+
candidates << File.join(search_path, "#{import_path}.scss")
|
|
131
|
+
candidates << File.join(search_path, "_#{import_path}.scss")
|
|
132
|
+
candidates << File.join(search_path, import_path)
|
|
133
|
+
end
|
|
134
|
+
found = candidates.find { |c| File.exist?(c) }
|
|
135
|
+
if found
|
|
136
|
+
imported = File.read(found)
|
|
137
|
+
process_imports(imported, File.dirname(found))
|
|
138
|
+
else
|
|
139
|
+
"/* import not found: #{import_path} */"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def flatten_nesting(content)
|
|
145
|
+
# Very basic nesting flattener - handles single level
|
|
146
|
+
result = ""
|
|
147
|
+
content.scan(/([^{]+)\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/m) do |selector, body|
|
|
148
|
+
selector = selector.strip
|
|
149
|
+
# Check for nested rules
|
|
150
|
+
if body =~ /\{/
|
|
151
|
+
# Has nested content
|
|
152
|
+
props = ""
|
|
153
|
+
nested = ""
|
|
154
|
+
body.scan(/([^{;]+(?:\{[^}]*\}|;))/m) do |part_arr|
|
|
155
|
+
part = part_arr[0].strip
|
|
156
|
+
if part.include?("{")
|
|
157
|
+
# Nested rule
|
|
158
|
+
if part =~ /\A(.+?)\s*\{(.*?)\}\s*\z/m
|
|
159
|
+
nested_sel = Regexp.last_match(1).strip
|
|
160
|
+
nested_body = Regexp.last_match(2).strip
|
|
161
|
+
full_sel = "#{selector} #{nested_sel}"
|
|
162
|
+
nested += "#{full_sel} { #{nested_body} }\n"
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
props += " #{part}\n" unless part.empty?
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
result += "#{selector} {\n#{props}}\n" unless props.strip.empty?
|
|
169
|
+
result += nested
|
|
170
|
+
else
|
|
171
|
+
result += "#{selector} {\n#{body}\n}\n"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
result.empty? ? content : result
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|