view release on metacpan or search on metacpan
bench/vs_crayon.pl
include/litavis.h
include/litavis_ast.h
include/litavis_cascade.h
include/litavis_colour.h
include/litavis_emitter.h
include/litavis_parser.h
include/litavis_tokeniser.h
include/litavis_vars.h
lib/Litavis.pm
Litavis.bs
Litavis.c
Litavis.o
Litavis.xs
Makefile.PL
MANIFEST This list of files
ppport.h
t/00-load.t
t/01-ast.t
t/02-parser.t
t/03-cascade.t
t/04-variables.t
t/05-compiler.t
t/06-colour.t
t/07-integration.t
t/08-crayon-compat.t
t/09-edge-cases.t
t/10-regression.t
t/11-directory.t
t/12-error-handling.t
t/13-config-combos.t
Makefile.PL view on Meta::CPAN
INC => "-I. -Iinclude -I$colour_inc",
OBJECT => '$(O_FILES)',
# Install headers so downstream XS modules can #include them
PM => {
'lib/Litavis.pm' => '$(INST_LIB)/Litavis.pm',
'include/litavis.h' => '$(INST_LIB)/Litavis/include/litavis.h',
'include/litavis_ast.h' => '$(INST_LIB)/Litavis/include/litavis_ast.h',
'include/litavis_tokeniser.h' => '$(INST_LIB)/Litavis/include/litavis_tokeniser.h',
'include/litavis_parser.h' => '$(INST_LIB)/Litavis/include/litavis_parser.h',
'include/litavis_cascade.h' => '$(INST_LIB)/Litavis/include/litavis_cascade.h',
'include/litavis_vars.h' => '$(INST_LIB)/Litavis/include/litavis_vars.h',
'include/litavis_emitter.h' => '$(INST_LIB)/Litavis/include/litavis_emitter.h',
'include/litavis_colour.h' => '$(INST_LIB)/Litavis/include/litavis_colour.h',
},
dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz' },
clean => { FILES => 'Litavis-*' },
);
# Compatibility with old versions of ExtUtils::MakeMaker
include/litavis.h view on Meta::CPAN
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
/* Route fatal errors through Perl's croak() */
#define LITAVIS_FATAL(msg) croak("litavis: %s", (msg))
/* Pull in the C engine â order-independent headers first */
#include "litavis_ast.h"
#include "litavis_tokeniser.h"
#include "litavis_cascade.h"
#include "litavis_vars.h"
#include "litavis_colour.h"
#define LITAVIS_VERSION "0.01"
/* ââ Top-level context â holds all state for one Litavis instance ââ */
typedef struct {
LitavisAST *ast; /* accumulated parsed rules */
LitavisVarScope *global_scope; /* preprocessor variable scope */
include/litavis_cascade.h view on Meta::CPAN
#ifndef LITAVIS_CASCADE_H
#define LITAVIS_CASCADE_H
/* ââ Dedup strategy enum ââââââââââââââââââââââââââââââââââââ */
typedef enum {
LITAVIS_DEDUPE_OFF = 0, /* no deduplication */
LITAVIS_DEDUPE_CONSERVATIVE = 1, /* merge only when provably safe */
LITAVIS_DEDUPE_AGGRESSIVE = 2 /* merge all identical, ignore cascade */
} LitavisDedupeStrategy;
/* ââ Comparison helpers âââââââââââââââââââââââââââââââââââââ */
/*
* Deep compare two rules' properties.
* Returns 1 if same keys and values in same order, 0 otherwise.
*/
static int litavis_props_equal(LitavisRule *a, LitavisRule *b) {
int i;
include/litavis_cascade.h view on Meta::CPAN
* Merge rules that share the same selector.
* Later properties overwrite earlier ones (matches browser behaviour).
* Always runs regardless of strategy â this is never wrong.
*/
static void litavis_merge_same_selectors(LitavisAST *ast) {
int i, j;
for (i = 0; i < ast->count; i++) {
for (j = i + 1; j < ast->count; ) {
if (strcmp(ast->rules[i].selector, ast->rules[j].selector) == 0) {
/* Merge j's properties into i. For conflicts, j wins
* (later in cascade). Then remove j. */
litavis_rule_merge_props(&ast->rules[i], &ast->rules[j]);
litavis_ast_remove_rule(ast, j);
/* Don't increment j â re-check this index */
} else {
j++;
}
}
}
}
include/litavis_cascade.h view on Meta::CPAN
}
if (!merged) i++;
}
}
/* ââ Aggressive dedup âââââââââââââââââââââââââââââââââââââââ */
/*
* Merge ALL rules with identical properties regardless of position.
* User accepts cascade reordering. Good for atomic/utility CSS.
*/
static void litavis_dedupe_aggressive(LitavisAST *ast) {
int i = 0;
while (i < ast->count) {
int j, merged;
if (ast->rules[i].is_at_rule) {
i++;
continue;
}
lib/Litavis.pm view on Meta::CPAN
=head2 Compilation Pipeline
Each call to C<compile> processes the full accumulated AST through these
stages, all in C:
1. Flatten nested selectors
2. Resolve preprocessor variables, mixins, and map variables
3. Evaluate colour functions (lighten, darken, etc.)
4. Merge rules with the same selector (later properties win)
5. Deduplicate rules with identical properties (cascade-aware)
6. Emit CSS string (minified or pretty-printed)
C<compile> is non-destructive; calling it multiple times returns the same
result. Use C<reset> to clear state between independent compilations.
=head1 METHODS
=head2 new
my $l = Litavis->new(%options);
lib/Litavis.pm view on Meta::CPAN
=item B<pretty> => 0 | 1
Output mode. C<0> (default) produces minified CSS with no whitespace.
C<1> produces human-readable output with indentation and newlines.
=item B<dedupe> => 0 | 1 | 2
Deduplication strategy. C<0> disables deduplication entirely. C<1> (default)
uses conservative mode which only merges rules when no intervening rule
defines a conflicting property. C<2> uses aggressive mode which merges all
rules with identical properties regardless of cascade position.
=item B<indent> => $string
Indent string for pretty mode. Default is two spaces C<" ">. Common
alternative is C<"\t">.
=item B<shorthand_hex> => 0 | 1
Hex colour shorthand. C<1> (default) converts C<#aabbcc> to C<#abc> when
possible. C<0> preserves the original form.
lib/Litavis.pm view on Meta::CPAN
=head2 Cascade-Aware Deduplication
# Conservative mode (default) â safe merging only
my $css = Litavis->new->parse('
.reset { color: black; margin: 0; }
.theme { color: red; }
.footer { color: black; margin: 0; }
')->compile;
# .reset and .footer are NOT merged because .theme
# defines "color" which conflicts â merging would
# reorder the cascade.
# Aggressive mode â merge all identical, ignore cascade
my $css = Litavis->new(dedupe => 2)->parse('
.a { padding: 8px; }
.b { color: red; }
.c { padding: 8px; }
')->compile;
# .a,.c{padding:8px;}.b{color:red;}
=head2 Pretty-Printed Output
my $css = Litavis->new(pretty => 1, indent => " ")->parse('
lib/Litavis.pm view on Meta::CPAN
=head1 C HEADER FILES
The entire engine is implemented in standalone C header files that can be
reused by other XS modules:
litavis.h Master include (context struct, lifecycle)
litavis_ast.h Ordered AST with hash index
litavis_tokeniser.h Single-pass CSS tokeniser
litavis_parser.h Recursive descent parser with selector flattening
litavis_cascade.h Cascade-aware deduplication
litavis_vars.h Variable, mixin, and map resolution
litavis_colour.h Colour function evaluation (uses colouring.h)
litavis_emitter.h CSS output (minified and pretty-printed)
=head1 DEPENDENCIES
=over 4
=item * L<Colouring::In::XS> - C headers for colour manipulation
t/03-cascade.t view on Meta::CPAN
# ââ Conservative: DO NOT merge when intervening conflict âââââ
{
my $d = Litavis->new;
$d->parse('
.reset { color: black; padding: 8px; }
.theme { color: red; }
.override { color: black; padding: 8px; }
');
$d->_dedupe(DEDUPE_CONSERVATIVE);
is($d->_ast_rule_count, 3, 'conservative cascade: no merge (intervening color conflict)');
is($d->_ast_rule_selector(0), '.reset', 'conservative cascade: .reset stays at 0');
is($d->_ast_rule_selector(1), '.theme', 'conservative cascade: .theme stays at 1');
is($d->_ast_rule_selector(2), '.override', 'conservative cascade: .override stays at 2');
}
# ââ Conservative: partial conflict blocks merge ââââââââââââââ
{
my $d = Litavis->new;
$d->parse('
.a { color: red; margin: 10px; }
.b { margin: 20px; }
.c { color: red; margin: 10px; }
t/03-cascade.t view on Meta::CPAN
.second { color: blue; }
.third { color: green; }
');
$d->_dedupe(DEDUPE_CONSERVATIVE);
is($d->_ast_rule_count, 3, 'order preserved: all different');
is($d->_ast_rule_selector(0), '.first', 'order preserved: 0');
is($d->_ast_rule_selector(1), '.second', 'order preserved: 1');
is($d->_ast_rule_selector(2), '.third', 'order preserved: 2');
}
# ââ The cascade problem from the plan ââââââââââââââââââââââââ
{
my $d = Litavis->new;
$d->parse('
.btn { background: grey; color: white; padding: 8px; }
.primary { background: blue; color: white; padding: 8px; }
.btn { background: grey; color: white; padding: 8px; }
');
$d->_dedupe(DEDUPE_CONSERVATIVE);
# Same-selector merge collapses the two .btn into one
# But .btn and .primary share properties, so conservative dedup
# should NOT merge them
is($d->_ast_rule_count, 2, 'cascade problem: .btn merged with itself');
is($d->_ast_rule_selector(0), '.btn', 'cascade problem: .btn first');
is($d->_ast_rule_selector(1), '.primary', 'cascade problem: .primary second');
}
done_testing;
t/10-regression.t view on Meta::CPAN
my $d = Litavis->new(%opts);
$d->parse($css);
return $d->compile();
}
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# Regression: Crayon dedup bug â hash modification during iteration
#
# Crayon's _dedupe_struct modified the hash it was iterating over,
# causing some identical selectors to NOT be merged, and others
# to be incorrectly merged when cascade position mattered.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
{
my $css = compile_css('.z { color: red; } .a { color: red; } .m { color: red; }');
# All three have identical props and no intervening conflicts â merge
like($css, qr/\.z/, 'hash iter bug: .z present');
like($css, qr/\.a/, 'hash iter bug: .a present');
like($css, qr/\.m/, 'hash iter bug: .m present');
# Should be merged into one rule
my @rules = ($css =~ /\{/g);
t/10-regression.t view on Meta::CPAN
', dedupe => 0);
# Must be in insertion order, NOT alphabetical
my ($z_pos) = ($css =~ /\.zebra/g) ? ($-[0]) : (-1);
my ($a_pos) = ($css =~ /\.apple/g) ? ($-[0]) : (-1);
my ($m_pos) = ($css =~ /\.mango/g) ? ($-[0]) : (-1);
ok($z_pos < $a_pos, 'order regression: .zebra before .apple');
ok($a_pos < $m_pos, 'order regression: .apple before .mango');
}
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# Regression: Cascade reordering â merging loses cascade position
#
# .reset { color: black; }
# .theme { color: red; }
# .override { color: black; }
#
# Merging .reset and .override would place .override before .theme,
# changing the cascade result for any element with both classes.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
{
my $css = compile_css('
.reset { color: black; }
.theme { color: red; }
.override { color: black; }
');
# .override must come AFTER .theme in the output
my ($theme_pos) = ($css =~ /\.theme/g) ? ($-[0]) : (-1);
my ($override_pos) = ($css =~ /\.override/g) ? ($-[0]) : (-1);
ok($override_pos > $theme_pos,
'cascade regression: .override stays after .theme');
}
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# Regression: Load order preserved across multiple parse calls
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
{
my $d = Litavis->new(dedupe => 0);
$d->parse('.first { color: red; }');
$d->parse('.second { color: blue; }');
t/10-regression.t view on Meta::CPAN
push @found_order, $1;
}
is_deeply(\@found_order, \@expected_order,
'large file regression: 200 selectors in correct order');
}
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# Regression: Same-selector merge must preserve later values
#
# If .btn appears twice with different color values, the later
# one must win (cascade rules).
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
{
my $css = compile_css('
.btn { color: red; padding: 8px; }
.btn { color: blue; margin: 4px; }
');
like($css, qr/color:blue/, 'same-sel regression: later color wins');
like($css, qr/padding:8px/, 'same-sel regression: first-only prop kept');
like($css, qr/margin:4px/, 'same-sel regression: second-only prop added');