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.
- checksums.yaml +7 -0
- data/bin/diecut +10 -0
- data/gem_test_suite.rb +17 -0
- data/lib/diecut/caller-locations-polyfill.rb +18 -0
- data/lib/diecut/cli.rb +77 -0
- data/lib/diecut/configurable.rb +142 -0
- data/lib/diecut/context-handler.rb +77 -0
- data/lib/diecut/errors.rb +14 -0
- data/lib/diecut/linter.rb +177 -0
- data/lib/diecut/mediator.rb +79 -0
- data/lib/diecut/mill.rb +68 -0
- data/lib/diecut/mustache.rb +20 -0
- data/lib/diecut/plugin-description/context-default.rb +13 -0
- data/lib/diecut/plugin-description/option.rb +63 -0
- data/lib/diecut/plugin-description.rb +147 -0
- data/lib/diecut/plugin-loader.rb +189 -0
- data/lib/diecut/report.rb +136 -0
- data/lib/diecut/template-reducer.rb +88 -0
- data/lib/diecut/template-set.rb +108 -0
- data/lib/diecut/template.rb +58 -0
- data/lib/diecut/ui-applier.rb +71 -0
- data/lib/diecut/ui-config.rb +31 -0
- data/lib/diecut.rb +54 -0
- data/spec/cli_spec.rb +1 -0
- data/spec/configurable_spec.rb +20 -0
- data/spec/linter_spec.rb +132 -0
- data/spec/mill_spec.rb +52 -0
- data/spec/plugin_loader_spec.rb +109 -0
- data/spec/register_plugin_spec.rb +105 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/template-reducer_spec.rb +36 -0
- data/spec/template_set_spec.rb +22 -0
- data/spec/template_spec.rb +60 -0
- metadata +152 -0
@@ -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,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
|