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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 08fc7fb9189730ce991dc231ca4eb5de54768e86
4
+ data.tar.gz: bd1845c3b3f7546f69c066e9a1681c8cd2cac5e9
5
+ SHA512:
6
+ metadata.gz: 1d1921e2541648638553bfaaf71854956c166c785fbffb60b62e88132ae84d340df3d06b86cedcfacebfe49fdfd35cd466954b3dfdb9fee789ed630e265c5f11
7
+ data.tar.gz: 079629726b5675b0ad18fb41b27152b43fc7997049b97fd5311fa8afbdb132e7c066e7a277c51c531285e5003b9a1eeef3f554637702f565a692bb34fac9990c
@@ -0,0 +1,6 @@
1
+ Gemfile.lock
2
+ coverage/
3
+ *.gem
4
+ *.DS_Store
5
+ .yardoc/
6
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ #ruby=ruby
4
+
5
+ gemspec
@@ -0,0 +1,30 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features)
6
+
7
+ ## Uncomment to clear the screen before every task
8
+ # clearing :on
9
+
10
+ ## Guard internally checks for changes in the Guardfile and exits.
11
+ ## If you want Guard to automatically start up again, run guard in a
12
+ ## shell loop, e.g.:
13
+ ##
14
+ ## $ while bundle exec guard; do echo "Restarting Guard..."; done
15
+ ##
16
+ ## Note: if you are using the `directories` clause above and you are not
17
+ ## watching the project directory ('.'), then you will want to move
18
+ ## the Guardfile to a watched dir and symlink it back, e.g.
19
+ #
20
+ # $ mkdir config
21
+ # $ mv Guardfile config/
22
+ # $ ln -s config/Guardfile .
23
+ #
24
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
25
+
26
+ guard :minitest, :bundler => false do
27
+ watch(%r{^test/(.*)\/?(.*)_test\.rb$})
28
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
29
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
30
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Stack Builders, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,62 @@
1
+ # simple_templates
2
+
3
+ [![Circle CI](https://circleci.com/gh/stackbuilders/simple_templates.svg?style=shield&circle-token=caa5840efa6767d08ac3082270580367327a8906)](https://circleci.com/gh/stackbuilders/simple_templates)
4
+
5
+ `simple_templates` is a minimalistic templates engine. This gem allows you to
6
+ work with several types of templates.
7
+
8
+ ##Installation
9
+
10
+ Clone the project
11
+ ```
12
+ git@github.com:stackbuilders/simple_templates.git
13
+ ```
14
+
15
+ Install the dependencies
16
+ ```
17
+ bundle install
18
+ ```
19
+
20
+ Run the tests
21
+ ```
22
+ rake test
23
+ ```
24
+
25
+ ##Quick Start
26
+
27
+ The basic use of the library can be seen like this:
28
+
29
+ You can send a `String` with the raw input that includes your placeholders and
30
+ a list of `String` containing the allowed placeholders, if it is `nil`, then all
31
+ the placeholders are allowed.
32
+
33
+ A example without errors, that allows us to call the method `render`
34
+ ```ruby
35
+ template = SimpleTemplates.parse("Hi <name>", %w[name])
36
+ template.render({ name: "Bob" }) if template.errors.empty?
37
+ => "Hi Bob"
38
+ template.remaining_tokens
39
+ => []
40
+ ```
41
+
42
+ An example with errors. Since the allowed placeholder is not in the raw input.
43
+ So we get are going to get a list of errors when parsing
44
+ ```ruby
45
+ template = SimpleTemplates.parse("Hi <name>", %w[date])
46
+ template.errors
47
+ => [...] # unknown placeholder
48
+ ```
49
+
50
+ ##Tasks
51
+ The default task executed by `rake` is only
52
+ ```
53
+ rake test
54
+ ```
55
+
56
+ Additionally you can generate the documentation by running
57
+ ```
58
+ rake docs
59
+ ```
60
+
61
+ ##License
62
+ MIT. See [LICENSE](https://github.com/stackbuilders/simple_templates/blob/master/LICENSE)
@@ -0,0 +1,20 @@
1
+ require "rake/testtask"
2
+
3
+ task :default => [:test]
4
+
5
+ begin
6
+ require "yard"
7
+ YARD::Rake::YardocTask.new do |t|
8
+ t.files = ["lib/**/*.rb"]
9
+ t.stats_options = ["--list-undoc", "--compact"]
10
+ end
11
+
12
+ task :docs => [:yard]
13
+ rescue LoadError
14
+ end
15
+
16
+ Rake::TestTask.new do |t|
17
+ t.verbose = true
18
+ t.libs.push("demo", "test")
19
+ t.pattern = "test/**/*_test.rb"
20
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib_dir = File.expand_path("../../lib", __FILE__)
4
+ $:<<lib_dir unless $:.include?(lib_dir)
5
+
6
+ require 'simple_templates'
7
+
8
+ template = ARGV.shift or
9
+ abort "Usage: #{$0} \"<person> has <pet>\" person:Bob pet:dog"
10
+
11
+ args = Hash[ARGV.map{|v| k,v = v.split(":",2); [k.to_sym, v]}]
12
+
13
+ puts SimpleTemplates.parse(template).render(args)
@@ -0,0 +1,38 @@
1
+ require 'simple_templates/template'
2
+ require 'simple_templates/lexer'
3
+ require 'simple_templates/parser'
4
+ require 'simple_templates/unescapes'
5
+ require 'simple_templates/delimiter'
6
+
7
+ # A minimalistic templates engine
8
+ module SimpleTemplates
9
+ #
10
+ # Builds a template renderer from given string template and list of
11
+ # allowed placeholders
12
+ #
13
+ # @param raw_template_string String the template to render
14
+ # @param allowed_placeholders Array[String] list of allowed placeholders
15
+ # @return [<SimpleTemplates::Template>]
16
+ # A template cointaining a list of ASTs, errors and unparsed tokens
17
+ #
18
+ # @example template without errors
19
+ # template = SimpleTemplates.parse("Hi <name>", %w[name])
20
+ # template.render({ name: "Bob" }) if template.errors.empty?
21
+ # => "Hi Bob"
22
+ #
23
+ # @example template with errors
24
+ # template = SimpleTemplates.parse("Hi <name>", %w[date])
25
+ # template.errors
26
+ # => [...] # unknown placeholder
27
+ #
28
+ def self.parse(raw_template_string, allowed_placeholders = nil)
29
+ Template.new(
30
+ *Parser.new(
31
+ Unescapes.new('<', '>'),
32
+ Lexer.new(Delimiter.new(/\\</, /\\>/, /\</, /\>/), raw_template_string).
33
+ tokenize,
34
+ allowed_placeholders
35
+ ).parse
36
+ )
37
+ end
38
+ end
@@ -0,0 +1,59 @@
1
+ module SimpleTemplates
2
+ # A module with the Abstract Syntax Tree for The Templates
3
+ module AST
4
+ #
5
+ # Parent class for a node in the AST.
6
+ # This class is not supposed to be instantiated.
7
+ #
8
+ # @!attribute [r] contents
9
+ # @return [String] the content of the node
10
+ # @!attribute [r] pos
11
+ # @return [Number] the position of the content in the input
12
+ # @!attribute [r] allowed
13
+ # @return [Boolean] if the node is allowed. (see allowed?)
14
+ class Node
15
+ attr_reader :contents, :pos, :allowed
16
+
17
+ # Initializes a new Node. Please note that this class is not supposed to
18
+ # be instantiated.
19
+ # @param contents [String] the content of the node
20
+ # @param pos [Numbers] the position of the content in the input
21
+ # @param allowed [Boolean] if the node is allowed
22
+ def initialize(contents, pos, allowed)
23
+ @contents = contents
24
+ @pos = pos
25
+ @allowed = allowed
26
+ end
27
+
28
+ # Compares the node to other node by comparing the attributes of the
29
+ # objects.
30
+ # @param other [SimpleTemplates::AST::Node]
31
+ # @return [Boolean]
32
+ def ==(other)
33
+ contents == other.contents && pos == other.pos && allowed == other.allowed
34
+ end
35
+
36
+ # Checks if the Node is allowed by returning the value in the class
37
+ # attribute +allowed+.
38
+ def allowed?
39
+ allowed
40
+ end
41
+
42
+ # Returns only the name of the class without the namespace.
43
+ # @return [String]
44
+ def name
45
+ self.class.to_s.split('::').last
46
+ end
47
+
48
+ # Not implemented method to render a Node that must be especialized by
49
+ # the class inheriting from this class.
50
+ # @param context [Hash{ Symbol => String }]
51
+ # @return [String] substituded contexts
52
+ # :nocov:
53
+ def render(context)
54
+ raise NotImplementedError
55
+ end
56
+ # :nocov:
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,25 @@
1
+ require 'simple_templates/AST/node'
2
+
3
+ module SimpleTemplates
4
+ module AST
5
+ #
6
+ # A Placeholder specialized Node that implements the +render+ method for
7
+ # placeholders
8
+ #
9
+ class Placeholder < Node
10
+ #
11
+ # Renders the substitutions in the input.
12
+ # Raises an error if it is not allowed. (see allowed?)
13
+ # @param substitutions [Hash{ Symbol => String }]
14
+ # @return [String] the content of the placeholder
15
+ #
16
+ def render(substitutions)
17
+ if allowed?
18
+ substitutions.fetch(contents.to_sym)
19
+ else
20
+ raise 'Unable to render invalid placeholder!'
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require 'simple_templates/AST/node'
2
+
3
+ module SimpleTemplates
4
+ module AST
5
+ #
6
+ # A Text specialized Node that implements the +render+ method for text
7
+ # inputs
8
+ #
9
+ class Text < Node
10
+ # Renders the content of the node. It doesn't use the context just takes
11
+ # the class contents.
12
+ # @param context [Hash{ Symbol => String }]
13
+ # @return [String] returns the +contents+ of the class since it doesn't
14
+ # apply any substitution
15
+ def render(context)
16
+ contents
17
+ end
18
+
19
+ # Appends the content of the Text node to another node, keeping the
20
+ # position and checking if both are allowed or not.
21
+ # @param other [SimpleTemplates::AST::Text]
22
+ # @return [SimpleTemplates::AST::Text]
23
+ def +(other)
24
+ Text.new(contents + other.contents, pos, allowed && other.allowed)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ module SimpleTemplates
2
+
3
+ # A +Struct+ for a Delimiter that takes +Regexp+ for the quoted start and
4
+ # quoted end of the placeholder as well as the start and end
5
+ Delimiter = Struct.new(:quoted_ph_start, :quoted_ph_end, :ph_start, :ph_end)
6
+ end
@@ -0,0 +1,73 @@
1
+ require 'strscan'
2
+
3
+ module SimpleTemplates
4
+
5
+ # The +SimpleTemplates::Lexer+ tokenizes the raw input into a more usable form
6
+ # for the +SimpleTemplates::Parser+.
7
+
8
+ class Lexer
9
+
10
+ # A +Struct+ for a Lexer::Token that takes the type, the content and
11
+ # position of a token.
12
+ Token = Struct.new(:type, :content, :pos)
13
+
14
+ # A Hash with the allowed +Regexp+ for a valid placeholder name
15
+ # @return [Hash{Symbol => Regexp}] a hash with the allowed Regexp for the
16
+ # placeholder name +:ph_name+
17
+ VALID_PLACEHOLDER = { ph_name: /[A-Za-z0-9_]+/ }.freeze
18
+
19
+ # Initializes a new Lexer
20
+ # @param delimiter [SimpleTemplates::Delimiter] a delimiter object
21
+ # @param input [String] a raw input for the lexer
22
+ def initialize(delimiter, input)
23
+ @input = input.clone.freeze
24
+ @matchers = delimiter.to_h.merge(text: /./m).freeze
25
+ @matchers_with_placeholder_name = VALID_PLACEHOLDER.merge(@matchers)
26
+ end
27
+
28
+ # Tokenizes the raw input and returns a list of tokens.
29
+ # @return <Array[SimpleTemplates::Lexer::Token]>
30
+ def tokenize
31
+ tokens = []
32
+ ss = StringScanner.new(@input)
33
+
34
+ until ss.eos?
35
+ tok = next_token(tokens, ss)
36
+
37
+ if tokens.any? && tok.type == :text && tokens.last.type == :text
38
+ tokens.last.content += tok.content
39
+ else
40
+ tokens << tok
41
+ end
42
+ end
43
+
44
+ tokens
45
+ end
46
+
47
+ private
48
+
49
+ # Returns a new token and moves to the next position in the +StringScanner+
50
+ # @param tokens <Array[SimpleTemplates::Lexer::Token]> list of tokens
51
+ # @param ss [StringScanner] a +StringScanner+ for the input
52
+ # @return [SimpleTemplates::Lexer::Token] the next token
53
+ def next_token(tokens, ss)
54
+ pos = ss.pos
55
+ token_type, _ = current_matchers(tokens).find { |_, pattern| ss.scan(pattern) }
56
+
57
+ Token.new(token_type, ss.matched, pos)
58
+ end
59
+
60
+ # Checks if the last token was the start of a placeholder to use include
61
+ # the placeholder name +:ph_name+ in the hash of matchers
62
+ # @param tokens <Array[SimpleTemplates::Lexer::Token]> the list of tokens
63
+ # @return [Hash {Symbol => Regexp}]
64
+ # (see #next_token)
65
+ def current_matchers(tokens)
66
+ if tokens.any? && tokens.last.type == :ph_start
67
+ @matchers_with_placeholder_name
68
+ else
69
+ @matchers
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,111 @@
1
+ require 'simple_templates/parser/error'
2
+ require 'simple_templates/parser/placeholder'
3
+ require 'simple_templates/parser/text'
4
+
5
+ module SimpleTemplates
6
+ # Parsing the SimpleTemplate means verifying that there are no malformed tags,
7
+ # and all tags are in the 'allowed' list.
8
+ class Parser
9
+
10
+ # A +static+ Hash with the meaning of each tag name
11
+ # @return [Hash{Symbol => String}]
12
+ FRIENDLY_TAG_NAMES = {
13
+ ph_start: 'placeholder start',
14
+ ph_name: 'placeholder name',
15
+ ph_end: 'placeholder end',
16
+ quoted_ph_start: 'quoted placeholder start',
17
+ quoted_ph_end: 'quoted placeholder end',
18
+ text: 'text'
19
+ }.freeze
20
+
21
+ # Initializes a new Parser.
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 with allowed
25
+ # placeholders if is left as nil, then all placeholders are permitted
26
+ def initialize(unescapes, tokens, allowed_placeholders)
27
+ @unescapes = unescapes.clone.freeze
28
+ @tokens = tokens.clone.freeze
29
+
30
+ @allowed_placeholders = allowed_placeholders &&
31
+ allowed_placeholders.clone.freeze
32
+ end
33
+
34
+ # Returns a Parser::Result containing the parsed AST nodes, the errors
35
+ # found when parsing and the remaining tokens that have not been parsed.
36
+ # @return [Array<Array<SimpleTemplates::AST::Node>,
37
+ # Array<SimpleTemplates::Parser::Error>,
38
+ # Array<SimpleTemplates::Lexer::Token>>]
39
+ def parse
40
+ ast = []
41
+ errors = []
42
+
43
+ tok_stream = tokens.dup
44
+
45
+ while tok_stream.any?
46
+ parser = detect_parser(tok_stream)
47
+
48
+ if parser.nil?
49
+ errors <<
50
+ Error.new("Encountered unexpected token in stream " +
51
+ "(#{FRIENDLY_TAG_NAMES[tok_stream.first.type]}), but expected to " +
52
+ "see one of the following types: #{acceptable_starting_tokens}.")
53
+ tok_stream = []
54
+
55
+ else
56
+ template, errors_result, remaining_tokens = parser.parse
57
+
58
+ if remaining_tokens.nil?
59
+ raise "Parser #{parser.class} shouldn't return nil remaining " +
60
+ "tokens (should be an Array), please report this bug."
61
+ end
62
+
63
+ if errors_result.empty?
64
+ tok_stream = remaining_tokens
65
+ ast = ast.concat(template)
66
+
67
+ else
68
+ # Once we get a syntax error, we can't really determine if anything
69
+ # else is broken syntactically, so return with the first Error.
70
+ tok_stream = []
71
+ errors.concat(errors_result)
72
+
73
+ end
74
+ end
75
+ end
76
+
77
+ errors.concat(invalid_node_content_errors(ast))
78
+
79
+ [ast, errors, tok_stream]
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :tokens, :allowed_placeholders, :unescapes
85
+
86
+ def invalid_node_content_errors(ast)
87
+ ast.reject(&:allowed?).map do |node|
88
+ Error.new("Invalid #{node.name} with contents, '#{node.contents}' " +
89
+ "found starting at position #{node.pos}.")
90
+ end
91
+ end
92
+
93
+ def acceptable_starting_tokens
94
+ (Placeholder::STARTING_TOKENS | Text::STARTING_TOKENS).map do |tag|
95
+ FRIENDLY_TAG_NAMES[tag]
96
+ end.join(', ')
97
+ end
98
+
99
+ def detect_parser(tokens_to_parse)
100
+ toks = tokens_to_parse.clone.freeze
101
+
102
+ [Placeholder, Text].each do |parser_class|
103
+ if parser_class.applicable?(toks)
104
+ return parser_class.new(unescapes, toks, allowed_placeholders)
105
+ end
106
+ end
107
+
108
+ nil
109
+ end
110
+ end
111
+ end