bob-compiler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module Bob
2
+ module Compiler
3
+ VERSION = "0.0.1"
4
+ end
5
+ 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
@@ -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", "&quot;abc&quot;"].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
+ ["!@#^& ", "&quot;&quot;"].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
+ }