giter8 0.1.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,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: []