giter8 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Giter8
4
+ # Module Renderer implements all mechanisms related to template rendering
5
+ module Renderer
6
+ # Executor implements methods for rendering remplates
7
+ class Executor
8
+ # Initializes the executor with a given Pairs set
9
+ def initialize(props)
10
+ @props = props
11
+ end
12
+
13
+ # Returns a string after rendering the provided AST tree with the Pairs
14
+ # provided upon initialisation
15
+ def exec(tree)
16
+ result = StringIO.new
17
+ exec_tree(tree, result)
18
+ result.string
19
+ end
20
+
21
+ private
22
+
23
+ # Returns a string array containing all formatting options present in the
24
+ # template's format options.
25
+ def extract_format_options(template)
26
+ unless template.is_a? Giter8::Template
27
+ raise Giter8::Error, "Can't call extract_format_options on non-template value"
28
+ end
29
+ return nil if template.options.empty?
30
+
31
+ all_forms = template.options.fetch(:format)
32
+ return nil if all_forms.nil?
33
+
34
+ all_forms.split(",").map(&:strip)
35
+ end
36
+
37
+ # Execute all formatting helpers (if any) for a given Template instance,
38
+ # returning the expanded variable value with all formatting applied.
39
+ def run_methods(template)
40
+ unless template.is_a? Giter8::Template
41
+ raise(Giter8::Error,
42
+ "Can't call run_methods on non-template value")
43
+ end
44
+
45
+ val = @props.fetch(template.name)
46
+ if val.nil?
47
+ raise Giter8::PropertyNotFoundError.new(template.name, template.source, template.line,
48
+ template.column)
49
+ end
50
+
51
+ opts = extract_format_options(template)
52
+ unless opts.nil?
53
+ return opts.inject(val) do |value, method_name|
54
+ fn = HELPERS.fetch(method_name, nil)
55
+ if fn.nil?
56
+ raise Giter8::FormatterNotFoundError.new(method_name, template.source, template.line,
57
+ template.column)
58
+ end
59
+
60
+ fn.call(value)
61
+ end
62
+ end
63
+
64
+ val
65
+ end
66
+
67
+ # Evaluates and returns a boolean value for a given Conditional instance
68
+ # of an AST.
69
+ def evaluate_conditional_expression(cond)
70
+ val = @props.find_pair(cond.property)
71
+ helper = cond.helper
72
+ helper = helper.downcase
73
+
74
+ return nil if %w[truthy present].include?(helper) && val.nil?
75
+
76
+ raise Giter8::PropertyNotFoundError.new(cond.property, cond.source, cond.line, cond.column) if val.nil?
77
+
78
+ case helper
79
+ when "truthy"
80
+ val.truthy?
81
+ when "present"
82
+ return nil if val.value.nil?
83
+
84
+ !val.value.strip.empty?
85
+ else
86
+ raise "BUG: helper #{helper} allowed by parser, but not implemented by renderer"
87
+ end
88
+ end
89
+
90
+ # Evaluates a given Conditional instance and writes a matching branch to
91
+ # the provided Writer. Returns whether a branch was matched.
92
+ def evaluate_conditional(cond, writer)
93
+ unless cond.is_a? Giter8::Conditional
94
+ raise Giter8::Error,
95
+ "Can't call evaluate_conditional on non-conditional type"
96
+ end
97
+
98
+ ok = evaluate_conditional_expression(cond)
99
+ if ok
100
+ exec_tree(cond.cond_then, writer)
101
+ return true
102
+ end
103
+
104
+ cond.cond_else_if.each do |inner_cond|
105
+ return true if evaluate_conditional(inner_cond, writer)
106
+ end
107
+
108
+ unless cond.cond_else.nil?
109
+ exec_tree(cond.cond_else, writer)
110
+ return true
111
+ end
112
+
113
+ false
114
+ end
115
+
116
+ # Recursively iterate a provided AST tree and writes its results to the
117
+ # provided writer.
118
+ def exec_tree(tree, writer)
119
+ tree.each do |node|
120
+ case node
121
+ when Giter8::Literal
122
+ writer << node.value
123
+ when Giter8::Template
124
+ writer << run_methods(node)
125
+ when Giter8::Conditional
126
+ evaluate_conditional(node, writer)
127
+ else
128
+ raise Giter8::Error, "BUG: Unexpected node type #{node.class.name}"
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Giter8
4
+ # nodoc
5
+ module Renderer
6
+ WORD_ONLY_REGEXP = /[^a-zA-Z0-9_]/.freeze
7
+ WORD_SPACE_REGEXP = /[^a-zA-Z0-9]/.freeze
8
+ SNAKE_CASE_REGEXP = /[\s.]/.freeze
9
+ ALPHABET = ((65..90).to_a + (97..122).to_a).map(&:chr).freeze
10
+
11
+ def self.uppercase(val)
12
+ val.upcase
13
+ end
14
+
15
+ def self.lowercase(val)
16
+ val.downcase
17
+ end
18
+
19
+ def self.capitalize(val)
20
+ val.capitalize
21
+ end
22
+
23
+ def self.decapitalize(val)
24
+ lowercase(val)
25
+ end
26
+
27
+ def self.start_case(val)
28
+ val.split.map(&:capitalize)
29
+ end
30
+
31
+ def self.word_only(val)
32
+ val.gsub(WORD_ONLY_REGEXP, "")
33
+ end
34
+
35
+ def self.word_space(val)
36
+ val.gsub(WORD_SPACE_REGEXP, " ")
37
+ end
38
+
39
+ def self.upper_camel(val)
40
+ word_only(start_case(val))
41
+ end
42
+
43
+ def self.lower_camel(val)
44
+ decapitalize(word_only(start_case(val)))
45
+ end
46
+
47
+ def self.hyphenate(val)
48
+ val.gsub(/\s/, "-")
49
+ end
50
+
51
+ def self.normalize(val)
52
+ lowercase(hyphenate(val))
53
+ end
54
+
55
+ def self.snake_case(val)
56
+ val.gsub(SNAKE_CASE_REGEXP, "_")
57
+ end
58
+
59
+ def self.package_naming(val)
60
+ val.gsub(/\s/, ".")
61
+ end
62
+
63
+ def self.package_dir(val)
64
+ val.gsub(/\./, "/")
65
+ end
66
+
67
+ def self.random
68
+ ALPHABET.sample(40).join
69
+ end
70
+
71
+ HELPERS = {
72
+ "upper" => method(:uppercase),
73
+ "uppercase" => method(:uppercase),
74
+ "lower" => method(:lowercase),
75
+ "lowercase" => method(:lowercase),
76
+ "cap" => method(:capitalize),
77
+ "capitalize" => method(:capitalize),
78
+ "decap" => method(:decapitalize),
79
+ "decapitalize" => method(:decapitalize),
80
+ "start" => method(:start_case),
81
+ "start-case" => method(:start_case),
82
+ "word" => method(:word_only),
83
+ "word-only" => method(:word_only),
84
+ "space" => method(:word_space),
85
+ "word-space" => method(:word_space),
86
+ "Camel" => method(:upper_camel),
87
+ "upper-camel" => method(:upper_camel),
88
+ "camel" => method(:lower_camel),
89
+ "lower-camel" => method(:lower_camel),
90
+ "hyphen" => method(:hyphenate),
91
+ "hyphenate" => method(:hyphenate),
92
+ "norm" => method(:normalize),
93
+ "normalize" => method(:normalize),
94
+ "snake" => method(:snake_case),
95
+ "snake-case" => method(:snake_case),
96
+ "package" => method(:package_naming),
97
+ "package-naming" => method(:package_naming),
98
+ "packaged" => method(:package_dir),
99
+ "package-dir" => method(:package_dir),
100
+ "random" => method(:random),
101
+ "generate-random" => method(:random)
102
+ }.freeze
103
+ end
104
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Giter8
4
+ # Template represents a Template variable in an AST. Contains a source file,
5
+ # the line and column where the template begins, the variable name to be
6
+ # looked up, a set of options, and the parent to which the node belongs to.
7
+ class Template
8
+ attr_accessor :source, :line, :column, :name, :options, :parent
9
+
10
+ def initialize(name, options, parent, source, line, column)
11
+ @source = source
12
+ @line = line
13
+ @column = column
14
+ @name = name
15
+ @options = options
16
+ @parent = parent
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Giter8
4
+ VERSION = "0.1.1"
5
+ end
data/lib/giter8.rb ADDED
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "stringio"
5
+ require "fileutils"
6
+
7
+ require_relative "giter8/version"
8
+ require_relative "giter8/error"
9
+ require_relative "giter8/pair"
10
+ require_relative "giter8/pairs"
11
+ require_relative "giter8/literal"
12
+ require_relative "giter8/template"
13
+ require_relative "giter8/conditional"
14
+ require_relative "giter8/ast"
15
+
16
+ require_relative "giter8/parsers/pairs_parser"
17
+ require_relative "giter8/parsers/template_parser"
18
+
19
+ require_relative "giter8/renderer/utils"
20
+ require_relative "giter8/renderer/executor"
21
+
22
+ require_relative "giter8/fs/fs"
23
+
24
+ require "ptools"
25
+
26
+ # Giter8 implements a parser and renderer for Giter8 templates
27
+ module Giter8
28
+ # Parses a given String, Hash or File into a Pairs set. When parsing from a
29
+ # File object, file metadata is used on error messages. The parser ignores
30
+ # any content beginning with a hash (#) until the end of the line. Properties
31
+ # are composed of a key comprised of a-z characters, numbers (0-9), unerscores
32
+ # and dashes. When a file is passed, the caller is responsible for closing it.
33
+ def self.parse_props(props)
34
+ return props if props.is_a? Pairs
35
+
36
+ if [String, Hash, File].none? { |type| props.is_a? type }
37
+ raise Giter8::Error, "parse_props can only be used with strings, hashes, and files. Got #{props.class.name}"
38
+ end
39
+
40
+ opts = {}
41
+ if props.is_a? File
42
+ opts[:source] = props.path
43
+ props = props.read
44
+ end
45
+ return Parsers::PairsParser.parse(props, opts) if props.is_a? String
46
+
47
+ Pairs.new(props)
48
+ end
49
+
50
+ # Parses a provided Giter8 template from a String or File. When a File is
51
+ # provided, its name will be used in error metadata. Also, when using a File
52
+ # object, the caller is responsible for closing it.
53
+ # Returns an AST object containing the file's contents.
54
+ def self.parse_template(template)
55
+ if [String, File, AST].none? { |type| template.is_a? type }
56
+ raise Giter8::Error, "parse_template can only be used with strings and files. Got #{template.class.name}"
57
+ end
58
+
59
+ return template if template.is_a? AST
60
+
61
+ opts = {}
62
+ if template.is_a? File
63
+ opts[:source] = template.path
64
+ template = template.read
65
+ end
66
+
67
+ Parsers::TemplateParser.parse(template, opts)
68
+ end
69
+
70
+ # Renders a given template using a set of props. Template may be a
71
+ # String or File, while props can be a Hash, String, or File. When providing
72
+ # a File to either parameter, the caller is responsible for closing it.
73
+ # Returns a string containing the rendered contents.
74
+ def self.render_template(template, props)
75
+ template = parse_template template
76
+ props = parse_props props
77
+
78
+ executor = Renderer::Executor.new(props)
79
+ executor.exec(template)
80
+ end
81
+
82
+ # Renders a provided input directory into an output directory using a provided
83
+ # props set.
84
+ def self.render_directory(props, input, output)
85
+ raise Giter8::Error, "Input directory #{input} does not exist" unless File.exist?(input)
86
+ raise Giter8::Error, "Input path #{input} is not a directory" unless File.stat(input).directory?
87
+ raise Giter8::Error, "Destination path #{output} already exists" if File.exist?(output)
88
+
89
+ FS.render(props, input, output)
90
+ end
91
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: giter8
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Victor Gama
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-08-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ptools
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.4.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.4'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.4.2
33
+ description: giter8 implements giter8 rendering mechanisms
34
+ email:
35
+ - hey@vito.io
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - ".github/workflows/main.yml"
41
+ - ".gitignore"
42
+ - ".rspec"
43
+ - ".rubocop.yml"
44
+ - CODE_OF_CONDUCT.md
45
+ - Gemfile
46
+ - Gemfile.lock
47
+ - LICENSE.txt
48
+ - README.md
49
+ - Rakefile
50
+ - bin/console
51
+ - bin/setup
52
+ - giter8.gemspec
53
+ - lib/giter8.rb
54
+ - lib/giter8/ast.rb
55
+ - lib/giter8/conditional.rb
56
+ - lib/giter8/error.rb
57
+ - lib/giter8/fs/fs.rb
58
+ - lib/giter8/literal.rb
59
+ - lib/giter8/pair.rb
60
+ - lib/giter8/pairs.rb
61
+ - lib/giter8/parsers/pairs_parser.rb
62
+ - lib/giter8/parsers/template_parser.rb
63
+ - lib/giter8/renderer/executor.rb
64
+ - lib/giter8/renderer/utils.rb
65
+ - lib/giter8/template.rb
66
+ - lib/giter8/version.rb
67
+ homepage: https://github.com/heyvito/giter8.rb
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/heyvito/giter8.rb
72
+ source_code_uri: https://github.com/heyvito/giter8.rb
73
+ changelog_uri: https://github.com/heyvito/giter8.rb
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '2.7'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.2.22
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: giter8 implements giter8 rendering mechanisms
93
+ test_files: []