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