bob-compiler 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +22 -0
- data/README.md +8 -0
- data/Rakefile +9 -0
- data/bob-compiler.gemspec +27 -0
- data/lib/bob/compiler.rb +8 -0
- data/lib/bob/compiler/base.rb +132 -0
- data/lib/bob/compiler/block.rb +21 -0
- data/lib/bob/compiler/buffer.rb +79 -0
- data/lib/bob/compiler/editable.rb +86 -0
- data/lib/bob/compiler/layout.rb +51 -0
- data/lib/bob/compiler/message.rb +21 -0
- data/lib/bob/compiler/substitution.rb +57 -0
- data/lib/bob/compiler/template.rb +90 -0
- data/lib/bob/compiler/version.rb +5 -0
- data/lib/bob/compiler/walker.rb +112 -0
- data/test/blocks_test.rb +82 -0
- data/test/fixtures/blocks/bar.html +10 -0
- data/test/fixtures/blocks/bar.js +63 -0
- data/test/fixtures/blocks/foo.html +4 -0
- data/test/fixtures/blocks/foo.js +30 -0
- data/test/fixtures/blocks/foobar.html +5 -0
- data/test/fixtures/blocks/foobar.js +23 -0
- data/test/fixtures/blocks/substitions.html +15 -0
- data/test/fixtures/blocks/substitions.js +56 -0
- data/test/fixtures/layout/simple.html +31 -0
- data/test/fixtures/layout/simple.js +66 -0
- data/test/fixtures/templates/simple.html +55 -0
- data/test/fixtures/templates/simple.js +168 -0
- data/test/layouts_test.rb +76 -0
- data/test/templates_test.rb +51 -0
- data/test/test_helper.rb +43 -0
- metadata +180 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
require "bob/compiler/walker"
|
2
|
+
require "bob/compiler/editable"
|
3
|
+
|
4
|
+
module Bob
|
5
|
+
module Compiler
|
6
|
+
class Layout < Walker
|
7
|
+
include Editable
|
8
|
+
|
9
|
+
attr_reader :container
|
10
|
+
|
11
|
+
private
|
12
|
+
def compile
|
13
|
+
super
|
14
|
+
|
15
|
+
if container.nil?
|
16
|
+
error("No container defined", @node.children.try(:last) || @node)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def after_element(node, root)
|
21
|
+
if is_container?(node)
|
22
|
+
if container
|
23
|
+
warn "Multiple containers found, extra contains are ignored", node
|
24
|
+
else
|
25
|
+
assign_container(node)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def filtered_children(node)
|
33
|
+
is_container?(node) ? [] : super
|
34
|
+
end
|
35
|
+
|
36
|
+
def is_container?(node)
|
37
|
+
node.has_attribute?("bob-container")
|
38
|
+
end
|
39
|
+
|
40
|
+
def assign_container(node)
|
41
|
+
@container = node
|
42
|
+
|
43
|
+
@buffer << <<-JS.strip_heredoc.chomp
|
44
|
+
descriptor.container = {
|
45
|
+
element: dom.getElement()
|
46
|
+
};
|
47
|
+
JS
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Bob
|
2
|
+
module Compiler
|
3
|
+
class Message
|
4
|
+
attr_reader :message, :line
|
5
|
+
|
6
|
+
def initialize(message, node_or_line_number)
|
7
|
+
@message = message
|
8
|
+
@line = extract_line_number(node_or_line_number)
|
9
|
+
end
|
10
|
+
|
11
|
+
def <=>(other)
|
12
|
+
line <=> other.line
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def extract_line_number(node_or_line_number)
|
17
|
+
node_or_line_number.line rescue node_or_line_number
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Bob
|
2
|
+
module Compiler
|
3
|
+
module Substitution
|
4
|
+
SUBSTITUTION_SCANNER = /(\\?{{.+?}})/
|
5
|
+
|
6
|
+
SUBSTITUTION_PARSER = /
|
7
|
+
\A
|
8
|
+
{{
|
9
|
+
[[:blank:]]*
|
10
|
+
(?<path> [0-9a-z_]+ (?:\.[0-9a-z_]+)* )
|
11
|
+
[[:blank:]]*
|
12
|
+
(?:
|
13
|
+
\|\|
|
14
|
+
[[:blank:]]*
|
15
|
+
(?:
|
16
|
+
(?: ' (?<fallback> (?:[^'\\{}]|\\.)*) ' ) |
|
17
|
+
(?: " (?<fallback> (?:[^"\\{}]|\\.)*) " )
|
18
|
+
)
|
19
|
+
[[:blank:]]*
|
20
|
+
)?
|
21
|
+
}}
|
22
|
+
\z
|
23
|
+
/xi
|
24
|
+
|
25
|
+
def quote_and_substitute(arg, node)
|
26
|
+
parts = arg.split(SUBSTITUTION_SCANNER).reject(&:empty?)
|
27
|
+
|
28
|
+
if parts.empty?
|
29
|
+
%{""}
|
30
|
+
else
|
31
|
+
parts.map { |str|
|
32
|
+
|
33
|
+
if SUBSTITUTION_SCANNER =~ str && str[0] != "/"
|
34
|
+
if (parsed = SUBSTITUTION_PARSER.match(str))
|
35
|
+
if parsed[:fallback]
|
36
|
+
"env.get(#{quote parsed[:path]}, #{quote unescape parsed[:fallback]})"
|
37
|
+
else
|
38
|
+
"env.get(#{quote parsed[:path]})"
|
39
|
+
end
|
40
|
+
else
|
41
|
+
warn "Treating invalid substitution as string: #{str.inspect}", node.parent.line
|
42
|
+
quote str
|
43
|
+
end
|
44
|
+
else
|
45
|
+
quote str
|
46
|
+
end
|
47
|
+
|
48
|
+
}.join(" + ")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def unescape(str)
|
53
|
+
str.gsub(/\\(.)/, "\\1")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require "bob/compiler/base"
|
3
|
+
require "bob/compiler/layout"
|
4
|
+
require "bob/compiler/block"
|
5
|
+
require "bob/compiler/editable"
|
6
|
+
|
7
|
+
module Bob
|
8
|
+
module Compiler
|
9
|
+
class Template < Base
|
10
|
+
def initialize(name, template_string)
|
11
|
+
super()
|
12
|
+
@name = name
|
13
|
+
@template_string = template_string
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def compile
|
18
|
+
doc = Nokogiri::HTML(@template_string)
|
19
|
+
key = sanitize_identifier(@name)
|
20
|
+
|
21
|
+
doc.errors.each do |error|
|
22
|
+
warn(error.message, error.line)
|
23
|
+
end
|
24
|
+
|
25
|
+
unless doc.root
|
26
|
+
error("Parse error", 0)
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
@buffer << 'Bob.registerTemplate(' << quote(key) << ', {'
|
31
|
+
|
32
|
+
@buffer.indented do
|
33
|
+
if @template_string =~ /<!DOCTYPE/i
|
34
|
+
@buffer.blankline!
|
35
|
+
@buffer << "doctype: " << quote(doc.internal_subset.to_xhtml) << ","
|
36
|
+
end
|
37
|
+
|
38
|
+
@buffer.blankline!
|
39
|
+
|
40
|
+
@buffer << "name: " << quote(@name) << ","
|
41
|
+
|
42
|
+
@buffer.blankline!
|
43
|
+
|
44
|
+
@buffer << "layout: "
|
45
|
+
|
46
|
+
compiled_layout = Layout.new(doc.root)
|
47
|
+
|
48
|
+
@warnings += compiled_layout.warnings
|
49
|
+
@errors += compiled_layout.errors
|
50
|
+
|
51
|
+
@buffer.merge(compiled_layout.buffer) << ","
|
52
|
+
|
53
|
+
@buffer.blankline!
|
54
|
+
|
55
|
+
@buffer << "blocks: {"
|
56
|
+
|
57
|
+
if compiled_layout.container
|
58
|
+
@buffer.indented do
|
59
|
+
|
60
|
+
compiled_layout.container.children.each do |block_element|
|
61
|
+
next unless block_element.element?
|
62
|
+
|
63
|
+
compiled_block = Block.new(block_element)
|
64
|
+
|
65
|
+
@warnings += compiled_block.warnings
|
66
|
+
@errors += compiled_block.errors
|
67
|
+
|
68
|
+
next unless compiled_block.name
|
69
|
+
|
70
|
+
@buffer.blankline!
|
71
|
+
|
72
|
+
@buffer << quote(compiled_block.name) << ": "
|
73
|
+
|
74
|
+
@buffer.merge(compiled_block.buffer) << ","
|
75
|
+
end
|
76
|
+
|
77
|
+
@buffer.blankline!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
@buffer << "}"
|
82
|
+
end
|
83
|
+
|
84
|
+
@buffer.blankline!
|
85
|
+
|
86
|
+
@buffer << "});"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'bob/compiler/base'
|
2
|
+
require 'bob/compiler/buffer'
|
3
|
+
require 'bob/compiler/editable'
|
4
|
+
require 'bob/compiler/substitution'
|
5
|
+
|
6
|
+
module Bob
|
7
|
+
module Compiler
|
8
|
+
class Walker < Base
|
9
|
+
include Substitution
|
10
|
+
|
11
|
+
def initialize(node)
|
12
|
+
super()
|
13
|
+
@node = node
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def compile
|
18
|
+
@buffer << "function(dom, env) {"
|
19
|
+
|
20
|
+
@buffer.indented do
|
21
|
+
@buffer << "var descriptor = #{pretty_quote descriptor};"
|
22
|
+
|
23
|
+
@buffer.blankline!
|
24
|
+
|
25
|
+
walk(@node, true)
|
26
|
+
|
27
|
+
@buffer.blankline!
|
28
|
+
|
29
|
+
@buffer << "descriptor.element = dom.getElement();"
|
30
|
+
|
31
|
+
@buffer.blankline!
|
32
|
+
|
33
|
+
@buffer << "return descriptor;"
|
34
|
+
end
|
35
|
+
|
36
|
+
@buffer << "}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def descriptor
|
40
|
+
{
|
41
|
+
element: nil,
|
42
|
+
editables: []
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def walk(node, root = false)
|
47
|
+
if node.element?
|
48
|
+
visit_element(node, root)
|
49
|
+
elsif node.text? || node.cdata?
|
50
|
+
visit_text_node(node)
|
51
|
+
elsif node.comment?
|
52
|
+
visit_comment_node(node)
|
53
|
+
else
|
54
|
+
raise TypeError, "Unsupported node type #{node.type}"
|
55
|
+
end
|
56
|
+
|
57
|
+
if node.element?
|
58
|
+
filtered_children(node).each do |child|
|
59
|
+
walk(child)
|
60
|
+
end
|
61
|
+
|
62
|
+
after_element(node, root)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def filtered_children(node)
|
67
|
+
node.children.select { |child| child.element? || child.text? || child.cdata? || child.comment? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def filtered_attributes(node)
|
71
|
+
# TODO: switch to a whitelist
|
72
|
+
node.attribute_nodes.reject { |attr| attr.name.start_with?('bob-') }
|
73
|
+
end
|
74
|
+
|
75
|
+
def visit_element(node, root)
|
76
|
+
build_element(node, root)
|
77
|
+
end
|
78
|
+
|
79
|
+
def after_element(node, root)
|
80
|
+
append_element(node) unless root
|
81
|
+
end
|
82
|
+
|
83
|
+
def visit_text_node(node)
|
84
|
+
build_text_node(node)
|
85
|
+
end
|
86
|
+
|
87
|
+
def visit_comment_node(node)
|
88
|
+
build_comment_node(node)
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_element(node, root)
|
92
|
+
@buffer << "dom.createElement(#{quote node.node_name});"
|
93
|
+
|
94
|
+
filtered_attributes(node).each do |attr|
|
95
|
+
@buffer << "dom.setAttribute(#{quote attr.name}, #{quote_and_substitute attr.value, node});"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def append_element(node)
|
100
|
+
@buffer << "dom.appendElement();"
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_text_node(node)
|
104
|
+
@buffer << "dom.appendTextNode(#{quote_and_substitute node.text, node});"
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_comment_node(node)
|
108
|
+
@buffer << "dom.appendComment(#{quote_and_substitute node.text, node});"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/test/blocks_test.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
require 'bob/compiler/block'
|
5
|
+
|
6
|
+
class BlocksTest < Minitest::Test
|
7
|
+
include FixtureHelpers
|
8
|
+
|
9
|
+
self.fixture_path << '/blocks'
|
10
|
+
|
11
|
+
fixtures.each do |name|
|
12
|
+
test "Fixture #{name}" do
|
13
|
+
input = fixture "#{name}.html"
|
14
|
+
expected = fixture "#{name}.js"
|
15
|
+
|
16
|
+
assert_equal expected, compile(input)
|
17
|
+
assert_predicate @compiled, :valid?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
[" abc ", " !@#abc^& ", "AbC", ""abc""].each do |identifier|
|
22
|
+
test "Invalid identifier: #{identifier.inspect}" do
|
23
|
+
compile %{<div bob-block="#{identifier}"></div>}
|
24
|
+
|
25
|
+
assert_predicate @compiled, :valid?
|
26
|
+
refute_predicate @compiled, :well_formed?
|
27
|
+
assert_predicate @compiled, :warnings?
|
28
|
+
refute_predicate @compiled, :errors?
|
29
|
+
|
30
|
+
assert_includes @compiled.warnings.first.message, "does not contain a valid identifier"
|
31
|
+
assert_includes @compiled.warnings.first.message, "assuming you meant"
|
32
|
+
assert_equal 1, @compiled.warnings.first.line
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
["!@#^& ", """"].each do |identifier|
|
37
|
+
test "Invalid identifier: #{identifier.inspect}" do
|
38
|
+
compile %{<div bob-block="#{identifier}"></div>}
|
39
|
+
|
40
|
+
refute_predicate @compiled, :valid?
|
41
|
+
refute_predicate @compiled, :well_formed?
|
42
|
+
refute_predicate @compiled, :warnings?
|
43
|
+
assert_predicate @compiled, :errors?
|
44
|
+
|
45
|
+
assert_includes @compiled.errors.first.message, "does not contain a valid identifier"
|
46
|
+
assert_equal 1, @compiled.errors.first.line
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
["", " \t "].each do |identifier|
|
51
|
+
test "Blank identifier: #{identifier.inspect}" do
|
52
|
+
compile %{<div bob-block="#{identifier}"></div>}
|
53
|
+
|
54
|
+
refute_predicate @compiled, :valid?
|
55
|
+
refute_predicate @compiled, :well_formed?
|
56
|
+
refute_predicate @compiled, :warnings?
|
57
|
+
assert_predicate @compiled, :errors?
|
58
|
+
|
59
|
+
assert_includes @compiled.errors.first.message, "cannot be blank"
|
60
|
+
assert_equal 1, @compiled.errors.first.line
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
test "Illegal substitution" do
|
65
|
+
compile %[<div bob-block="zomg">{{ not valid }}</div>]
|
66
|
+
|
67
|
+
assert_predicate @compiled, :valid?
|
68
|
+
refute_predicate @compiled, :well_formed?
|
69
|
+
assert_predicate @compiled, :warnings?
|
70
|
+
refute_predicate @compiled, :errors?
|
71
|
+
|
72
|
+
assert_includes @compiled.warnings.first.message, "invalid substitution"
|
73
|
+
assert_equal 1, @compiled.warnings.first.line
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def compile(str)
|
78
|
+
fragment = Nokogiri::HTML::DocumentFragment.parse(str)
|
79
|
+
@compiled = Bob::Compiler::Block.new(fragment.children.first)
|
80
|
+
@compiled.result
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<div bob-block="bar" class="bar">
|
2
|
+
<!-- BEGIN HEADER -->
|
3
|
+
<div class="header" style="line-height: 2" data-rick>
|
4
|
+
<h2 bob-editable="plaintext">Foo</h2>
|
5
|
+
<div bob-editable="html"></div>
|
6
|
+
</div>
|
7
|
+
<!-- END HEADER -->
|
8
|
+
<hr />
|
9
|
+
<div style="font-size: 11pt" bob-editable="html"></div>
|
10
|
+
</div>
|
@@ -0,0 +1,63 @@
|
|
1
|
+
function(dom, env) {
|
2
|
+
var descriptor = {
|
3
|
+
"type": "bar",
|
4
|
+
"element": null,
|
5
|
+
"editables": [
|
6
|
+
|
7
|
+
]
|
8
|
+
};
|
9
|
+
|
10
|
+
dom.createElement("div");
|
11
|
+
dom.setAttribute("class", "bar");
|
12
|
+
dom.appendTextNode("\n ");
|
13
|
+
dom.appendComment(" BEGIN HEADER ");
|
14
|
+
dom.appendTextNode("\n ");
|
15
|
+
dom.createElement("div");
|
16
|
+
dom.setAttribute("class", "header");
|
17
|
+
dom.setAttribute("style", "line-height: 2");
|
18
|
+
dom.setAttribute("data-rick", "");
|
19
|
+
dom.appendTextNode("\n ");
|
20
|
+
dom.createElement("h2");
|
21
|
+
dom.appendTextNode("Foo");
|
22
|
+
descriptor.editables.push({
|
23
|
+
type: "plaintext",
|
24
|
+
name: "plaintext0",
|
25
|
+
element: dom.getElement(),
|
26
|
+
options: {
|
27
|
+
}
|
28
|
+
});
|
29
|
+
dom.appendElement();
|
30
|
+
dom.appendTextNode("\n ");
|
31
|
+
dom.createElement("div");
|
32
|
+
descriptor.editables.push({
|
33
|
+
type: "html",
|
34
|
+
name: "html0",
|
35
|
+
element: dom.getElement(),
|
36
|
+
options: {
|
37
|
+
}
|
38
|
+
});
|
39
|
+
dom.appendElement();
|
40
|
+
dom.appendTextNode("\n ");
|
41
|
+
dom.appendElement();
|
42
|
+
dom.appendTextNode("\n ");
|
43
|
+
dom.appendComment(" END HEADER ");
|
44
|
+
dom.appendTextNode("\n ");
|
45
|
+
dom.createElement("hr");
|
46
|
+
dom.appendElement();
|
47
|
+
dom.appendTextNode("\n ");
|
48
|
+
dom.createElement("div");
|
49
|
+
dom.setAttribute("style", "font-size: 11pt");
|
50
|
+
descriptor.editables.push({
|
51
|
+
type: "html",
|
52
|
+
name: "html1",
|
53
|
+
element: dom.getElement(),
|
54
|
+
options: {
|
55
|
+
}
|
56
|
+
});
|
57
|
+
dom.appendElement();
|
58
|
+
dom.appendTextNode("\n");
|
59
|
+
|
60
|
+
descriptor.element = dom.getElement();
|
61
|
+
|
62
|
+
return descriptor;
|
63
|
+
}
|