bob-compiler 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 +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
|
+
}
|