simple_templates 0.8.7

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,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