351 lines
6.2 KiB
Plaintext
351 lines
6.2 KiB
Plaintext
##
|
|
# AST parser for CSS expressions.
|
|
#
|
|
class Oga::CSS::Parser
|
|
|
|
token T_IDENT T_PIPE T_LBRACK T_RBRACK T_COLON T_SPACE T_LPAREN T_RPAREN T_MINUS
|
|
token T_EQ T_SPACE_IN T_STARTS_WITH T_ENDS_WITH T_IN T_HYPHEN_IN
|
|
token T_GREATER T_TILDE T_PLUS
|
|
token T_NTH T_INT T_STRING T_ODD T_EVEN T_DOT T_HASH
|
|
|
|
options no_result_var
|
|
|
|
prechigh
|
|
left T_COLON T_HASH T_DOT
|
|
left T_GREATER T_TILDE T_PLUS
|
|
preclow
|
|
|
|
rule
|
|
css
|
|
: selectors { val[0] }
|
|
| /* none */ { nil }
|
|
;
|
|
|
|
selectors
|
|
: selector
|
|
{
|
|
# a single "+ y" selector
|
|
if val[0].is_a?(Array)
|
|
return s(:path, *val[0])
|
|
else
|
|
return val[0]
|
|
end
|
|
}
|
|
| selectors_ { s(:path, *val[0].flatten) }
|
|
;
|
|
|
|
selectors_
|
|
: selectors_ T_SPACE selector { val[0] << val[2] }
|
|
| selector T_SPACE selector { [val[0], val[2]] }
|
|
;
|
|
|
|
selector
|
|
: descendant_or_self
|
|
| axis
|
|
;
|
|
|
|
descendant_or_self
|
|
: node_test { s(:axis, 'descendant-or-self', val[0]) }
|
|
;
|
|
|
|
axis
|
|
: T_GREATER axis_selector
|
|
{
|
|
s(:axis, 'child', val[1])
|
|
}
|
|
| T_TILDE axis_selector
|
|
{
|
|
s(:axis, 'following-sibling', val[1])
|
|
}
|
|
| T_PLUS axis_selector
|
|
{
|
|
[
|
|
s(:axis, 'following-sibling', s(:test, nil, '*', s(:int, 1))),
|
|
s(:axis, 'self', val[1])
|
|
]
|
|
}
|
|
;
|
|
|
|
axis_selector
|
|
| node_test
|
|
| axis
|
|
;
|
|
|
|
node_test
|
|
# foo
|
|
: node_name
|
|
{
|
|
s(:test, *val[0])
|
|
}
|
|
|
|
# .foo, :root, etc
|
|
| predicates
|
|
{
|
|
s(:test, nil, '*', val[0])
|
|
}
|
|
|
|
# foo.bar
|
|
| node_name predicates
|
|
{
|
|
s(:test, *val[0], val[1])
|
|
}
|
|
|
|
# foo[bar]
|
|
| node_name attribute_predicate
|
|
{
|
|
s(:test, *val[0], val[1])
|
|
}
|
|
|
|
# foo[bar].baz
|
|
| node_name attribute_predicate predicates
|
|
{
|
|
s(:test, *val[0], s(:and, val[1], val[2]))
|
|
}
|
|
;
|
|
|
|
node_name
|
|
# foo
|
|
: T_IDENT { [nil, val[0]] }
|
|
|
|
# ns|foo
|
|
| T_IDENT T_PIPE T_IDENT { [val[0], val[2]] }
|
|
|
|
# |foo
|
|
| T_PIPE T_IDENT { [nil, val[1]] }
|
|
;
|
|
|
|
predicates
|
|
: predicates predicate { s(:and, val[0], val[1]) }
|
|
| predicate
|
|
;
|
|
|
|
predicate
|
|
: class
|
|
| id
|
|
| pseudo_class
|
|
;
|
|
|
|
attribute_predicate
|
|
: T_LBRACK attribute_predicate_members T_RBRACK { val[1] }
|
|
;
|
|
|
|
attribute_predicate_members
|
|
: attribute
|
|
| operator
|
|
;
|
|
|
|
attribute
|
|
: node_name { s(:axis, 'attribute', s(:test, *val[0])) }
|
|
;
|
|
|
|
# The AST of these operators is mostly based on what
|
|
# `Nokogiri::CSS.xpath_for('...')` returns.
|
|
operator
|
|
# a="b"
|
|
: attribute T_EQ string
|
|
{
|
|
s(:eq, val[0], val[2])
|
|
}
|
|
|
|
# a~="b"
|
|
| attribute T_SPACE_IN string
|
|
{
|
|
s(
|
|
:call,
|
|
'contains',
|
|
s(:call, 'concat', s(:string, ' '), val[0], s(:string, ' ')),
|
|
s(:call, 'concat', s(:string, ' '), val[2], s(:string, ' '))
|
|
)
|
|
}
|
|
|
|
# a^="b"
|
|
| attribute T_STARTS_WITH string
|
|
{
|
|
s(:call, 'starts-with', val[0], val[2])
|
|
}
|
|
|
|
# a$="b"
|
|
| attribute T_ENDS_WITH string
|
|
{
|
|
s(
|
|
:eq,
|
|
s(
|
|
:call,
|
|
'substring',
|
|
val[0],
|
|
s(
|
|
:add,
|
|
s(
|
|
:sub,
|
|
s(:call, 'string-length', val[0]),
|
|
s(:call, 'string-length', val[2])
|
|
),
|
|
s(:int, 1)
|
|
),
|
|
s(:call, 'string-length', val[2])
|
|
),
|
|
val[2]
|
|
)
|
|
}
|
|
|
|
# a*="b"
|
|
| attribute T_IN string
|
|
{
|
|
s(:call, 'contains', val[0], val[2])
|
|
}
|
|
|
|
# a|="b"
|
|
| attribute T_HYPHEN_IN string
|
|
{
|
|
s(
|
|
:or,
|
|
s(:eq, val[0], val[2]),
|
|
s(
|
|
:call,
|
|
'starts-with',
|
|
val[0],
|
|
s(:call, 'concat', val[2], s(:string, '-'))
|
|
)
|
|
)
|
|
}
|
|
;
|
|
|
|
class
|
|
: T_DOT T_IDENT
|
|
{
|
|
axis = s(:axis, 'attribute', s(:test, nil, 'class'))
|
|
|
|
s(
|
|
:call,
|
|
'contains',
|
|
s(:call, 'concat', s(:string, ' '), axis, s(:string, ' ')),
|
|
s(:string, " #{val[1]} ")
|
|
)
|
|
}
|
|
;
|
|
|
|
id
|
|
: T_HASH T_IDENT
|
|
{
|
|
s(
|
|
:eq,
|
|
s(:axis, 'attribute', s(:test, nil, 'id')),
|
|
s(:string, val[1])
|
|
)
|
|
}
|
|
;
|
|
|
|
pseudo_class
|
|
# :root
|
|
: pseudo_name { on_pseudo_class(val[0]) }
|
|
|
|
# :nth-child(2)
|
|
| pseudo_name pseudo_args { on_pseudo_class(val[0], val[1]) }
|
|
;
|
|
|
|
pseudo_name
|
|
: T_COLON T_IDENT { val[1] }
|
|
;
|
|
|
|
pseudo_args
|
|
: T_LPAREN pseudo_arg T_RPAREN { val[1] }
|
|
;
|
|
|
|
pseudo_arg
|
|
: integer
|
|
| odd
|
|
| even
|
|
| nth
|
|
| selector
|
|
;
|
|
|
|
nth
|
|
: T_NTH { s(:nth) }
|
|
| T_MINUS T_NTH { s(:nth) }
|
|
| integer T_NTH { s(:nth, val[0]) }
|
|
| integer T_NTH integer { s(:nth, val[0], val[2]) }
|
|
;
|
|
|
|
odd
|
|
: T_ODD { s(:nth, s(:int, 2), s(:int, 1)) }
|
|
;
|
|
|
|
even
|
|
: T_EVEN { s(:nth, s(:int, 2)) }
|
|
;
|
|
|
|
string
|
|
: T_STRING { s(:string, val[0]) }
|
|
;
|
|
|
|
integer
|
|
: T_INT { s(:int, val[0].to_i) }
|
|
;
|
|
end
|
|
|
|
---- inner
|
|
##
|
|
# @param [String] data The input to parse.
|
|
#
|
|
def initialize(data)
|
|
@lexer = Lexer.new(data)
|
|
end
|
|
|
|
##
|
|
# @param [Symbol] type
|
|
# @param [Array] children
|
|
# @return [AST::Node]
|
|
#
|
|
def s(type, *children)
|
|
return AST::Node.new(type, children)
|
|
end
|
|
|
|
##
|
|
# Yields the next token from the lexer.
|
|
#
|
|
# @yieldparam [Array]
|
|
#
|
|
def yield_next_token
|
|
@lexer.advance do |*args|
|
|
yield args
|
|
end
|
|
|
|
yield [false, false]
|
|
end
|
|
|
|
##
|
|
# Parses the input and returns the corresponding AST.
|
|
#
|
|
# @example
|
|
# parser = Oga::CSS::Parser.new('foo.bar')
|
|
# ast = parser.parse
|
|
#
|
|
# @return [AST::Node]
|
|
#
|
|
def parse
|
|
ast = yyparse(self, :yield_next_token)
|
|
|
|
return ast
|
|
end
|
|
|
|
##
|
|
# @param [String] name
|
|
# @param [AST::Node] arg
|
|
# @return [AST::Node]
|
|
#
|
|
def on_pseudo_class(name, arg = nil)
|
|
handler = "on_pseudo_class_#{name.gsub('-', '_')}"
|
|
|
|
return arg ? send(handler, arg) : send(handler)
|
|
end
|
|
|
|
##
|
|
# Generates the AST for the `root` pseudo class.
|
|
#
|
|
# @return [AST::Node]
|
|
#
|
|
def on_pseudo_class_root
|
|
return s(:call, 'not', s(:axis, 'parent', s(:test, nil, '*')))
|
|
end
|
|
# vim: set ft=racc:
|