html-conditional-comment 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +41 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/html-conditional-comment.gemspec +27 -0
- data/lib/html-conditional-comment.rb +35 -0
- data/lib/html-conditional-comment/lexer.rb +108 -0
- data/lib/html-conditional-comment/nodes.rb +54 -0
- data/lib/html-conditional-comment/parser.rb +228 -0
- data/lib/html-conditional-comment/version.rb +3 -0
- data/lib/html-conditional-comment/version_vector.rb +42 -0
- data/lib/html-conditional-comment/visitor.rb +99 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bf4b71499353f362569739053e61a2557c2a4cea
|
4
|
+
data.tar.gz: dba497e3d758323e16684aff985ad62e807ba564
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cbe607ef7e65c9b3fc5207b3f0e6d9275b638a897093e2d608bd9189614a98a9d789fee620d83a495df56437d087241054031cc57b75c7fec40f9b0be9f32d16
|
7
|
+
data.tar.gz: 5ab6afde82cf240722973804f7f43fff9889fe61b5929ff3d4d11a07e440d831dd20831505a7c71d98d5e7816a3f5e96c075c9cae5e7c5294d9b1e9bdbbbb514
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Carson Reinke
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# HTML Conditional Comment Evaluater, Parser, and Lexer
|
2
|
+
|
3
|
+
Evaluate, parse, and tokenizing HTML conditional comments using provided features and version. Allows for existing HTML to be maintained and only conditional comments to evaluated.
|
4
|
+
|
5
|
+
## Why?
|
6
|
+
|
7
|
+
Conditional comments are really a legacy approach, however the application is still quite prevelant in the email marketing industry.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'html-conditional-comment'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install html-conditional-comment
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
* Evaluating:
|
28
|
+
`HtmlConditionalComment.to_string("What is the <!--[if gte mso 9]>Outlook<![endif]-->?", "mso", 9) => "What is the Outlook?"`
|
29
|
+
|
30
|
+
* Parsing: `HtmlConditionalComment.parse(...) => HtmlConditionalComment::Nodes::Nodes`
|
31
|
+
|
32
|
+
* Tokenizing: `HtmlConditionalComment.lex(...) => [[:open, "<!--["], [:if, "if"]...`
|
33
|
+
|
34
|
+
## Contributing
|
35
|
+
|
36
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/carsonreinke/html-conditional-comment.
|
37
|
+
|
38
|
+
|
39
|
+
## License
|
40
|
+
|
41
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "html-conditional-comment"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'html-conditional-comment/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "html-conditional-comment"
|
8
|
+
spec.version = HtmlConditionalComment::VERSION
|
9
|
+
spec.authors = ["Carson Reinke"]
|
10
|
+
spec.email = ["carson@reinke.co"]
|
11
|
+
|
12
|
+
spec.summary = %q{Parse HTML conditional comments}
|
13
|
+
spec.homepage = "https://github.com/carsonreinke/html-conditional-comment"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
26
|
+
spec.add_development_dependency "byebug"
|
27
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'html-conditional-comment/version'
|
2
|
+
require 'html-conditional-comment/lexer'
|
3
|
+
require 'html-conditional-comment/parser'
|
4
|
+
require 'html-conditional-comment/nodes'
|
5
|
+
require 'html-conditional-comment/visitor'
|
6
|
+
require 'html-conditional-comment/version_vector'
|
7
|
+
|
8
|
+
module HtmlConditionalComment
|
9
|
+
class << self
|
10
|
+
##
|
11
|
+
# Tokenize the HTML into an array of tokens
|
12
|
+
#
|
13
|
+
def lex(html)
|
14
|
+
Lexer.new(html).tokenize()
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Parse into tree of nodes the HTML
|
19
|
+
#
|
20
|
+
def parse(html)
|
21
|
+
Parser.new(self.lex(html)).parse()
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Evaluate conditional comments in HTML using the supplied browser
|
26
|
+
# information and return a string
|
27
|
+
#
|
28
|
+
# * +features+ - String or Array of features of browser
|
29
|
+
# * +version+ - String, Integer, or Float representing version of the browser
|
30
|
+
#
|
31
|
+
def to_string(html, features, version)
|
32
|
+
self.parse(html).accept(Visitors::ToString.new(features, version))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module HtmlConditionalComment
|
2
|
+
class TokenError < StandardError
|
3
|
+
def initialize(rest)
|
4
|
+
super("Invalid token \"#{rest.slice(0, 25)}\"")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
##
|
9
|
+
# Converts string into array of tokens. Token is an array, first element is
|
10
|
+
# symbol representing the token, second element is string value.
|
11
|
+
#
|
12
|
+
class Lexer
|
13
|
+
LESS_THAN = /lt/i
|
14
|
+
LESS_THAN_EQUAL = /lte/i
|
15
|
+
GREATER_THAN = /gt/i
|
16
|
+
GREATER_THAN_EQUAL = /gte/i
|
17
|
+
OPEN_PAREN = /\(/
|
18
|
+
CLOSE_PAREN = /\)/
|
19
|
+
NOT = /\!/
|
20
|
+
OR = /\|/
|
21
|
+
AND = /\&/
|
22
|
+
TRUE = /true/i
|
23
|
+
FALSE = /false/i
|
24
|
+
IF_STATEMENT = /if/i
|
25
|
+
ENDIF_STATEMENT = /endif/i
|
26
|
+
#Opening statement plus positive look ahead to avoid conflicts with other
|
27
|
+
#comments
|
28
|
+
OPEN = /<!(\-\-)?\[(?=(end)?if)/
|
29
|
+
CLOSE = /\](\-\-)?>/
|
30
|
+
WHITE_SPACE = /\s+/
|
31
|
+
FEATURE = /[a-z]+/i
|
32
|
+
VERSION_VECTOR = /\d+(\.[\d]+)?/
|
33
|
+
|
34
|
+
TOKENS = [
|
35
|
+
[:if, IF_STATEMENT],
|
36
|
+
[:endif, ENDIF_STATEMENT],
|
37
|
+
|
38
|
+
[:paren_open, OPEN_PAREN],
|
39
|
+
[:paren_close, CLOSE_PAREN],
|
40
|
+
|
41
|
+
[:operator_less_than_equal, LESS_THAN_EQUAL],
|
42
|
+
[:operator_less_than, LESS_THAN],
|
43
|
+
[:operator_greater_than_equal, GREATER_THAN_EQUAL],
|
44
|
+
[:operator_greater_than, GREATER_THAN],
|
45
|
+
|
46
|
+
[:operator_not, NOT],
|
47
|
+
[:operator_or, OR],
|
48
|
+
[:operator_and, AND],
|
49
|
+
|
50
|
+
[:boolean_true, TRUE],
|
51
|
+
[:boolean_false, FALSE],
|
52
|
+
[:feature, FEATURE],
|
53
|
+
[:version_vector, VERSION_VECTOR]
|
54
|
+
]
|
55
|
+
|
56
|
+
def initialize(html_or_comment)
|
57
|
+
@scanner = StringScanner.new(html_or_comment)
|
58
|
+
end
|
59
|
+
|
60
|
+
def tokenize()
|
61
|
+
tokens = []
|
62
|
+
open = false
|
63
|
+
|
64
|
+
#Run until nothing left in string
|
65
|
+
until @scanner.eos?()
|
66
|
+
#Split between if the conditional comment has been opened or not
|
67
|
+
#State will help handle all the other HTML we don't care about
|
68
|
+
if open
|
69
|
+
@scanner.skip(WHITE_SPACE)
|
70
|
+
if token = @scanner.scan(CLOSE)
|
71
|
+
open = false
|
72
|
+
tokens << [:close, token]
|
73
|
+
else
|
74
|
+
#Go through token specs and scan and stop on first one
|
75
|
+
token = TOKENS.inject(nil) do |previous, spec|
|
76
|
+
t = @scanner.scan(spec[1])
|
77
|
+
unless t.nil?()
|
78
|
+
break [spec[0], t]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
if token
|
82
|
+
tokens << token
|
83
|
+
else
|
84
|
+
raise TokenError.new(@scanner.rest())
|
85
|
+
end
|
86
|
+
end
|
87
|
+
#Closed (not opened) conditional comment
|
88
|
+
else
|
89
|
+
#Scan till we find an open token, if not done and use the rest
|
90
|
+
if match = @scanner.scan_until(OPEN)
|
91
|
+
open = true
|
92
|
+
#TODO Gross way to get up till scan has succeeded
|
93
|
+
match = match.slice(0..-(@scanner.matched.size() + 1))
|
94
|
+
tokens << [:html, match] unless match.empty?()
|
95
|
+
tokens << [:open, @scanner.matched]
|
96
|
+
else
|
97
|
+
tokens << [:html, @scanner.rest()] if @scanner.rest?()
|
98
|
+
break
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
@scanner.reset()
|
103
|
+
|
104
|
+
tokens
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module HtmlConditionalComment
|
5
|
+
module Nodes
|
6
|
+
module Node
|
7
|
+
def accept(visitor)
|
8
|
+
visitor.visit(self)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class NodeItem
|
13
|
+
include Node
|
14
|
+
end
|
15
|
+
|
16
|
+
class Nodes < Array
|
17
|
+
include Node
|
18
|
+
end
|
19
|
+
|
20
|
+
class ChildOperator < NodeItem
|
21
|
+
attr_accessor :child
|
22
|
+
end
|
23
|
+
class BranchOperator < NodeItem
|
24
|
+
attr_accessor :left, :right
|
25
|
+
end
|
26
|
+
class Comparison < ChildOperator; end
|
27
|
+
class Condition < BranchOperator; end
|
28
|
+
|
29
|
+
|
30
|
+
class Or < BranchOperator; end
|
31
|
+
class And < BranchOperator; end
|
32
|
+
class Not < ChildOperator; end
|
33
|
+
|
34
|
+
class Equal < Comparison; end
|
35
|
+
class LessThan < Comparison; end
|
36
|
+
class LessThanEqual < Comparison; end
|
37
|
+
class GreaterThan < Comparison; end
|
38
|
+
class GreaterThanEqual < Comparison; end
|
39
|
+
|
40
|
+
class Browser < NodeItem
|
41
|
+
attr_accessor :feature, :version_vector
|
42
|
+
end
|
43
|
+
class True < NodeItem
|
44
|
+
include Singleton
|
45
|
+
end
|
46
|
+
class False < NodeItem
|
47
|
+
include Singleton
|
48
|
+
end
|
49
|
+
|
50
|
+
class Html < NodeItem
|
51
|
+
attr_accessor :content
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
module HtmlConditionalComment
|
2
|
+
class ParseError < StandardError
|
3
|
+
def initialize(msg, example)
|
4
|
+
super("#{msg} at \"#{example.slice(0, 25)}\"")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
##
|
9
|
+
#
|
10
|
+
# Parse tokens into a tree of nodes
|
11
|
+
#
|
12
|
+
# Pseudo grammar
|
13
|
+
#
|
14
|
+
#template = { html | statement }
|
15
|
+
#statement = "<!" , [ "--" ] , "if" , expression , "]" , [ "--" ] , ">" , template , "<!" , [ "--" ] , "endif" , "]" , [ "--" ] , ">"
|
16
|
+
#expression = term [ "|" , term ]
|
17
|
+
#term = factor [ "&" , factor ]
|
18
|
+
#factor = subexpression | "!" , factor | "(" , expression , ")"
|
19
|
+
#subexpression = [ operator ] browser | boolean
|
20
|
+
#operator = "gt" | "gte" | "lt" | "lte"
|
21
|
+
#boolean = "true" | "false"
|
22
|
+
#browser = feature [ version_vector ]
|
23
|
+
#
|
24
|
+
class Parser
|
25
|
+
def initialize(tokens)
|
26
|
+
@symbol = nil
|
27
|
+
@tokens = tokens
|
28
|
+
@max_pos = tokens.size() - 1
|
29
|
+
@pos = -1
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse()
|
33
|
+
self.next()
|
34
|
+
|
35
|
+
nodes = template()
|
36
|
+
|
37
|
+
#Tokens left, syntax error
|
38
|
+
error() if @pos < @max_pos
|
39
|
+
|
40
|
+
nodes
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
protected
|
45
|
+
#Browser is combination of feature and optional version
|
46
|
+
def browser()
|
47
|
+
node = Nodes::Browser.new()
|
48
|
+
node.feature = @value
|
49
|
+
expect(:feature)
|
50
|
+
|
51
|
+
if current(:version_vector)
|
52
|
+
node.version_vector = VersionVector.new(@value)
|
53
|
+
accept(:version_vector)
|
54
|
+
else
|
55
|
+
node.version_vector = VersionVector.new(nil)
|
56
|
+
end
|
57
|
+
|
58
|
+
node
|
59
|
+
end
|
60
|
+
|
61
|
+
#True or false
|
62
|
+
def boolean()
|
63
|
+
if accept(:boolean_true)
|
64
|
+
Nodes::True.instance()
|
65
|
+
elsif accept(:boolean_false)
|
66
|
+
Nodes::False.instance()
|
67
|
+
else
|
68
|
+
error()
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#Comparison operators
|
73
|
+
def operator()
|
74
|
+
if accept(:operator_less_than)
|
75
|
+
Nodes::LessThan.new()
|
76
|
+
elsif accept(:operator_less_than_equal)
|
77
|
+
Nodes::LessThanEqual.new()
|
78
|
+
elsif accept(:operator_greater_than)
|
79
|
+
Nodes::GreaterThan.new()
|
80
|
+
elsif accept(:operator_greater_than_equal)
|
81
|
+
Nodes::GreaterThanEqual.new()
|
82
|
+
else
|
83
|
+
error()
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
#Either a comparison with the browser, boolean, or simply just the browser
|
88
|
+
def subexpression()
|
89
|
+
node = nil
|
90
|
+
|
91
|
+
if current(:operator_less_than) || current(:operator_less_than_equal) ||
|
92
|
+
current(:operator_greater_than) || current(:operator_greater_than_equal)
|
93
|
+
|
94
|
+
node = operator()
|
95
|
+
node.child = browser()
|
96
|
+
elsif current(:boolean_true) || current(:boolean_false)
|
97
|
+
node = boolean()
|
98
|
+
else
|
99
|
+
#No comparison operator is assuming equals
|
100
|
+
node = Nodes::Equal.new()
|
101
|
+
node.child = browser()
|
102
|
+
end
|
103
|
+
|
104
|
+
node
|
105
|
+
end
|
106
|
+
|
107
|
+
#Negated self or paranthesised expression
|
108
|
+
def factor()
|
109
|
+
node = nil
|
110
|
+
|
111
|
+
if accept(:operator_not)
|
112
|
+
node = Nodes::Not.new()
|
113
|
+
node.child = factor()
|
114
|
+
elsif accept(:paren_open)
|
115
|
+
node = expression()
|
116
|
+
expect(:paren_close)
|
117
|
+
else
|
118
|
+
node = subexpression()
|
119
|
+
end
|
120
|
+
|
121
|
+
node
|
122
|
+
end
|
123
|
+
|
124
|
+
#And
|
125
|
+
def term()
|
126
|
+
node = factor()
|
127
|
+
while accept(:operator_and)
|
128
|
+
branch_node = Nodes::And.new()
|
129
|
+
branch_node.left = node
|
130
|
+
branch_node.right = factor()
|
131
|
+
node = branch_node
|
132
|
+
end
|
133
|
+
|
134
|
+
node
|
135
|
+
end
|
136
|
+
|
137
|
+
#Or
|
138
|
+
def expression()
|
139
|
+
node = term()
|
140
|
+
while accept(:operator_or)
|
141
|
+
branch_node = Nodes::Or.new()
|
142
|
+
branch_node.left = node
|
143
|
+
branch_node.right = term()
|
144
|
+
node = branch_node
|
145
|
+
end
|
146
|
+
|
147
|
+
node
|
148
|
+
end
|
149
|
+
|
150
|
+
def condition()
|
151
|
+
node = Nodes::Condition.new()
|
152
|
+
|
153
|
+
expect(:open)
|
154
|
+
expect(:if)
|
155
|
+
node.left = expression()
|
156
|
+
expect(:close)
|
157
|
+
|
158
|
+
unless current(:open) && peek(:endif)
|
159
|
+
node.right = template()
|
160
|
+
end
|
161
|
+
|
162
|
+
expect(:open)
|
163
|
+
expect(:endif)
|
164
|
+
expect(:close)
|
165
|
+
|
166
|
+
node
|
167
|
+
end
|
168
|
+
|
169
|
+
def html()
|
170
|
+
node = Nodes::Html.new()
|
171
|
+
node.content = @value
|
172
|
+
expect(:html)
|
173
|
+
node
|
174
|
+
end
|
175
|
+
|
176
|
+
def template()
|
177
|
+
nodes = Nodes::Nodes.new()
|
178
|
+
|
179
|
+
while current(:html) || (current(:open) && peek(:if))
|
180
|
+
nodes << if current(:html)
|
181
|
+
html()
|
182
|
+
elsif current(:open) && peek(:if)
|
183
|
+
condition()
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
nodes
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
protected
|
192
|
+
#Accept the symbol and move on or not
|
193
|
+
def accept(symbol)
|
194
|
+
if(symbol == @symbol)
|
195
|
+
self.next()
|
196
|
+
return true
|
197
|
+
else
|
198
|
+
return false
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
#Expect a current symbol or raise
|
203
|
+
def expect(symbol)
|
204
|
+
raise HtmlConditionalComment::ParseError.new("Expected #{symbol}, received #{@symbol}", @value) unless accept(symbol)
|
205
|
+
end
|
206
|
+
|
207
|
+
def current(symbol)
|
208
|
+
@symbol == symbol
|
209
|
+
end
|
210
|
+
|
211
|
+
def peek(symbol)
|
212
|
+
@tokens[@pos+1][0] == symbol
|
213
|
+
end
|
214
|
+
|
215
|
+
def next()
|
216
|
+
@pos += 1
|
217
|
+
#raise HtmlConditionalComment::ParserError.new('EOF') if @pos >= @max_pos
|
218
|
+
token = @tokens[@pos] || []
|
219
|
+
@symbol = token[0]
|
220
|
+
@value = token[1]
|
221
|
+
token
|
222
|
+
end
|
223
|
+
|
224
|
+
def error()
|
225
|
+
raise HtmlConditionalComment::ParseError.new("Syntax error", @value)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module HtmlConditionalComment
|
2
|
+
class VersionVector
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
DOT = /\./
|
6
|
+
DIGIT = /\d/
|
7
|
+
|
8
|
+
attr_accessor :string
|
9
|
+
|
10
|
+
def initialize(string)
|
11
|
+
@string = string.to_s() unless string.nil?()
|
12
|
+
end
|
13
|
+
|
14
|
+
def <=>(other)
|
15
|
+
#Force comparison class
|
16
|
+
other = VersionVector.new(other) unless other.is_a?(VersionVector)
|
17
|
+
|
18
|
+
return 0 if @string.nil?() || other.string.nil?()
|
19
|
+
return 0 if @string == other.string
|
20
|
+
|
21
|
+
#Normalize version array sizes
|
22
|
+
left, right = self.to_a(), other.to_a()
|
23
|
+
size = [left.size(), right.size()].min()
|
24
|
+
left.slice!(size..-1)
|
25
|
+
right.slice!(size..-1)
|
26
|
+
|
27
|
+
#Compare based on number
|
28
|
+
left.join.to_f() <=> right.join.to_f()
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Split string into array of version numbers, major can be multiple digits,
|
33
|
+
# minor can only be a single digit
|
34
|
+
#
|
35
|
+
def to_a()
|
36
|
+
major, minors = @string.split(DOT)
|
37
|
+
versions = (minors || '').scan(DIGIT)
|
38
|
+
versions.unshift(major, '.')
|
39
|
+
versions
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module HtmlConditionalComment
|
2
|
+
class VisitError < StandardError
|
3
|
+
def initialize(klass)
|
4
|
+
super("Cannot visit #{klass}")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module Visitors
|
9
|
+
class Visitor
|
10
|
+
def initialize(features, version)
|
11
|
+
@features = features
|
12
|
+
@features = [@features] unless @features.is_a?(Enumerable)
|
13
|
+
|
14
|
+
@version = if version.is_a?(VersionVector)
|
15
|
+
version
|
16
|
+
else
|
17
|
+
VersionVector.new(version)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
#Copied from https://blog.bigbinary.com/2013/07/07/visitor-pattern-and-double-dispatch.html
|
22
|
+
def visit(subject)
|
23
|
+
method_name = :"visit_#{(subject.class.name || '').gsub('::', '_')}"
|
24
|
+
__send__(method_name, subject)
|
25
|
+
end
|
26
|
+
|
27
|
+
#Provide method missing for better interpretation
|
28
|
+
def method_missing(method, args)
|
29
|
+
if method.to_s() =~ /^visit\_(.+)/
|
30
|
+
raise VisitError.new($1)
|
31
|
+
else
|
32
|
+
super(method, args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Evaluates conditions to boolean
|
39
|
+
#
|
40
|
+
class Eval < Visitor
|
41
|
+
protected
|
42
|
+
def visit_HtmlConditionalComment_Nodes_True(subject)
|
43
|
+
true
|
44
|
+
end
|
45
|
+
def visit_HtmlConditionalComment_Nodes_False(subject)
|
46
|
+
false
|
47
|
+
end
|
48
|
+
def visit_HtmlConditionalComment_Nodes_Browser(subject)
|
49
|
+
@features.include?(subject.feature)
|
50
|
+
end
|
51
|
+
|
52
|
+
def visit_HtmlConditionalComment_Nodes_Equal(subject)
|
53
|
+
subject.child.accept(self) && @version == subject.child.version_vector
|
54
|
+
end
|
55
|
+
def visit_HtmlConditionalComment_Nodes_LessThan(subject)
|
56
|
+
subject.child.accept(self) && @version < subject.child.version_vector
|
57
|
+
end
|
58
|
+
def visit_HtmlConditionalComment_Nodes_LessThanEqual(subject)
|
59
|
+
subject.child.accept(self) && @version <= subject.child.version_vector
|
60
|
+
end
|
61
|
+
def visit_HtmlConditionalComment_Nodes_GreaterThan(subject)
|
62
|
+
subject.child.accept(self) && @version > subject.child.version_vector
|
63
|
+
end
|
64
|
+
def visit_HtmlConditionalComment_Nodes_GreaterThanEqual(subject)
|
65
|
+
subject.child.accept(self) && @version >= subject.child.version_vector
|
66
|
+
end
|
67
|
+
|
68
|
+
def visit_HtmlConditionalComment_Nodes_Or(subject)
|
69
|
+
subject.left.accept(self) || subject.right.accept(self)
|
70
|
+
end
|
71
|
+
def visit_HtmlConditionalComment_Nodes_And(subject)
|
72
|
+
subject.left.accept(self) && subject.right.accept(self)
|
73
|
+
end
|
74
|
+
def visit_HtmlConditionalComment_Nodes_Not(subject)
|
75
|
+
!subject.child.accept(self)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Converts parser nodes to a string by evaluating each conditional comment
|
81
|
+
#
|
82
|
+
class ToString < Visitor
|
83
|
+
protected
|
84
|
+
def visit_HtmlConditionalComment_Nodes_Nodes(subject)
|
85
|
+
subject.map{|node| node.accept(self)}.join
|
86
|
+
end
|
87
|
+
|
88
|
+
def visit_HtmlConditionalComment_Nodes_Condition(subject)
|
89
|
+
if subject.left.accept(Eval.new(@features, @version))
|
90
|
+
subject.right.accept(self)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def visit_HtmlConditionalComment_Nodes_Html(subject)
|
95
|
+
subject.content
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: html-conditional-comment
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carson Reinke
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.13'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.13'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- carson@reinke.co
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".travis.yml"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- bin/console
|
83
|
+
- bin/setup
|
84
|
+
- html-conditional-comment.gemspec
|
85
|
+
- lib/html-conditional-comment.rb
|
86
|
+
- lib/html-conditional-comment/lexer.rb
|
87
|
+
- lib/html-conditional-comment/nodes.rb
|
88
|
+
- lib/html-conditional-comment/parser.rb
|
89
|
+
- lib/html-conditional-comment/version.rb
|
90
|
+
- lib/html-conditional-comment/version_vector.rb
|
91
|
+
- lib/html-conditional-comment/visitor.rb
|
92
|
+
homepage: https://github.com/carsonreinke/html-conditional-comment
|
93
|
+
licenses:
|
94
|
+
- MIT
|
95
|
+
metadata: {}
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.6.8
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Parse HTML conditional comments
|
116
|
+
test_files: []
|