Module: Cataract::Backends::Native
- Defined in:
- ext/cataract/cataract.c
Constant Summary collapse
- COMPILE_FLAGS =
compile_flags- NATIVE_EXTENSION_LOADED =
Flag to indicate the native backend is loaded (for pure Ruby fallback detection)
Qtrue- IMPLEMENTATION =
Implementation type constant
ID2SYM(rb_intern("native"))
Class Method Summary collapse
- .calculate_specificity ⇒ Object
- .expand_shorthand ⇒ Object
-
.flatten(input) ⇒ Object
Output: Stylesheet with flattened declarations (cascade applied).
-
.parse(*args) ⇒ Hash
Parse CSS and return hash with parsed data This matches the old parse_css API.
-
.parse_declarations(declarations_string) ⇒ Array<Declaration>
Ruby-facing wrapper for new_parse_declarations.
-
.stylesheet_to_formatted_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
Formatted version with indentation and newlines (with nesting support).
-
.stylesheet_to_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
New stylesheet serialization entry point - checks for nesting and delegates.
Class Method Details
.calculate_specificity ⇒ Object
.expand_shorthand ⇒ Object
.flatten(input) ⇒ Object
Output: Stylesheet with flattened declarations (cascade applied)
1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 |
# File 'ext/cataract/flatten.c', line 1187
VALUE cataract_flatten(VALUE self, VALUE input) {
VALUE rules_array;
// Handle different input types
// Most calls pass Stylesheet (common case), String is rare
if (TYPE(input) == T_STRING) {
// Parse CSS string first
VALUE argv[1] = { input };
VALUE parsed = parse_css_new(1, argv, self);
rules_array = rb_hash_aref(parsed, ID2SYM(rb_intern("rules")));
} else if (rb_obj_is_kind_of(input, cStylesheet)) {
// Extract @rules from Stylesheet (common case)
rules_array = rb_ivar_get(input, id_ivar_rules);
} else {
rb_raise(rb_eTypeError, "Expected Stylesheet or String, got %s",
rb_obj_classname(input));
}
Check_Type(rules_array, T_ARRAY);
long num_rules = RARRAY_LEN(rules_array);
// Empty stylesheets are rare
if (num_rules == 0) {
// Return empty stylesheet
VALUE empty_sheet = rb_class_new_instance(0, NULL, cStylesheet);
return empty_sheet;
}
/*
* ============================================================================
* FLATTEN ALGORITHM - Rules and Implementation Notes
* ============================================================================
*
* CORE PRINCIPLE: Group rules by selector, flatten declarations within each group
*
* Different selectors (.test vs #test) target different elements and must stay separate.
* Same selectors should flatten into one rule to reduce output size.
*
* ALGORITHM STEPS:
* 1. Group rules by selector (.test, #test, etc.)
* 2. For each selector group:
* a. Expand shorthand properties (margin, background, font, etc.)
* b. Apply CSS cascade rules to resolve conflicts
* c. Recreate shorthand properties where beneficial
* 3. Output one rule per unique selector
*
* CSS CASCADE RULES (in order of precedence):
* 1. !important declarations always win over non-!important
* 2. Higher specificity wins (#id > .class > element)
* 3. Later source order wins (for same importance + specificity)
*
* SOURCE ORDER CALCULATION:
* source_order = rule_id * 1000 + declaration_index
* This ensures declarations within the same rule maintain relative order.
*
* SHORTHAND EXPANSION:
* When flattening, all shorthands must be expanded to longhands first.
* Example: "background: blue" expands to:
* - background-color: blue
* - background-image: none
* - background-repeat: repeat
* - background-position: 0% 0%
* - background-attachment: scroll
*
* This is REQUIRED because partial overrides must work correctly:
* .test { background: blue; }
* .test { background-image: url(x.png); }
* Should result in: blue background with image (not image reset to none)
*
* SHORTHAND RECREATION:
* After cascade resolution, recreate shorthands for smaller output:
* - margin-top: 10px, margin-right: 10px, ... → margin: 10px
* - background-color: blue, background-image: none, ... → background: blue
*
* Optimization: Omit default values ONLY when all properties are present
* (indicating they came from shorthand expansion, not explicit longhands)
*
* If only some properties present (explicit longhands), include all values:
* background-color: black, background-image: none → "black none"
* Not: "black" (user explicitly set image to none)
*
* If all properties present (from expansion), omit defaults:
* background-color: blue, background-image: none, repeat: repeat, ... → "blue"
* (The "none", "repeat", etc. are just defaults from expansion)
*
* EDGE CASES:
* - Empty rules (no declarations): Skip during flatten
* - Nested CSS: Parent rules with children are containers only, skip their declarations
* - Mixed !important: Properties with different importance cannot flatten into shorthand
* - Single property: Don't create shorthand (e.g., background-color alone stays as-is)
* Reason: "background: blue" resets all other background properties to defaults,
* which is semantically different from just setting background-color.
*
* PERFORMANCE NOTES:
* - Use cached static strings (VALUE) for property names (no allocation)
* - Group by selector in single pass (O(n) hash building)
* - Flatten within groups (O(n*m) where m is avg declarations per rule)
* ============================================================================
*/
// Build rule_media_map: rule_id => media_query_id
// This is used to group rules by (selector, media) instead of just selector
VALUE input_media_index = Qnil;
VALUE rule_media_map = build_rule_media_map(input, rules_array, num_rules, &input_media_index);
// Group rules by (selector, media) instead of just selector
// Rules with same selector but different media contexts should NOT be merged
DEBUG_PRINTF("\n=== Building selector+media groups ===\n");
VALUE passthrough_rules = rb_ary_new(); // AtRules to pass through unchanged
VALUE selector_groups = group_rules_by_selector_and_media(rules_array, num_rules, rule_media_map, passthrough_rules);
DEBUG_PRINTF("=== Total selector+media groups: %ld ===\n\n", RHASH_SIZE(selector_groups));
// ALWAYS group by selector and keep them separate
// Different selectors target different elements and must remain distinct
// Example: .test { color: red; } #test { color: blue; }
// Should return 2 rules (not merged into one)
DEBUG_PRINTF("=== DECISION POINT ===\n");
DEBUG_PRINTF(" selector_groups size: %ld\n", RHASH_SIZE(selector_groups));
if (RHASH_SIZE(selector_groups) == 0 && RARRAY_LEN(passthrough_rules) == 0) {
DEBUG_PRINTF(" -> No rules to merge (all were empty or skipped)\n");
// Return empty stylesheet
VALUE empty_sheet = rb_class_new_instance(0, NULL, cStylesheet);
return empty_sheet;
}
// Handle case where we only have passthrough rules (no regular rules to merge)
if (RHASH_SIZE(selector_groups) == 0 && RARRAY_LEN(passthrough_rules) > 0) {
DEBUG_PRINTF(" -> Only passthrough rules (no regular rules to merge)\n");
VALUE passthrough_sheet = rb_class_new_instance(0, NULL, cStylesheet);
rb_ivar_set(passthrough_sheet, id_ivar_rules, passthrough_rules);
// Set empty @media_index (no media rules after flatten)
VALUE media_idx = rb_hash_new();
rb_ivar_set(passthrough_sheet, id_ivar_media_index, media_idx);
// Copy @media_queries and @_media_query_lists from input
if (rb_obj_is_kind_of(input, cStylesheet)) {
VALUE media_queries = rb_ivar_get(input, rb_intern("@media_queries"));
VALUE media_query_lists = rb_ivar_get(input, rb_intern("@_media_query_lists"));
Check_Type(media_queries, T_ARRAY);
if (!NIL_P(media_query_lists)) Check_Type(media_query_lists, T_HASH);
rb_ivar_set(passthrough_sheet, rb_intern("@media_queries"), media_queries);
rb_ivar_set(passthrough_sheet, rb_intern("@_media_query_lists"), media_query_lists);
}
return passthrough_sheet;
} else {
DEBUG_PRINTF(" -> Taking SELECTOR-GROUPED path (%ld unique selectors)\n",
RHASH_SIZE(selector_groups));
VALUE merged_sheet = rb_class_new_instance(0, NULL, cStylesheet);
VALUE merged_rules = rb_ary_new();
int rule_id_counter = 0;
// Iterate through each selector group using rb_hash_foreach
// to avoid rb_funcall in hot path
VALUE old_to_new_id = rb_hash_new(); // Hash to track old rule IDs -> new merged rule IDs
struct flatten_selectors_context merge_ctx;
merge_ctx.merged_rules = merged_rules;
merge_ctx.rules_array = rules_array;
merge_ctx.rule_id_counter = &rule_id_counter;
merge_ctx.selector_index = 0;
merge_ctx.total_selectors = RHASH_SIZE(selector_groups);
merge_ctx.old_to_new_id = old_to_new_id;
DEBUG_PRINTF("\n=== Processing %ld selector groups ===\n", merge_ctx.total_selectors);
rb_hash_foreach(selector_groups, flatten_selector_group_callback, (VALUE)&merge_ctx);
// Add passthrough AtRules to output (preserve @keyframes, @font-face, etc.)
long num_passthrough = RARRAY_LEN(passthrough_rules);
for (long i = 0; i < num_passthrough; i++) {
VALUE at_rule = RARRAY_AREF(passthrough_rules, i);
// Update AtRule's id to maintain sequential IDs
rb_struct_aset(at_rule, INT2FIX(AT_RULE_ID), INT2FIX(rule_id_counter++));
rb_ary_push(merged_rules, at_rule);
DEBUG_PRINTF(" -> Added passthrough AtRule (new id=%d)\n", rule_id_counter - 1);
}
DEBUG_PRINTF("\n=== Created %d output rules (%ld passthrough) ===\n",
rule_id_counter, num_passthrough);
rb_ivar_set(merged_sheet, id_ivar_rules, merged_rules);
// Handle selector list divergence: remove rules from selector lists if declarations no longer match
// This makes selector_list_id authoritative - if set, declarations MUST be identical
// Only process if selector_lists is enabled in the stylesheet's parser options
VALUE selector_lists = rb_hash_new();
int selector_lists_enabled = 0;
if (rb_obj_is_kind_of(input, cStylesheet)) {
VALUE parser_options = rb_ivar_get(input, rb_intern("@parser_options"));
if (!NIL_P(parser_options)) {
VALUE enabled_val = rb_hash_aref(parser_options, ID2SYM(rb_intern("selector_lists")));
selector_lists_enabled = RTEST(enabled_val);
if (selector_lists_enabled) {
update_selector_lists_for_divergence(merged_rules, selector_lists);
} else {
// Clear all selector_list_ids when feature is disabled
for (long i = 0; i < rule_id_counter; i++) {
VALUE rule = RARRAY_AREF(merged_rules, i);
if (!rb_obj_is_kind_of(rule, cAtRule)) {
rb_struct_aset(rule, INT2FIX(RULE_SELECTOR_LIST_ID), Qnil);
}
}
}
} else {
// Default behavior when parser_options is nil: assume enabled
update_selector_lists_for_divergence(merged_rules, selector_lists);
}
}
// Preserve media_index by remapping old rule IDs to new rule IDs
// This is important for @media rules and @import statements with media constraints
VALUE new_media_index = rb_hash_new();
if (!NIL_P(input_media_index) && TYPE(input_media_index) == T_HASH) {
struct remap_media_context remap_ctx;
remap_ctx.old_to_new_id = old_to_new_id;
remap_ctx.new_media_index = new_media_index;
rb_hash_foreach(input_media_index, remap_media_index_callback, (VALUE)&remap_ctx);
}
rb_ivar_set(merged_sheet, id_ivar_media_index, new_media_index);
// Copy @media_queries and @_media_query_lists from input (these don't change during flatten)
if (rb_obj_is_kind_of(input, cStylesheet)) {
VALUE media_queries = rb_ivar_get(input, rb_intern("@media_queries"));
VALUE media_query_lists = rb_ivar_get(input, rb_intern("@_media_query_lists"));
Check_Type(media_queries, T_ARRAY);
if (!NIL_P(media_query_lists)) Check_Type(media_query_lists, T_HASH);
rb_ivar_set(merged_sheet, rb_intern("@media_queries"), media_queries);
rb_ivar_set(merged_sheet, rb_intern("@_media_query_lists"), media_query_lists);
}
// Set @_selector_lists with divergence tracking
rb_ivar_set(merged_sheet, rb_intern("@_selector_lists"), selector_lists);
// Protect intermediate VALUEs from being collected
RB_GC_GUARD(input_media_index);
RB_GC_GUARD(rule_media_map);
RB_GC_GUARD(selector_groups);
RB_GC_GUARD(passthrough_rules);
RB_GC_GUARD(old_to_new_id);
RB_GC_GUARD(new_media_index);
return merged_sheet;
}
}
|
.parse(*args) ⇒ Hash
Parse CSS and return hash with parsed data This matches the old parse_css API
112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'ext/cataract/cataract.c', line 112
VALUE parse_css_new(int argc, VALUE *argv, VALUE self) {
VALUE css_string, parser_options;
// Parse arguments: required css_string, optional parser_options hash
rb_scan_args(argc, argv, "11", &css_string, &parser_options);
// Default to empty hash if not provided
if (NIL_P(parser_options)) {
parser_options = rb_hash_new();
}
return parse_css_new_impl(css_string, parser_options, 0);
}
|
.parse_declarations(declarations_string) ⇒ Array<Declaration>
Ruby-facing wrapper for new_parse_declarations
1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 |
# File 'ext/cataract/cataract.c', line 1126
static VALUE new_parse_declarations(VALUE self, VALUE declarations_string) {
Check_Type(declarations_string, T_STRING);
const char *input = RSTRING_PTR(declarations_string);
long input_len = RSTRING_LEN(declarations_string);
// Strip outer braces and whitespace (css_parser compatibility)
const char *start = input;
const char *end = input + input_len;
while (start < end && (IS_WHITESPACE(*start) || *start == '{')) start++;
while (end > start && (IS_WHITESPACE(*(end-1)) || *(end-1) == '}')) end--;
VALUE result = new_parse_declarations_string(start, end);
RB_GC_GUARD(result);
return result;
}
|
.stylesheet_to_formatted_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
Formatted version with indentation and newlines (with nesting support)
1065 1066 1067 |
# File 'ext/cataract/cataract.c', line 1065
static VALUE stylesheet_to_formatted_s(VALUE self, VALUE rules_array, VALUE charset, VALUE has_nesting, VALUE selector_lists, VALUE media_queries, VALUE media_query_lists) {
return stylesheet_serialize_impl(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists, 1);
}
|
.stylesheet_to_s(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists) ⇒ Object
New stylesheet serialization entry point - checks for nesting and delegates
1060 1061 1062 |
# File 'ext/cataract/cataract.c', line 1060
static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE has_nesting, VALUE selector_lists, VALUE media_queries, VALUE media_query_lists) {
return stylesheet_serialize_impl(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists, 0);
}
|