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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +39 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +117 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/giter8.gemspec +35 -0
- data/lib/giter8/ast.rb +67 -0
- data/lib/giter8/conditional.rb +57 -0
- data/lib/giter8/error.rb +21 -0
- data/lib/giter8/fs/fs.rb +75 -0
- data/lib/giter8/literal.rb +36 -0
- data/lib/giter8/pair.rb +29 -0
- data/lib/giter8/pairs.rb +80 -0
- data/lib/giter8/parsers/pairs_parser.rb +128 -0
- data/lib/giter8/parsers/template_parser.rb +563 -0
- data/lib/giter8/renderer/executor.rb +134 -0
- data/lib/giter8/renderer/utils.rb +104 -0
- data/lib/giter8/template.rb +19 -0
- data/lib/giter8/version.rb +5 -0
- data/lib/giter8.rb +91 -0
- metadata +93 -0
@@ -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
|
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: []
|