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.
@@ -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
+ }