diff --git a/lib/CSS/Specification/Build.rakumod b/lib/CSS/Specification/Build.rakumod index 3dbd493..dac8eb4 100755 --- a/lib/CSS/Specification/Build.rakumod +++ b/lib/CSS/Specification/Build.rakumod @@ -1,214 +1,249 @@ -unit module CSS::Specification::Build; +#!/usr/bin/env raku -use CSS::Specification; -use CSS::Specification::Actions; -use CSS::Specification::Compiler; -my subset Path where Str|IO::Path; -use experimental :rakuast; +#= translates w3c property definitions to basic Raku roles, grammars or actions. -#= generate parsing grammar -our proto sub generate(|) { * }; -multi sub generate('grammar', Str $grammar-name, Path :$input-path?) { +module CSS::Specification::Build { - my CSS::Specification::Actions $actions .= new; - my CSS::Specification::Compiler $compiler .= new: :$actions; - my @defs = $compiler.load-defs($input-path); + use CSS::Specification; + use CSS::Specification::Actions; + my subset Path where { ($_//Str) ~~ Str|IO::Path }; - say qq:to; - use v6; - # -- DO NOT EDIT -- - # generated by: {($*PROGRAM-NAME, @*ARGS.Slip).join: ' '}; + #= generate parsing grammar + our proto sub generate(Str $type, Str $name, Path :$input-path?) { * }; + multi sub generate('grammar', Str $grammar-name, Path :$input-path?) { - unit grammar {$grammar-name}; - END-HDR + my CSS::Specification::Actions $actions .= new; + my @defs = load-defs($input-path, $actions); - generate-raku-rules(@defs); -} + say qq:to; + use v6; + # -- DO NOT EDIT -- + # generated by: {($*PROGRAM-NAME, @*ARGS.Slip).join: ' '}; -#= generate actions class -multi sub generate('actions', Str $class-name, Path :$input-path?) { + unit grammar {$grammar-name}; + END-HDR - my CSS::Specification::Actions $actions .= new; - my CSS::Specification::Compiler $compiler .= new: :$actions; - my @defs = $compiler.load-defs($input-path); + generate-raku-rules(@defs); + } - say qq:to; - use v6; - # -- DO NOT EDIT -- - # generated by: {($*PROGRAM-NAME, @*ARGS.Slip).join: ' '} + #= generate actions class + multi sub generate('actions', Str $class-name, Path :$input-path?) { - unit class {$class-name}; - END-HDR + my CSS::Specification::Actions $actions .= new; + my @defs = load-defs($input-path, $actions); - my %prop-refs = $actions.prop-refs; - generate-raku-actions(@defs, %prop-refs); -} + say qq:to; + use v6; + # -- DO NOT EDIT -- + # generated by: {($*PROGRAM-NAME, @*ARGS.Slip).join: ' '} -multi sub generate('interface', $role-name, :$input-path? --> RakuAST::Package:D) is DEPRECATED { + unit class {$class-name}; + END-HDR - my CSS::Specification::Actions $actions .= new; - my CSS::Specification::Compiler $compiler .= new: :$actions; - my @role-id = $role-name.split('::'); - $compiler.load-defs($input-path); - $compiler.role-ast(@role-id); -} + my %prop-refs = $actions.prop-refs; + generate-raku-actions(@defs, %prop-refs); + } -#= generate interface roles. -multi sub generate('interface', @role-id, Path :$input-path? --> RakuAST::Package:D) { + #= generate interface roles. + multi sub generate('interface', Str $role-name, Path :$input-path?) { - my CSS::Specification::Actions $actions .= new; - my CSS::Specification::Compiler $compiler .= new: :$actions; - $compiler.load-defs($input-path); - $compiler.role-ast(@role-id); -} + my CSS::Specification::Actions $actions .= new; + my @defs = load-defs($input-path, $actions); -sub find-edges(%properties, %child-props) { - # match boxed properties with children - for %properties.pairs { - my $key = .key; - my $value = .value; - unless $key ~~ / top|right|bottom|left / { - # see if the property has any children - for -> $side { - # find child. could be xxxx-side (e.g. margin-left) - # or xxx-yyy-side (e.g. border-left-width); - for $key ~ '-' ~ $side, $key.subst("-", [~] '-', $side, '-') -> $edge { - if $edge ne $key - && (%properties{$edge}:exists) { - my $prop = %properties{$edge}; - $prop = $key; - $value.push: $edge; - $value ||= True; - last; + say qq:to; + use v6; + # -- DO NOT EDIT -- + # generated by: {($*PROGRAM-NAME, @*ARGS.Slip).join: ' '} + unit role {$role-name}; + END-HDR + + my %prop-refs = $actions.prop-refs; + my %props = $actions.props; + my %rules = $actions.rules; + generate-raku-interface(%prop-refs, %props, %rules); + } + + sub find-edges(%properties, %child-props) { + # match boxed properties with children + for %properties.pairs { + my $key = .key; + my $value = .value; + unless $key ~~ / top|right|bottom|left / { + # see if the property has any children + for -> $side { + # find child. could be xxxx-side (e.g. margin-left) + # or xxx-yyy-side (e.g. border-left-width); + for $key ~ '-' ~ $side, $key.subst("-", [~] '-', $side, '-') -> $edge { + if $edge ne $key + && (%properties{$edge}:exists) { + my $prop = %properties{$edge}; + $prop = $key; + $value.push: $edge; + $value ||= True; + last; + } } } } - } - with %child-props{$key} { - for .unique -> $child-prop { - next if $value && $value{$child-prop}; - my $prop = %properties{$child-prop}; - # property may have multiple parents - $value.push: $child-prop; + with %child-props{$key} { + for .unique -> $child-prop { + next if $value && $value{$child-prop}; + my $prop = %properties{$child-prop}; + # property may have multiple parents + $value.push: $child-prop; + } } + # we can get defaults from the children + $value:delete + if ($value:exists) + || ($value:exists); } - # we can get defaults from the children - $value:delete - if ($value:exists) - || ($value:exists); } -} -sub check-edges(%properties) { - for %properties.pairs { - my $key = .key; - my $value = .value; - my $edges = $value; + sub check-edges(%properties) { + for %properties.pairs { + my $key = .key; + my $value = .value; + my $edges = $value; - note "box property doesn't have four edges $key: $edges" - if $edges && +$edges != 4; + note "box property doesn't have four edges $key: $edges" + if $edges && +$edges != 4; - my $children = $value; - if $value && $children { - my $non-edges = $children.grep: { ! %properties{$_} }; - note "edge property $key has non-edge properties: $non-edges" - if $non-edges; + my $children = $value; + if $value && $children { + my $non-edges = $children.grep: { ! %properties{$_} }; + note "edge property $key has non-edge properties: $non-edges" + if $non-edges; + } } } -} - -our sub summary(Path :$input-path? ) { - - my CSS::Specification::Actions $actions .= new; - my CSS::Specification::Compiler $compiler .= new: :$actions; - my @defs = $compiler.load-defs($input-path); - my @summary; - my %properties; - - for @defs -> $def { - with $def -> @props { - my $raku = $def; - my $synopsis = $def; - my $box = $raku ~~ /:s '**' '1..4' $/; - - for @props -> $name { - my %details = :$name, :$synopsis; - %details = $_ - with $def; - %details = $_ - with $def; - %details = True - if $box; - %properties{$name} = %details; - @summary.push: %details; + our sub summary(Path :$input-path? ) { + + my CSS::Specification::Actions $actions .= new; + my @defs = load-defs($input-path, $actions); + my @summary; + my %properties; + + for @defs -> $def { + + with $def -> @props { + my $raku = $def; + my $synopsis = $def; + my $box = $raku ~~ /:s '**' '1..4' $/; + + for @props -> $name { + my %details = :$name, :$synopsis; + %details = $_ + with $def; + %details = $_ + with $def; + %details = True + if $box; + %properties{$name} = %details; + @summary.push: %details; + } } } + + find-edges(%properties, $actions.child-props); + check-edges(%properties); + + return @summary; } - find-edges(%properties, $actions.child-props); - check-edges(%properties); + sub load-defs (Path $properties-spec, $actions?) { + my $fh = $properties-spec + ?? open $properties-spec, :r + !! $*IN; - return @summary; -} + my @defs; + for $fh.lines -> $prop-spec { + # handle full line comments + next if $prop-spec ~~ /^'#'/ || $prop-spec eq ''; + # '| inherit' and '| initial' are implied anyway; get rid of them + my $spec = $prop-spec.subst(/\s* '|' \s* [inherit|initial]/, ''):g; -sub generate-raku-rules(@defs) { + my $/ = CSS::Specification.subparse($spec, :actions($actions) ); + die "unable to parse: $spec" + unless $/; + my $defs = $/.ast; + @defs.append: @$defs; + } - for @defs -> $def { + return @defs; + } - with $def -> @props { - my $raku = $def; - my $synopsis = $def; + sub generate-raku-rules(@defs) { - # boxed repeating property. repeat the expr - my $box = $raku ~~ /:s '**' '1..4' $/ - ?? ', :box' - !! ''; - my $repeats = ''; - if $box { - $raku ~~ s/:s '**' '1..4' $//; - $repeats = ' ** 1..4'; - } + for @defs -> $def { + + with $def -> @props { + my $raku = $def; + my $synopsis = $def; - for @props -> $prop { - my $match = $prop.subst(/\-/, '\-'):g; + # boxed repeating property. repeat the expr + my $box = $raku ~~ /:s '**' '1..4' $/ + ?? ', :box' + !! ''; + my $repeats = ''; + if $box { + $raku ~~ s/:s '**' '1..4' $//; + $repeats = ' ** 1..4'; + } + for @props -> $prop { + my $match = $prop.subst(/\-/, '\-'):g; + + say ""; + say "#| $prop: $synopsis"; + say "rule decl:sym<{$prop}> \{:i ($match) ':' $repeats \}, &?ROUTINE.WHY)> \}"; + say "rule expr-$prop \{:i $raku \}"; + } + } + else { + my $rule = $def; + my $raku = $def; + my $synopsis = $def; say ""; - say "#| $prop: $synopsis"; - say "rule decl:sym<{$prop}> \{:i ($match) ':' $repeats \}, &?ROUTINE.WHY)> \}"; - say "rule expr-$prop \{:i $raku \}"; + say "#| $rule: $synopsis"; + say "rule $rule \{:i $raku \}"; } } - else { - my $rule = $def; - my $raku = $def; - my $synopsis = $def; - say ""; - say "#| $rule: $synopsis"; - say "rule $rule \{:i $raku \}"; - } } -} -sub generate-raku-actions(@defs, %references) { + sub generate-raku-actions(@defs, %references) { - for @defs -> $def { + for @defs -> $def { - my $synopsis = $def; + my $synopsis = $def; - with $def -> @props { - for @props -> $prop { + with $def -> @props { + for @props -> $prop { - say "method expr-{$prop}(\$/) \{ make \$.build.list(\$/) \}" - if %references{'expr-' ~ $prop}:exists; + say "method expr-{$prop}(\$/) \{ make \$.build.list(\$/) \}" + if %references{'expr-' ~ $prop}:exists; + } + } + else { + my $rule = $def; + say "method $rule\(\$/\) \{ make \$.build.rule(\$/) \}" } - } - else { - my $rule = $def; - say "method $rule\(\$/\) \{ make \$.build.rule(\$/) \}" } } -} + #= generate an interface class for all unresolved terms. + sub generate-raku-interface(%references, %prop-names, %rule-names) { + + my %unresolved = %references; + %unresolved{'expr-' ~ $_}:delete + for %prop-names.keys; + %unresolved{$_}:delete + for %rule-names.keys; + for %unresolved.keys.sort -> $sym { + say "method {$sym}(\$/) \{ ... \}"; + } + } +} diff --git a/lib/CSS/Specification/Compiler.rakumod b/lib/CSS/Specification/Compiler.rakumod index 0808600..7dc0760 100644 --- a/lib/CSS/Specification/Compiler.rakumod +++ b/lib/CSS/Specification/Compiler.rakumod @@ -73,11 +73,10 @@ method !interface-methods { our proto sub compile (|) {*} multi sub compile(:@occurs! ($quant! is copy, *%term)) { - my RakuAST::Regex $separator; - my $atom = compile(|%term); - if $quant[0] ~~ '#' { - $separator = compile(:op<,>); - } + my RakuAST::Regex $atom = compile(|%term); + my RakuAST::Regex $separator = compile(:op<,>) + if $quant[0] ~~ '#'; + my RakuAST::Regex::Quantifier $quantifier = do given $quant { when '?' { RakuAST::Regex::Quantifier::ZeroOrOne.new @@ -100,23 +99,34 @@ multi sub compile(:@occurs! ($quant! is copy, *%term)) { sub id(Str:D $id) { RakuAST::Name.from-identifier($id) } -sub assertion(Str:D $id, Bool :$capturing = True) { - my $name := $id.&id; - RakuAST::Regex::Assertion::Named.new( - :$name, :$capturing +sub look-ahead(RakuAST::Regex::Assertion $assertion, Bool :$negated = False, Bool :$capturing = False) { + RakuAST::Regex::Assertion::Lookahead.new( + :$assertion, :$negated ); } -sub assertion-arg(Str:D $id, Str:D $arg, Bool :$capturing = True) { +multi sub assertion(Str:D $id, Bool :$capturing = True, RakuAST::ArgList :$args!) { my $name := $id.&id; - my @segments = RakuAST::StrLiteral.new($arg); - my $args = RakuAST::ArgList.new: RakuAST::QuotedString.new(:@segments); - RakuAST::Regex::Assertion::Named::Args.new( - :$name, :$args :$capturing + :$name, :$capturing, :$args, ); } +multi sub assertion(Str:D $id, Bool :$capturing = True) { + my $name := $id.&id; + RakuAST::Regex::Assertion::Named.new( + :$name, :$capturing, + ); +} + +multi sub arg(Str:D $arg) { + RakuAST::ArgList.new: RakuAST::StrLiteral.new($arg); +} + +multi sub arg(Int:D $arg) { + RakuAST::ArgList.new: RakuAST::IntLiteral.new($arg); +} + sub ws(RakuAST::Regex $r) { RakuAST::Regex::WithWhitespace.new($r) } sub lit(Str:D $s) { RakuAST::Regex::Literal.new($s) } @@ -161,12 +171,26 @@ multi sub compile(:@numbers!) { } multi sub compile(Str:D :$op!) { - assertion-arg 'op', $op; + my $args = ','.&arg; + 'op'.&assertion(:$args); } multi sub compile(:@alt!) { alt @alt.map(&compile) } multi sub compile(:@seq!) { seq @seq.map(&compile) } multi sub compile(:$group!) { group compile($group) } +multi sub compile(:@required) { + my $id = 0; + my $atom = alt @required.map: { + my $args = ($id++).&arg; + my $seen = 'seen'.&assertion(:$args).&look-ahead(:negated); + my $term = compile($_); + [$term, $seen].&seq; + } + $atom .= &group; + my RakuAST::Regex::Quantifier::Range $quantifier .= new: :min($id), :max($id); + RakuAST::Regex::QuantifiedAtom.new: :$atom, :$quantifier; +} + multi sub compile($arg) { compile |$arg } diff --git a/lib/CSS/Specification/Compiler/Actions.rakumod b/lib/CSS/Specification/Compiler/Actions.rakumod index 31466fa..621931a 100644 --- a/lib/CSS/Specification/Compiler/Actions.rakumod +++ b/lib/CSS/Specification/Compiler/Actions.rakumod @@ -80,16 +80,14 @@ method term-options($/) { method term-combo($/) { my @combo = @>>.ast; - +warn @combo.elems; make @combo == 1 ?? @combo[0] !! (:@combo) } method term-required($/) { - my @choices = $>>.ast; + my @required = $>>.ast; - make @choices == 1 - ?? @choices[0] - !! [~] ('required' => @choices) + make @required == 1 ?? @required[0] !! (:@required); } method term-seq($/) { @@ -134,7 +132,8 @@ method value:sym($/) is DEPRECATED { } method value:sym($/) { - make 'keywords' => @.map: {.ast.value}; + my @keywords = @.map: {.ast.value}; + make (:@keywords); } method value:sym($/) { diff --git a/t/00compile.t b/t/00compile.t index abfc916..b702719 100644 --- a/t/00compile.t +++ b/t/00compile.t @@ -73,11 +73,13 @@ for ( deparse => '** 1..4% ', }, ## # precedence tests taken from: https://developer.mozilla.org/en-US/docs/CSS/Value_definition_syntax -## 'spec' => {input => 'bold thin && ', -## deparse => ':my @*SEEN; [ bold & thin & | ]**2', -## }, + 'spec' => { + input => 'bold thin && ', + ast => :required[:seq[:keywords["bold"], :keywords["thin"]], :rule("length")], + deparse => ':my @S; [ bold & thin & | ]**2', + }, ## 'spec' => {input => 'bold || thin && ', -## deparse => ':my @*SEEN; [ bold & | [ thin & | ]**2 ]+', +## deparse => ':my @S; [ bold & | [ thin & | ]**2 ]+', ## }, ## 'property-spec' => {input => "'content'\tnormal | none | [ | | | attr() | open-quote | close-quote | no-open-quote | no-close-quote ]+ | inherit normal :before and :after pseudo-elements no", ## ast => {:props['content'], diff --git a/t/build.t b/t/build.t index 51a3a18..8b78ae9 100644 --- a/t/build.t +++ b/t/build.t @@ -33,9 +33,11 @@ capture({ }, 't/lib/Test/CSS/Aural/Spec/Actions.rakumod'); lives-ok {require ::($actions-name)}, "$actions-name compilation"; -my RakuAST::Package $interface-package = CSS::Specification::Build::generate( 'interface', @role-id, :$input-path ); -'t/lib/Test/CSS/Aural/Spec/Interface.rakumod'.IO.spurt: $interface-package.DEPARSE; my $role-name = @role-id.join: '::'; + +capture({ + CSS::Specification::Build::generate( 'interface', $role-name, :$input-path ); +}, 't/lib/Test/CSS/Aural/Spec/Interface.rakumod'); lives-ok {require ::($role-name)}, "$role-name compilation"; dies-ok {require ::("Test::CSS::Aural::BadGrammar")}, 'grammar composition, unimplemented interface - dies'; diff --git a/t/lib/Test/CSS/Aural/Spec/Interface.rakumod b/t/lib/Test/CSS/Aural/Spec/Interface.rakumod index 94c68f5..1b301c8 100644 --- a/t/lib/Test/CSS/Aural/Spec/Interface.rakumod +++ b/t/lib/Test/CSS/Aural/Spec/Interface.rakumod @@ -1,35 +1,16 @@ -role Test::CSS::Aural::Spec::Interface { - method angle ($/) { - ... - } - method color ($/) { - ... - } - method frequency ($/) { - ... - } - method generic-voice ($/) { - ... - } - method identifier ($/) { - ... - } - method number ($/) { - ... - } - method percentage ($/) { - ... - } - method specific-voice ($/) { - ... - } - method string ($/) { - ... - } - method time ($/) { - ... - } - method uri ($/) { - ... - } -} \ No newline at end of file +use v6; +# -- DO NOT EDIT -- +# generated by: t/build.t +unit role Test::CSS::Aural::Spec::Interface; + +method angle($/) { ... } +method color($/) { ... } +method frequency($/) { ... } +method generic-voice($/) { ... } +method identifier($/) { ... } +method number($/) { ... } +method percentage($/) { ... } +method specific-voice($/) { ... } +method string($/) { ... } +method time($/) { ... } +method uri($/) { ... }