diecut 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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