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

Class Method Details

.calculate_specificityObject

.expand_shorthandObject

.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

Parameters:

  • css_string (String)

    CSS to parse

  • parser_options (Hash)

    Parser options (optional, defaults to {})

Returns:

  • (Hash)

    { rules: [...], media_index: ..., charset: "..." }



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

Parameters:

  • declarations_string (String)

    CSS declarations like "color: red; margin: 10px"

Returns:

  • (Array<Declaration>)

    Array of parsed declaration structs



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);
}