simple_templates 0.8.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ module SimpleTemplates
2
+ class Parser
3
+ # A +Struct+ that takes a message to create a parser error
4
+ Error = Struct.new(:message)
5
+ end
6
+ end
@@ -0,0 +1,45 @@
1
+ require 'set'
2
+
3
+ module SimpleTemplates
4
+ class Parser
5
+ # A Base class for the Placeholders and Text parsers
6
+ class NodeParser
7
+
8
+ # The Base class doesn't accept any tokens for parsing, since it isn't
9
+ # supposed to be instantiated.
10
+ # @return [Set<Object>]
11
+ STARTING_TOKENS = Set[]
12
+
13
+ # Checks if the class is applicable for the first token in the list
14
+ # @param tokens <Array[SimpleTemplates::Lexer::Token]> a list of tokens
15
+ def self.applicable?(tokens)
16
+ tokens.any? && self::STARTING_TOKENS.include?(tokens.first.type)
17
+ end
18
+
19
+ # Initializes a new NodeParser. Please note that this class is not
20
+ # supposed to be instantiated.
21
+ # Raises an error if it is not applicable.
22
+ # @param unescapes [SimpleTemplates::Unescapes] a Unescapes object
23
+ # @param tokens <Array[SimpleTemplates::Lexer::Token]> a list of tokens
24
+ # @param allowed_placeholders <Array[String]> a list of allowed placeholders
25
+ def initialize(unescapes, tokens, allowed_placeholders)
26
+ raise ArgumentError, "Invalid Parser for String!" unless self.class.applicable?(tokens)
27
+
28
+ @unescapes = unescapes.to_h.clone.freeze
29
+ @tokens = tokens.clone.freeze
30
+
31
+ # Placeholders to match are mapped to strings for our validity checks.
32
+ # This is because if we go the other way and convert all possible
33
+ # placeholder names to symbols before comparing to the whitelist, we
34
+ # could cause a memory leak by allocating an infinite amount of symbols
35
+ # that won't be garbage-collected.
36
+ @allowed_placeholders = allowed_placeholders &&
37
+ allowed_placeholders.map(&:to_s).freeze
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :tokens, :allowed_placeholders, :unescapes
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,83 @@
1
+ require 'set'
2
+
3
+ require 'simple_templates/parser/node_parser'
4
+ require 'simple_templates/AST/placeholder'
5
+
6
+ module SimpleTemplates
7
+ class Parser
8
+ # Recognizes a set of input tokens as a Placeholder.
9
+ class Placeholder < NodeParser
10
+
11
+ # The expected tag order for a valid placeholder.
12
+ EXPECTED_TAG_ORDER = [:ph_start, :ph_name, :ph_end]
13
+
14
+ # The starting token that the input must have
15
+ STARTING_TOKENS = Set[:ph_start]
16
+
17
+ # If this stream starts with a placeholder token, parse out the
18
+ # Placeholder, or a Result with errors indicating the syntax problem.
19
+ # @return <Array <Array[SimpleTemplates::AST::Placeholder]>,
20
+ # <Array[SimpleTemplates::Parser::Error]>,
21
+ # <Array[SimpleTemplates::Lexer::Token]>> an +Array+ with the
22
+ # AST::Placeholder as first element, a list of parser errors and a list
23
+ # of the remaining tokens.
24
+ def parse
25
+ errors = check_placeholder_syntax
26
+
27
+ remaining_tokens = []
28
+
29
+ placeholder_ast = if errors.empty?
30
+ remaining_tokens = tokens[3..-1] || []
31
+
32
+ allowed = allowed_placeholders.nil? ||
33
+ allowed_placeholders.include?(tag_name.content)
34
+
35
+ [AST::Placeholder.new(tag_name.content, tag_start.pos, allowed)]
36
+ else
37
+ [] # we don't have an AST portion to return if we encountered errors
38
+ end
39
+
40
+ [placeholder_ast, errors, remaining_tokens]
41
+ end
42
+
43
+ private
44
+
45
+ def check_placeholder_syntax
46
+ expected_order_with_found_tokens = EXPECTED_TAG_ORDER.zip(tag_tokens)
47
+
48
+ errors = expected_order_with_found_tokens.
49
+ reduce([]) do |errs, (expected_type, found_tag)|
50
+
51
+ if found_tag.nil?
52
+ break errs << Parser::Error.new(
53
+ "Expected #{FRIENDLY_TAG_NAMES.fetch(expected_type)} token, but" +
54
+ " reached end of input.")
55
+
56
+ elsif expected_type != found_tag.type
57
+ break errs << Parser::Error.new(
58
+ "Expected #{FRIENDLY_TAG_NAMES.fetch(expected_type)} token at " +
59
+ "character position #{found_tag.pos}, but found a " +
60
+ "#{FRIENDLY_TAG_NAMES.fetch(found_tag.type)} token instead.")
61
+
62
+ else
63
+ # This token was expected at this point in the placeholder sequence,
64
+ # no need to add errors.
65
+ errs
66
+ end
67
+ end
68
+ end
69
+
70
+ def tag_tokens
71
+ tokens[0..2].compact
72
+ end
73
+
74
+ def tag_start
75
+ tokens[0]
76
+ end
77
+
78
+ def tag_name
79
+ tokens[1]
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,52 @@
1
+ require 'set'
2
+
3
+ require 'simple_templates/parser/node_parser'
4
+ require 'simple_templates/AST/text'
5
+
6
+ module SimpleTemplates
7
+ class Parser
8
+ #Recognizes a set of input tokens as a Text
9
+ class Text < NodeParser
10
+
11
+ # The starting tokens that the input can have
12
+ # @return [Set<Symbol>]
13
+ STARTING_TOKENS = Set[:quoted_ph_start, :quoted_ph_end, :text]
14
+
15
+ # A hash containing the method for a quoted placeholder start or end
16
+ # @return [Hash{ Symbol => Symbol }]
17
+ UNESCAPE_METHODS = {
18
+ quoted_ph_start: :start,
19
+ quoted_ph_end: :end
20
+ }
21
+
22
+ # It parses the stream, if it starts with a text node then it parses out
23
+ # the text until it is not applicable for the input anymore.
24
+ # @return <Array <Array[SimpleTemplates::AST::Text]>,
25
+ # <Array>,
26
+ # <Array[SimpleTemplates::Lexer::Token]>> an +Array+ with a list of
27
+ # AST::Text as first element, always an Empty list of Errors and the
28
+ # remaining unparsed tokens.
29
+ def parse
30
+ txt_node = nil
31
+ toks = tokens.dup
32
+
33
+ while self.class.applicable?(toks)
34
+ next_txt_token = toks.shift
35
+
36
+ this_txt_node =
37
+ AST::Text.new(unescape(next_txt_token), next_txt_token.pos, true)
38
+
39
+ txt_node = txt_node.nil? ? this_txt_node : txt_node + this_txt_node
40
+ end
41
+
42
+ [[txt_node], [], toks || []]
43
+ end
44
+
45
+ private
46
+
47
+ def unescape(token)
48
+ unescapes[UNESCAPE_METHODS[token.type]] || token.content
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,65 @@
1
+ require 'set'
2
+
3
+ require 'simple_templates/AST/placeholder'
4
+
5
+ module SimpleTemplates
6
+ #
7
+ # A +Template+ is a renderable collection of SimpleTemplates::AST nodes.
8
+ #
9
+ # @!attribute [r] ast
10
+ # @return <Array[SimpleTemplates::AST::Node]> a list of renderable nodes
11
+ # @!attribute [r] errors
12
+ # @return <Array[SimpleTemplates::Parser::Error]> a list of errors found
13
+ # during parsing
14
+ # @!attribute [r] remaining_tokens
15
+ # @return <Array[SimpleTemplates::Lexer::Token]> a list of the remaining
16
+ # not parsed tokens
17
+ #
18
+ class Template
19
+
20
+ attr_reader :ast, :errors, :remaining_tokens
21
+
22
+ # Initializes a new Template
23
+ # @param ast <Array[SimpleTemplates::AST::Node]> list of AST nodes
24
+ # @param errors <Array[SimpleTemplates::Parser::Error]> a list of errors
25
+ # found during parsing
26
+ # @param remaining_tokens <Array[SimpleTemplates::Lexer::Token]> list of
27
+ # unparsed tokens from the input
28
+ def initialize(ast = [], errors = [], remaining_tokens = [])
29
+ @ast = ast.clone.freeze
30
+ @errors = errors.clone.freeze
31
+ @remaining_tokens = remaining_tokens.clone.freeze
32
+ end
33
+
34
+ # Returns all placeholder names used in the template.
35
+ # @return [Set<String>] Placeholders content
36
+ def placeholder_names
37
+ placeholders.map(&:contents).to_set
38
+ end
39
+
40
+ # Accepts a hash with the placeholder names as keys and the values for
41
+ # substitution
42
+ # @param substitutions [Hash{Symbol => String}] a hash with the placeholder
43
+ # name as the key and the substitution for that placeholder as value
44
+ # @return [String] The concatenated result of rendering the substitutions
45
+ def render(substitutions)
46
+ raise errors.map(&:message).join(", ") unless errors.empty?
47
+ ast.map { |node| node.render(substitutions) }.join
48
+ end
49
+
50
+ # Returns all the +SimpleTemplates::AST::Placeholder+ nodes in the +ast+
51
+ # list of the instance
52
+ # @return [Set<SimpleTemplates::AST::Placeholder>]
53
+ def placeholders
54
+ ast.select{ |node| SimpleTemplates::AST::Placeholder === node }.to_set
55
+ end
56
+
57
+ # Compares a +Template+ with another by comparing the +ast+, +errors+
58
+ # and the +remaining_tokens+ of each one
59
+ def ==(other)
60
+ ast == other.ast &&
61
+ errors == other.errors &&
62
+ remaining_tokens == other.remaining_tokens
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ module SimpleTemplates
2
+ # A +Struct+ for the unescaped symbols. You want this to mark the placeholder
3
+ # tags. Takes a character for start and another for the end tag
4
+ Unescapes = Struct.new(:start, :end)
5
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleTemplates
2
+ VERSION = "0.8.7"
3
+ end
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ require File.expand_path("../lib/simple_templates/version.rb", __FILE__)
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "simple_templates"
8
+ s.version = SimpleTemplates::VERSION
9
+ s.authors = ["Michal Papis"]
10
+ s.email = ["support@stackbuilders.com"]
11
+ s.homepage = "https://github.com/stackbuilders/simple_templates"
12
+ s.summary = "Minimalistic templates engine"
13
+ s.license = "MIT"
14
+ s.files = `git ls-files`.split("\n")
15
+ s.executables << "simple-template"
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.required_ruby_version = ">= 2.0.0"
18
+ %w{rake minitest simplecov coveralls guard-minitest yard}.each do |name|
19
+ s.add_development_dependency(name)
20
+ end
21
+ s.add_development_dependency("guard", ">=2.12.8", "<3")
22
+ # s.add_development_dependency("smf-gem")
23
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module SimpleTemplates
4
+ module AST
5
+ class TestNodeImpl < Node
6
+ def render(context)
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ describe SimpleTemplates::AST::Node do
13
+ let(:target) {SimpleTemplates::AST::Node}
14
+ let(:allowed) {target.new("a", 0, true)}
15
+ let(:impl) {SimpleTemplates::AST::TestNodeImpl.new("a", 0, true)}
16
+
17
+ describe "==" do
18
+ it "is equal" do
19
+ allowed.must_equal target.new("a", 0, true)
20
+ end
21
+ it "isn't equal" do
22
+ allowed.wont_equal target.new("b", 0, true)
23
+ allowed.wont_equal target.new("a", 1, true)
24
+ allowed.wont_equal target.new("a", 0, false)
25
+ allowed.wont_equal target.new("b", 1, false)
26
+ end
27
+ end
28
+
29
+ describe "allowed?" do
30
+ it "is allowed?" do
31
+ allowed.allowed?.must_equal true
32
+ end
33
+ it "isn't allowed?" do
34
+ target.new("a", 0, false).allowed?.must_equal false
35
+ end
36
+ end
37
+
38
+ describe "render" do
39
+ let(:context) { {a: 1} }
40
+ it "fails" do
41
+ ->(){
42
+ allowed.render(context)
43
+ }.must_raise NotImplementedError
44
+ end
45
+ it "succeeds" do
46
+ impl.render(context).must_be_nil
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "../../test_helper"
2
+
3
+ describe SimpleTemplates::AST::Placeholder do
4
+ let(:target) {SimpleTemplates::AST::Placeholder}
5
+
6
+ describe "render" do
7
+
8
+ let(:substitutions) { { my_placeholder: 'result1' } }
9
+
10
+ it "fails when the placeholder name is not in the given Hash" do
11
+ ->(){
12
+ target.new('not_in_substitutions', 0, true).render(substitutions)
13
+ }.must_raise KeyError
14
+ end
15
+
16
+ it "fails when the placeholder is marked as invalid" do
17
+ ->(){
18
+ target.new('my_placeholder', 0, false).render(substitutions)
19
+ }.must_raise RuntimeError
20
+ end
21
+
22
+ it "succeeds" do
23
+ target.new('my_placeholder', 0, true).
24
+ render(substitutions).must_equal("result1")
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,23 @@
1
+ require_relative "../../test_helper"
2
+
3
+ describe SimpleTemplates::AST::Text do
4
+ let(:target) {SimpleTemplates::AST::Text}
5
+ let(:valid) {target.new("a", 0, true)}
6
+ let(:context) {Struct.new(:a).new("result1")}
7
+
8
+ describe "render" do
9
+ it "succeeds" do
10
+ valid.render(context).must_equal("a")
11
+ end
12
+ end
13
+
14
+ describe "+" do
15
+ it "adds valid" do
16
+ valid.+(target.new("b", 1, true)).must_equal target.new("ab", 0, true)
17
+ end
18
+ it "adds invalid" do
19
+ valid.+(target.new("b", 1, false)).must_equal target.new("ab", 0, false)
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,145 @@
1
+ require_relative "../test_helper"
2
+
3
+ describe SimpleTemplates::Lexer do
4
+ let(:delimiter) { SimpleTemplates::Delimiter.new(/\\</, /\\>/, /\</, /\>/) }
5
+ let(:token) { SimpleTemplates::Lexer::Token }
6
+
7
+ describe '#tokenize' do
8
+ it 'tokenizes a string with no placeholders' do
9
+ raw_input = 'string with no placeholders'
10
+ tokens = SimpleTemplates::Lexer.new(
11
+ delimiter,
12
+ raw_input
13
+ ).tokenize
14
+
15
+ tokens.must_equal [
16
+ token.new(:text, 'string with no placeholders', 0)
17
+ ]
18
+ end
19
+
20
+ it 'tokenizes a string with placeholders' do
21
+ raw_input = 'string with <placeholder>'
22
+ tokens = SimpleTemplates::Lexer.new(
23
+ delimiter,
24
+ raw_input
25
+ ).tokenize
26
+
27
+ tokens.must_equal [
28
+ token.new(:text, 'string with ', 0),
29
+ token.new(:ph_start, '<', 12),
30
+ token.new(:ph_name, 'placeholder', 13),
31
+ token.new(:ph_end, '>', 24)
32
+ ]
33
+ end
34
+
35
+ it 'tokenizes a string with invalid placeholders, containing a new line character' do
36
+ raw_input = "string with <foo\nbar> text"
37
+ tokens = SimpleTemplates::Lexer.new(
38
+ delimiter,
39
+ raw_input
40
+ ).tokenize
41
+
42
+ tokens.must_equal [
43
+ token.new(:text, 'string with ', 0),
44
+ token.new(:ph_start, '<', 12),
45
+ token.new(:ph_name, 'foo', 13),
46
+ token.new(:text, "\nbar", 16),
47
+ token.new(:ph_end, '>', 20),
48
+ token.new(:text, ' text', 21)
49
+ ]
50
+ end
51
+
52
+ it 'tokenizes a string with invalid placeholders, contains leading and trailing space' do
53
+ raw_input = "string with < foobar > text"
54
+ tokens = SimpleTemplates::Lexer.new(
55
+ delimiter,
56
+ raw_input
57
+ ).tokenize
58
+
59
+ tokens.must_equal [
60
+ token.new(:text, 'string with ', 0),
61
+ token.new(:ph_start, '<', 12),
62
+ token.new(:text, ' foobar ', 13),
63
+ token.new(:ph_end, '>', 21),
64
+ token.new(:text, ' text', 22)
65
+ ]
66
+ end
67
+
68
+ it 'tokenizes a string with placeholders and a newline character' do
69
+ raw_input = "string with <placeholder>\n Something else"
70
+ tokens = SimpleTemplates::Lexer.new(
71
+ delimiter,
72
+ raw_input
73
+ ).tokenize
74
+
75
+ tokens.must_equal [
76
+ token.new(:text, 'string with ', 0),
77
+ token.new(:ph_start, '<', 12),
78
+ token.new(:ph_name, 'placeholder', 13),
79
+ token.new(:ph_end, '>', 24),
80
+ token.new(:text, "\n Something else", 25)
81
+ ]
82
+ end
83
+
84
+ it 'tokenizes a string with invalid placeholder and an empty placeholder name' do
85
+ raw_input = "string with <> text"
86
+ tokens = SimpleTemplates::Lexer.new(
87
+ delimiter,
88
+ raw_input
89
+ ).tokenize
90
+
91
+ tokens.must_equal [
92
+ token.new(:text, 'string with ', 0),
93
+ token.new(:ph_start, '<', 12),
94
+ token.new(:ph_end, '>', 13),
95
+ token.new(:text, ' text', 14)
96
+ ]
97
+ end
98
+ it 'tokenizes a string with placeholders having a new line character in the placeholder name' do
99
+ raw_input = "string with <placeholder\n> Something else"
100
+ tokens = SimpleTemplates::Lexer.new(
101
+ delimiter,
102
+ raw_input
103
+ ).tokenize
104
+
105
+ tokens.must_equal [
106
+ token.new(:text, 'string with ', 0),
107
+ token.new(:ph_start, '<', 12),
108
+ token.new(:ph_name, 'placeholder', 13),
109
+ token.new(:text, "\n", 24),
110
+ token.new(:ph_end, '>', 25),
111
+ token.new(:text, " Something else", 26)
112
+ ]
113
+ end
114
+
115
+ it 'tokenizes a string with a placeholder containing numbers' do
116
+ raw_input = 'string with <1placeholder1>'
117
+ tokens = SimpleTemplates::Lexer.new(
118
+ delimiter,
119
+ raw_input
120
+ ).tokenize
121
+
122
+ tokens.must_equal [
123
+ token.new(:text, 'string with ', 0),
124
+ token.new(:ph_start, '<', 12),
125
+ token.new(:ph_name, '1placeholder1', 13),
126
+ token.new(:ph_end, '>', 26),
127
+ ]
128
+ end
129
+
130
+ it 'tokenizes a string with a placeholder containing underscores' do
131
+ raw_input = 'string with <_place_holder_>'
132
+ tokens = SimpleTemplates::Lexer.new(
133
+ delimiter,
134
+ raw_input
135
+ ).tokenize
136
+
137
+ tokens.must_equal [
138
+ token.new(:text, 'string with ', 0),
139
+ token.new(:ph_start, '<', 12),
140
+ token.new(:ph_name, '_place_holder_', 13),
141
+ token.new(:ph_end, '>', 27),
142
+ ]
143
+ end
144
+ end
145
+ end