diecut 0.0.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,20 @@
1
+ require 'mustache'
2
+ module Diecut
3
+ class Mustache < ::Mustache
4
+ attr_accessor :partials_hash
5
+
6
+ def partial(name)
7
+ partials_hash.fetch(name).template_string
8
+ end
9
+
10
+ def raise_on_context_miss?
11
+ true
12
+ end
13
+
14
+ # Diecut's templates aren't HTML files - if they need escaping it should
15
+ # happen in the source file
16
+ def escapeHTML(str)
17
+ str
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Diecut
2
+ class PluginDescription
3
+ class ContextDefault < Struct.new(:context_path, :value, :block)
4
+ def simple?
5
+ value != NO_VALUE
6
+ end
7
+
8
+ def compute_value(context)
9
+ block.call(context)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ module Diecut
2
+ class PluginDescription
3
+ class Option
4
+ def initialize(name)
5
+ @name = name
6
+ @default_value = NO_VALUE
7
+ end
8
+ attr_reader :name, :description, :context_path, :default_value
9
+
10
+ def has_default?
11
+ default_value != NO_VALUE
12
+ end
13
+
14
+ def has_context_path?
15
+ !context_path.nil?
16
+ end
17
+
18
+ # A description for the option in the user interface.
19
+ # @param desc [String]
20
+ # The description itself
21
+ def description(desc = NO_VALUE)
22
+ if desc == NO_VALUE
23
+ return @description
24
+ else
25
+ @description = desc
26
+ end
27
+ end
28
+
29
+ # Defines the templating context path this value should be copied to.
30
+ # @param context_path [Array,String]
31
+ # The path into the context to set from this option's value.
32
+ #
33
+ # @example Three equivalent calls
34
+ # option.goes_to("deeply.nested.field")
35
+ # option.goes_to(%w{deeply nested field})
36
+ # option.goes_to("deeply", "nested", "field")
37
+ def goes_to(*context_path)
38
+ if context_path.length == 1
39
+ context_path =
40
+ case context_path.first
41
+ when Array
42
+ context_path.first
43
+ when /.+\..+/ # has an embedded .
44
+ context_path.first.split('.')
45
+ else
46
+ context_path
47
+ end
48
+ end
49
+
50
+ @context_path = context_path
51
+ end
52
+
53
+ # Gives the option a default value (and therefore makes it optional for
54
+ # the user to provide)
55
+ #
56
+ # @param value
57
+ # The default value
58
+ def default(value)
59
+ @default_value = value
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,147 @@
1
+ require 'diecut/errors'
2
+ require 'diecut/caller-locations-polyfill'
3
+ require 'diecut/plugin-description/context-default'
4
+ require 'diecut/plugin-description/option'
5
+
6
+ module Diecut
7
+ class PluginDescription
8
+ include CallerLocationsPolyfill
9
+ NO_VALUE = Object.new.freeze
10
+
11
+ KindStem = Struct.new(:kind, :stem, :template_dir)
12
+
13
+ def initialize(name, source_path)
14
+ @name = name
15
+ @source_path = source_path
16
+ @default_activated = true
17
+ @context_defaults = []
18
+ @options = []
19
+ @resolve_block = nil
20
+ @kind_stems = {}
21
+ end
22
+ attr_reader :default_activated, :name, :source_path, :context_defaults,
23
+ :options, :resolve_block
24
+
25
+ def kinds
26
+ @kind_stems.keys
27
+ end
28
+
29
+ def stem_for(kind)
30
+ @kind_stems.fetch(kind)
31
+ end
32
+
33
+ def has_kind?(kind)
34
+ @kind_stems.key?(kind)
35
+ end
36
+
37
+ def default_active?
38
+ @default_activated
39
+ end
40
+
41
+ def apply_resolve(ui, context)
42
+ @resolve_block.call(ui, context)
43
+ end
44
+
45
+ # Attaches this plugin to a particular kind of diecut generator. Can be
46
+ # called multiple times in order to reuse the plugin.
47
+ #
48
+ # @param kind [String, Symbol]
49
+ # The kind of generator to register the plugin to
50
+ # @param templates [String]
51
+ # The directory of templates that the plugin adds to the generation
52
+ # process. Relative paths are resolved from the directory the plugin is
53
+ # being defined in. If omitted (or nil) defaults to "templates"
54
+ # @param stem [Array(String), String]
55
+ # A prefix for the templates directory when it's used for this kind of
56
+ # generator. By default, this will be [kind], which is what you'll
57
+ # probably want in a gem plugin. For local plugins, you probably want to
58
+ # have directories per kind, and set this to []
59
+ #
60
+ # For instance, you might set up a plugin for Rails that also works in Xing
61
+ # projects that use Rails for a backend
62
+ #
63
+ # @example Install for Rails and Xing
64
+ # plugin.for_kind(:rails)
65
+ # plugin.for_kind(:xing, nil, "xing/backend")
66
+ #
67
+ #
68
+ def for_kind(kind, templates = nil, stem = nil)
69
+ stem ||= [kind]
70
+ templates ||= "templates"
71
+ templates = File.expand_path(templates, File.dirname(caller_locations(1..1).first.absolute_path))
72
+ @kind_stems[kind] = KindStem.new(kind, stem, templates)
73
+ end
74
+
75
+ # Force this plugin to be enabled to be used. Good for optional features.
76
+ def default_off
77
+ @default_activated = false
78
+ end
79
+
80
+ # Make this plugin part of the generation process by default. The is the
81
+ # default behavior anyway, provided for consistency.
82
+ def default_on
83
+ @default_activated = true
84
+ end
85
+
86
+ # Set a default value for a field in the templating context.
87
+ #
88
+ # @param context_path [String, Array]
89
+ # Either an array of strings or a dotted string (e.g.
90
+ # "deeply.nested.value") that describes a path into the templating
91
+ # context to give a default value to.
92
+ #
93
+ # @param value
94
+ # A simple default value, which will be used verbatim (n.b. it will be
95
+ # cloned if appropriate, so you can use [] for an array).
96
+ #
97
+ # @yieldreturn
98
+ # A computed default value. The block will be called when the context is
99
+ # set up. You cannot use both a simple value and a computed value.
100
+ #
101
+ # @example
102
+ # plugin.default("built_at"){ Time.now }
103
+ # plugin.default("author", "Judson")
104
+ def default(context_path, value = NO_VALUE, &block)
105
+ context_path =
106
+ case context_path
107
+ when Array
108
+ context_path
109
+ when /.+\..+/ # has an embedded .
110
+ context_path.split('.')
111
+ else
112
+ [context_path]
113
+ end
114
+ if value != NO_VALUE and not block.nil?
115
+ raise InvalidPlugin, "Default on #{name.inspect} both has a simple default value (#{value}) and a dynamic block value, which isn't allowed."
116
+ end
117
+ @context_defaults << ContextDefault.new(context_path, value, block)
118
+ end
119
+
120
+ # Define an option to provide to the user interface.
121
+ # @param name [String,Symbol]
122
+ # The name for the option, as it'll be provided to the user.
123
+ # @yieldparam option [Option]
124
+ # The option description object
125
+ def option(name)
126
+ name = name.to_sym
127
+ option = Option.new(name)
128
+ yield option
129
+ @options << option
130
+ return option
131
+ end
132
+
133
+ # The resolve block provides the loophole to allow complete configuration
134
+ # of the rendering context. The last thing that happens before files are
135
+ # generated is that all the plugin resolves are run, so that e.g. values
136
+ # can be calculated from other values. It's very difficult to analyze
137
+ # resolve blocks, however: use them as sparingly as possible.
138
+ #
139
+ # @yeildparam ui [UIContext]
140
+ # the values supplied by the user to satisfy options
141
+ # @yeildparam context [Configurable]
142
+ # the configured rendering context
143
+ def resolve(&block)
144
+ @resolve_block = block
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,189 @@
1
+ require 'tsort'
2
+ require 'rubygems/specification'
3
+ require 'diecut/plugin-description'
4
+
5
+ module Diecut
6
+ class PluginLoader
7
+ include TSort
8
+
9
+ NO_VALUE = Object.new.freeze
10
+
11
+ class GemPlugin < Struct.new(:gem, :path)
12
+ def gem?
13
+ gem != NO_VALUE
14
+ end
15
+ end
16
+
17
+ def initialize
18
+ @sources = {}
19
+ @local_sources = []
20
+ @by_gem_name = Hash.new{|h,k| h[k] = []}
21
+ @plugins = []
22
+ end
23
+ attr_reader :plugins
24
+ attr_accessor :local_valise
25
+
26
+ def local_valise
27
+ @local_valise ||= Valise::Set.define do
28
+ ro '.diecut'
29
+ ro '~/.config/diecut'
30
+ ro '/usr/share/diecut'
31
+ ro '/etc/diecut'
32
+ end
33
+ end
34
+
35
+ # :nocov:
36
+ def latest_specs(prerelease)
37
+ Gem::Specification.latest_specs(prerelease)
38
+ end
39
+
40
+ def require_plugin(path)
41
+ require path
42
+ end
43
+ # :nocov:
44
+
45
+ PLUGIN_FILENAME = 'diecut_plugin.rb'
46
+ def discover(prerelease)
47
+ latest_specs(prerelease).map do |spec|
48
+ spec.matches_for_glob(PLUGIN_FILENAME).map do |match|
49
+ from_gem(spec, match)
50
+ end
51
+ end
52
+ local_valise.get(PLUGIN_FILENAME).present.map(&:full_path).each do |path|
53
+ from_local(path)
54
+ end
55
+ end
56
+
57
+ def from_gem(spec, path)
58
+ plugin = GemPlugin.new(spec, path)
59
+
60
+ @sources[path] = plugin
61
+ spec.dependencies.map(&:name).each do |depname|
62
+ @by_gem_name[depname] << plugin
63
+ end
64
+ end
65
+
66
+ def from_local(path)
67
+ source = GemPlugin.new(NO_VALUE, path)
68
+ @sources[path] = source
69
+ @local_sources << source
70
+ end
71
+
72
+ def tsort_each_node(&block)
73
+ @sources.each_value(&block)
74
+ end
75
+
76
+ def tsort_each_child(node)
77
+ if node.gem?
78
+ @by_gem_name[node.gem.name].each do |depplugin|
79
+ yield depplugin
80
+ end
81
+ @local_sources.each do |local|
82
+ yield local
83
+ end
84
+ else
85
+ @local_sources.drop_while do |src|
86
+ src != node
87
+ end.drop(1).each do |node|
88
+ yield(node)
89
+ end
90
+ end
91
+ end
92
+
93
+ def component_sort
94
+ unless block_given?
95
+ return enum_for(:component_sort)
96
+ end
97
+ child_idxs = {}
98
+ strongly_connected_components.each_with_index do |component, idx|
99
+ component.sort_by{|node| child_idxs.fetch(node, -1) }.each do |comp|
100
+ yield(comp)
101
+ end
102
+
103
+ component.each do |comp|
104
+ tsort_each_child(comp) do |child|
105
+ child_idxs[child] = idx
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def each_path
112
+ component_sort.reverse_each do |source|
113
+ yield source.path
114
+ end
115
+ end
116
+
117
+ # Can a chain of "is after" arrows be walked from 'from' to 'to'.
118
+ # The rules are:
119
+ # A plugin defined by a gem that depends on another gem "is after" the
120
+ # plugin defined in the latter gem.
121
+ # A plugin defined in a local config file is after "more general" local
122
+ # files (project config is after personal is after system).
123
+ # All local plugins are after all gem plugins
124
+ #
125
+ # The rationale for these rules is that decisions made later in time have
126
+ # more information and that decisions made closer to the problem know the
127
+ # problem domain better.
128
+ #
129
+ def strict_sequence?(to, from)
130
+ from_source = @sources[from.source_path]
131
+ to_source = @sources[to.source_path]
132
+
133
+ case [from_source.gem?, to_source.gem?]
134
+ when [true, true]
135
+ dep_path?(to_source, from_source)
136
+ when [true, false]
137
+ false
138
+ when [false, true]
139
+ true
140
+ when [false, false]
141
+ @local_sources.index_of(from.source_path) <= @local_sources.index_of(to.source_path)
142
+ end
143
+ end
144
+
145
+ def dep_path?(from_gem, to_gem)
146
+ # potential to optimize this: build a map of reachablility and test
147
+ # against that.
148
+ closed = {}
149
+ open = [from_gem]
150
+ until open.empty?
151
+ current = open.shift
152
+ return true if current == to_gem
153
+ closed[current] = true
154
+ open += @by_gem_name[current.gem.name].select{|gem| !closed.has_key?(gem)}
155
+ end
156
+ return false
157
+ end
158
+
159
+ def load_plugins(prerelease = false)
160
+ discover(prerelease)
161
+ each_path do |path|
162
+ require_plugin(path)
163
+ end
164
+ end
165
+
166
+ def choose_source(locations)
167
+ locations.each do |loc|
168
+ path = loc.absolute_path
169
+ if @sources.has_key?(path)
170
+ return path
171
+ end
172
+ end
173
+ raise "Couldn't find source of plugin..."
174
+ end
175
+
176
+ def add_plugin_desc(desc)
177
+ plugins << desc
178
+ end
179
+
180
+ def describe_plugin(name)
181
+ source_path = choose_source(caller_locations)
182
+ desc = PluginDescription.new(name, source_path)
183
+ yield(desc)
184
+ add_plugin_desc(desc)
185
+ return desc
186
+ end
187
+
188
+ end
189
+ end
@@ -0,0 +1,136 @@
1
+ require 'paint'
2
+ require 'diecut/mustache'
3
+
4
+ module Diecut
5
+ #Adopted gratefully from Xavier Shay's Cane
6
+ class ReportFormatter
7
+ def initialize(reports)
8
+ @reports = reports
9
+ end
10
+ attr_reader :reports
11
+
12
+ def rejection_fields
13
+ %i(file_and_line label value)
14
+ end
15
+
16
+ def template
17
+ (<<-EOT).gsub(/^ /, '')
18
+ {{#reports}}{{#status_color}}{{name}}: {{status}} {{#length}} {{length}}{{/length}}
19
+ {{/status_color}}
20
+ {{#summary}}{{summary}}
21
+ {{/summary}}{{^empty }}{{#headers}}{{it}} {{/headers}}
22
+ {{/empty }}{{#rejects}} {{#reject }}{{it}} {{/reject}}
23
+ {{/rejects}}{{#advice}}
24
+ {{advice}}
25
+ {{/advice}}
26
+ {{/reports}}
27
+ {{#status_color}}Total QA report items: {{total_items}}
28
+ Total QA failing reports: {{total_fails}}
29
+ {{/status_color}}
30
+ EOT
31
+ end
32
+
33
+ def to_s(widths=nil)
34
+ renderer = Mustache.new
35
+
36
+ # require 'pp'; puts "\n#{__FILE__}:#{__LINE__} => {context(renderer).pretty_inspect}"
37
+ renderer.render(template, context(renderer))
38
+ end
39
+
40
+ def passed?
41
+ fail_count == 0
42
+ end
43
+
44
+ def fail_count
45
+ reports.inject(0){|sum, report| sum + (report.passed ? 0 : 1)}
46
+ end
47
+
48
+ def context(renderer)
49
+ bad_color = proc{|text,render| Paint[renderer.render(text), :red]}
50
+ good_color = proc{|text,render| Paint[renderer.render(text), :green]}
51
+ warn_color = proc{|text,render| Paint[renderer.render(text), :yellow]}
52
+
53
+ context = {
54
+ reports: reports.map(&:context),
55
+ passed: passed?,
56
+ total_items: reports.inject(0){|sum, report| sum + report.length},
57
+ total_fails: fail_count,
58
+ status_color: passed? ? good_color : bad_color
59
+ }
60
+ context[:reports].each do |report|
61
+ report[:status_color] =
62
+ case report[:status]
63
+ when /ok/i
64
+ good_color
65
+ when /fail/i
66
+ bad_color
67
+ else
68
+ warn_color
69
+ end
70
+ end
71
+ context
72
+ end
73
+ end
74
+
75
+ class Report
76
+ def initialize(name, column_headers)
77
+ @name = name
78
+ @column_headers = column_headers
79
+ @rejects = []
80
+ @status = "OK"
81
+ @passed = true
82
+ @summary = ""
83
+ @summary_counts = true
84
+ end
85
+ attr_reader :name, :column_headers, :rejects
86
+ attr_accessor :summary, :passed, :summary_count, :advice, :status
87
+
88
+ def add(*args)
89
+ @rejects << args
90
+ end
91
+
92
+ def fail(summary)
93
+ @passed = false
94
+ @status = "FAIL"
95
+ @summary = summary
96
+ end
97
+
98
+ def length
99
+ @rejects.length
100
+ end
101
+ alias count length
102
+
103
+ def empty?
104
+ @rejects.empty?
105
+ end
106
+
107
+ def column_widths
108
+ column_headers.map.with_index do |header, idx|
109
+ (@rejects.map{|reject| reject[idx]} + [header]).map{|field|
110
+ field.to_s.length
111
+ }.max
112
+ end
113
+ end
114
+
115
+ def sized(array, widths)
116
+ array.take(widths.length).zip(widths).map{|item, width| { it: item.to_s.ljust(width)}}
117
+ end
118
+
119
+ def context
120
+ widths = column_widths
121
+ {
122
+ empty: empty?,
123
+ passing: passed,
124
+ status: status,
125
+ name: name,
126
+ length: summary_count,
127
+ summary: summary,
128
+ advice: advice,
129
+ headers: sized(column_headers, widths),
130
+ rejects: rejects.map do |reject|
131
+ {reject: sized(reject, widths)}
132
+ end
133
+ }
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,88 @@
1
+ module Diecut
2
+ class TemplateReducer
3
+ def initialize(tokens)
4
+ @tokens = tokens
5
+ end
6
+
7
+ def fields
8
+ process([], [@tokens]) if @fields.nil?
9
+ @fields.keys
10
+ end
11
+
12
+ def sections
13
+ process([], [@tokens]) if @sections.nil?
14
+ @sections.keys
15
+ end
16
+
17
+ def partials
18
+ process([], [@tokens]) if @partials.nil?
19
+ @partials.keys
20
+ end
21
+
22
+ def unknown
23
+ process([], [@tokens]) if @unknown.nil?
24
+ @unknown
25
+ end
26
+
27
+ def leaf_fields
28
+ leaves_and_nodes if @leaf_fields.nil?
29
+ return @leaf_fields
30
+ end
31
+
32
+ def node_fields
33
+ leaves_and_nodes if @node_fields.nil?
34
+ return @node_fields
35
+ end
36
+
37
+ def leaves_and_nodes
38
+ @node_fields, @leaf_fields = fields.partition.with_index do |field, idx|
39
+ fields.drop(idx + 1).any? do |other|
40
+ field.zip(other).all? {|l,r|
41
+ l==r}
42
+ end
43
+ end
44
+ end
45
+
46
+ # XXX Used as a section and as a field is different from used as a field
47
+ # and as parent of a field. Tricky tricky
48
+ def validate
49
+ not_sections = node_fields.find_all {|node| !sections.include?(node)}
50
+ unless not_sections.empty?
51
+ warn "These fields are referenced directly, and as the parent of another field:\n" +
52
+ not_sections.map{|sec| sec.join(".")}.join("\n")
53
+ end
54
+ end
55
+
56
+ def process(prefix, tokens)
57
+ @fields ||= {}
58
+ @partials ||= {}
59
+ @unknown ||= []
60
+ @sections ||= {}
61
+
62
+ tokens.each do |token|
63
+ case token[0]
64
+ when :multi
65
+ process(prefix, token[1..-1])
66
+ when :static
67
+ when :mustache
68
+ case token[1]
69
+ when :etag, :utag
70
+ process(prefix, [token[2]])
71
+ when :section, :inverted_section
72
+ @sections[prefix + token[2][2]] = true
73
+ process(prefix, [token[2]])
74
+ process(prefix + token[2][2], [token[4]])
75
+ when :partial
76
+ @partials[[token[2], prefix]] = true
77
+ when :fetch
78
+ @fields[prefix + token[2]] = true
79
+ else
80
+ @unknown << token
81
+ end
82
+ else
83
+ @unknown << token
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end