yarrow 0.7.2 → 0.7.4
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 +4 -4
- data/.gitignore +1 -0
- data/README.md +0 -1
- data/bin/yarrow-server +2 -2
- data/lib/yarrow/config.rb +7 -7
- data/lib/yarrow/content/expansion.rb +6 -41
- data/lib/yarrow/content/expansion_strategy.rb +51 -0
- data/lib/yarrow/content/graph.rb +2 -2
- data/lib/yarrow/content/manifest.rb +4 -4
- data/lib/yarrow/content/model.rb +46 -0
- data/lib/yarrow/content/policy.rb +48 -0
- data/lib/yarrow/content/resource.rb +29 -0
- data/lib/yarrow/content/source.rb +70 -3
- data/lib/yarrow/content/tree_expansion.rb +15 -6
- data/lib/yarrow/defaults.yml +1 -1
- data/lib/yarrow/generator.rb +1 -1
- data/lib/yarrow/schema/definitions.rb +30 -0
- data/lib/yarrow/schema/dictionary.rb +49 -0
- data/lib/yarrow/schema/entity.rb +51 -0
- data/lib/yarrow/schema/types.rb +111 -0
- data/lib/yarrow/schema/value.rb +57 -0
- data/lib/yarrow/schema.rb +28 -119
- data/lib/yarrow/server.rb +5 -4
- data/lib/yarrow/symbols.rb +15 -0
- data/lib/yarrow/version.rb +1 -1
- data/lib/yarrow.rb +10 -3
- data/yarrow.gemspec +4 -3
- metadata +36 -16
- data/lib/yarrow/content/collection_expander.rb +0 -241
- data/lib/yarrow/content/object_type.rb +0 -42
- data/lib/yarrow/content/source_collector.rb +0 -78
@@ -1,241 +0,0 @@
|
|
1
|
-
module Yarrow
|
2
|
-
module Content
|
3
|
-
class CollectionExpander
|
4
|
-
include Yarrow::Tools::FrontMatter
|
5
|
-
|
6
|
-
# If a list of object types is not provided, a default `pages` type is
|
7
|
-
# created.
|
8
|
-
def initialize(object_types=nil)
|
9
|
-
@object_types = object_types || [
|
10
|
-
Yarrow::Content::ObjectType.from_name(:pages)
|
11
|
-
]
|
12
|
-
end
|
13
|
-
|
14
|
-
def expand(graph)
|
15
|
-
@object_types.each do |object_type|
|
16
|
-
expand_nested(graph, object_type)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def expand_nested(graph, content_type)
|
21
|
-
strategy = TreeExpansion.new(graph)
|
22
|
-
strategy.expand(content_type)
|
23
|
-
end
|
24
|
-
|
25
|
-
def expand_nested_legacy(graph, content_type)
|
26
|
-
type = content_type.collection
|
27
|
-
exts = content_type.extensions
|
28
|
-
|
29
|
-
# If match path represents entire content dir, then include the entire
|
30
|
-
# content dir instead of scanning from a subfolder matching the name of
|
31
|
-
# the collection.
|
32
|
-
start_node = if content_type.match_path == "."
|
33
|
-
graph.n(:root)
|
34
|
-
else
|
35
|
-
graph.n(:root).out(name: type.to_s)
|
36
|
-
end
|
37
|
-
|
38
|
-
# Extract metadata from given start node
|
39
|
-
data = extract_metadata(start_node, type)
|
40
|
-
|
41
|
-
# Collects all nested collections in the subgraph for this content type
|
42
|
-
subcollections = {}
|
43
|
-
index = nil
|
44
|
-
|
45
|
-
# Define alias for accessing metadata in the loop
|
46
|
-
metadata = data
|
47
|
-
|
48
|
-
# Scan and collect all nested directories under the top level source
|
49
|
-
start_node.depth_first.each do |node|
|
50
|
-
if node.label == :directory
|
51
|
-
# Check if this entry has metadata defined at the top level
|
52
|
-
if data[:collections]
|
53
|
-
item = data[:collections].find { |c| c[:slug] == node.props[:slug] }
|
54
|
-
metadata = item if item
|
55
|
-
end
|
56
|
-
|
57
|
-
# Create a collection node representing a collection of documents
|
58
|
-
index = graph.create_node do |collection_node|
|
59
|
-
collection_node.label = :collection
|
60
|
-
collection_node.props[:type] = type
|
61
|
-
collection_node.props[:name] = node.props[:name]
|
62
|
-
collection_node.props[:slug] = node.props[:slug]
|
63
|
-
collection_node.props[:title] = metadata[:title]
|
64
|
-
|
65
|
-
# Override default status so that mapped index collections always show
|
66
|
-
# up in the resulting document manifest, when they don’t have
|
67
|
-
# associated metadata. This is the opposite of how individual pieces
|
68
|
-
# of content behave (default to draft status if one isn’t supplied).
|
69
|
-
collection_node.props[:status] = if data[:status]
|
70
|
-
data[:status]
|
71
|
-
else
|
72
|
-
"published"
|
73
|
-
end
|
74
|
-
|
75
|
-
# TODO: URL generation might need to happen elsewhere
|
76
|
-
collection_node.props[:url] = if data[:url]
|
77
|
-
data[:url]
|
78
|
-
else
|
79
|
-
"#{node.props[:path].split('./content').last}/"
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# Add this collection id to the lookup table for edge construction
|
84
|
-
subcollections[node.props[:path]] = index
|
85
|
-
|
86
|
-
# Join the collection to its parent
|
87
|
-
unless node.props[:slug] == type.to_s || !subcollections.key?(node.props[:entry].parent.to_s)
|
88
|
-
graph.create_edge do |edge|
|
89
|
-
edge.label = :child
|
90
|
-
edge.from = subcollections[node.props[:entry].parent.to_s].id
|
91
|
-
edge.to = index.id
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# If there are no subcollections then we need to look at the start node
|
98
|
-
# TODO: test to verify if this could be used in all cases, not just
|
99
|
-
# the situation where there are subfolders to be mapped.
|
100
|
-
if subcollections.empty?
|
101
|
-
# Collect files that match the content type extension and group them
|
102
|
-
# under a common key for each slug (this is so we can merge multiple
|
103
|
-
# files with the same name together into a single content type, a
|
104
|
-
# specific pattern found in some legacy content folders).
|
105
|
-
#
|
106
|
-
# Ideally, this code should be deleted once we have a clean workflow
|
107
|
-
# and can experiment with decoupling different strategies for
|
108
|
-
# expansion/enrichment of content objects.
|
109
|
-
objects = start_node.out(:file).all.select do |file|
|
110
|
-
file.props[:name].end_with?(*exts)
|
111
|
-
end.group_by do |file|
|
112
|
-
file.props[:slug]
|
113
|
-
end
|
114
|
-
|
115
|
-
# This is a massive hack to deal with situations where we don’t
|
116
|
-
# recurse down the list of directories. The best way to clean it up
|
117
|
-
# will be to document the different supported mapping formats and
|
118
|
-
# URL generation strategies and break these up into separate
|
119
|
-
# traversal objects for each particular style of content organisation.
|
120
|
-
if index.nil?
|
121
|
-
index = graph.create_node do |collection_node|
|
122
|
-
collection_node.label = :collection
|
123
|
-
collection_node.props[:type] = type
|
124
|
-
collection_node.props[:name] = type
|
125
|
-
collection_node.props[:slug] = type.to_s
|
126
|
-
collection_node.props[:title] = metadata[:title]
|
127
|
-
|
128
|
-
# Override default status so that mapped index collections always show
|
129
|
-
# up in the resulting document manifest, when they don’t have
|
130
|
-
# associated metadata. This is the opposite of how individual pieces
|
131
|
-
# of content behave (default to draft status if one isn’t supplied).
|
132
|
-
collection_node.props[:status] = if data[:status]
|
133
|
-
data[:status]
|
134
|
-
else
|
135
|
-
"published"
|
136
|
-
end
|
137
|
-
|
138
|
-
# TODO: URL generation might need to happen elsewhere
|
139
|
-
collection_node.props[:url] = if data[:url]
|
140
|
-
data[:url]
|
141
|
-
else
|
142
|
-
"/#{type}/"
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
build_content_nodes(graph, objects, type, index)
|
148
|
-
end
|
149
|
-
|
150
|
-
# Go through each subcollection and expand content nodes step by step.
|
151
|
-
subcollections.each do |path, index|
|
152
|
-
# Group files matching the same slug under a common key
|
153
|
-
objects = graph.n(path: path).out(:file).all.select do |file|
|
154
|
-
file.props[:name].end_with?(*exts)
|
155
|
-
end.group_by do |file|
|
156
|
-
file.props[:slug]
|
157
|
-
end
|
158
|
-
|
159
|
-
build_content_nodes(graph, objects, type, index)
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
def build_content_nodes(graph, objects, type, parent_index)
|
164
|
-
# TODO: this may need to use a strategy that can be overriden
|
165
|
-
content_type = Yarrow::Symbols.to_singular(type)
|
166
|
-
|
167
|
-
# Process collected content objects and generate entity nodes
|
168
|
-
objects.each do |name, sources|
|
169
|
-
item_node = graph.create_node do |node|
|
170
|
-
# TODO: Rename this to :entry and support similar fields to Atom
|
171
|
-
node.label = :item
|
172
|
-
node.props[:name] = name
|
173
|
-
node.props[:type] = content_type
|
174
|
-
|
175
|
-
meta = {}
|
176
|
-
content = ""
|
177
|
-
|
178
|
-
sources.each do |source|
|
179
|
-
body, data = process_content(source.props[:entry])
|
180
|
-
meta.merge!(data) unless data.nil?
|
181
|
-
content << body unless body.nil?
|
182
|
-
end
|
183
|
-
|
184
|
-
if meta[:url]
|
185
|
-
# If a URL is explicitly proided in metadata then use it
|
186
|
-
node.props[:url] = meta[:url]
|
187
|
-
elsif meta[:permalink]
|
188
|
-
# Support for legacy permalink attribute
|
189
|
-
node.props[:url] = meta[:permalink]
|
190
|
-
else
|
191
|
-
# Default URL generation strategy when no explicit URL is provided
|
192
|
-
# TODO: collection nodes need URL generation too
|
193
|
-
# TODO: replace this with URL generation strategy
|
194
|
-
# TODO: slug vs name - why do some nodes have 2 and some 3 props?
|
195
|
-
node.props[:url] = if parent_index.props[:name].to_sym == parent_index.props[:type]
|
196
|
-
"/#{parent_index.props[:type]}/#{name}"
|
197
|
-
else
|
198
|
-
"/#{parent_index.props[:type]}/#{parent_index.props[:slug]}/#{name}"
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
# For now, we are storing title, url, etc on the top-level item.
|
203
|
-
node.props[:title] = meta[:title]
|
204
|
-
|
205
|
-
# TODO: What belongs on the entity and what belongs on the item?
|
206
|
-
entity_props = meta.merge(body: content, name: meta[:id], url: node.props[:url])
|
207
|
-
|
208
|
-
|
209
|
-
# TODO: consider whether to provide `body` on the item/document or at
|
210
|
-
# the custom content type level.
|
211
|
-
begin
|
212
|
-
content_struct = Yarrow::Symbols.to_const(content_type)
|
213
|
-
rescue
|
214
|
-
# No immutable struct found: fall back to slower dynamically typed open struct
|
215
|
-
require "ostruct"
|
216
|
-
content_struct = OpenStruct
|
217
|
-
end
|
218
|
-
|
219
|
-
node.props[:entity] = content_struct.new(entity_props)
|
220
|
-
end
|
221
|
-
|
222
|
-
# Connect entity with source content
|
223
|
-
sources.each do |source|
|
224
|
-
graph.create_edge do |edge|
|
225
|
-
edge.label = :source
|
226
|
-
edge.from = item_node
|
227
|
-
edge.to = source.id
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
# Connect entity with parent collection
|
232
|
-
graph.create_edge do |edge|
|
233
|
-
edge.label = :child
|
234
|
-
edge.from = parent_index
|
235
|
-
edge.to = item_node
|
236
|
-
end
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
@@ -1,42 +0,0 @@
|
|
1
|
-
gem "strings-inflection"
|
2
|
-
|
3
|
-
module Yarrow
|
4
|
-
module Content
|
5
|
-
class ObjectType
|
6
|
-
Value = Yarrow::Schema::Value.new(:collection, :entity, :extensions)
|
7
|
-
|
8
|
-
DEFAULT_EXTENSIONS = [".md", ".yml", ".htm"]
|
9
|
-
|
10
|
-
def self.from_name(name)
|
11
|
-
new(Value.new(collection: name.to_sym))
|
12
|
-
end
|
13
|
-
|
14
|
-
def initialize(properties)
|
15
|
-
unless properties.respond_to?(:collection) || properties.respond_to?(:entity)
|
16
|
-
raise "Must provide a collection name or entity name"
|
17
|
-
end
|
18
|
-
|
19
|
-
@properties = properties
|
20
|
-
end
|
21
|
-
|
22
|
-
def collection
|
23
|
-
return @properties.collection if @properties.collection
|
24
|
-
Yarrow::Symbols.to_plural(@properties.entity)
|
25
|
-
end
|
26
|
-
|
27
|
-
def entity
|
28
|
-
return @properties.entity if @properties.entity
|
29
|
-
Yarrow::Symbols.to_singular(@properties.collection)
|
30
|
-
end
|
31
|
-
|
32
|
-
def extensions
|
33
|
-
return @properties.extensions if @properties.extensions
|
34
|
-
DEFAULT_EXTENSIONS
|
35
|
-
end
|
36
|
-
|
37
|
-
def match_path
|
38
|
-
"."
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
module Yarrow
|
2
|
-
module Content
|
3
|
-
# Collects a digraph of all directories and files underneath the given input
|
4
|
-
# directory.
|
5
|
-
class SourceCollector
|
6
|
-
def self.collect(input_dir)
|
7
|
-
Mementus::Graph.new(is_mutable: true) do
|
8
|
-
root = create_node do |root|
|
9
|
-
root.label = :root
|
10
|
-
end
|
11
|
-
|
12
|
-
root_dir_entry = Pathname.new(input_dir)
|
13
|
-
|
14
|
-
root_dir = create_node do |dir|
|
15
|
-
dir.label = :directory
|
16
|
-
dir.props = {
|
17
|
-
name: root_dir_entry.basename.to_s,
|
18
|
-
path: root_dir_entry.to_s,
|
19
|
-
entry: root_dir_entry
|
20
|
-
}
|
21
|
-
end
|
22
|
-
|
23
|
-
create_edge do |child|
|
24
|
-
child.label = :child
|
25
|
-
child.from = root.id
|
26
|
-
child.to = root_dir.id
|
27
|
-
end
|
28
|
-
|
29
|
-
directories = {
|
30
|
-
root_dir_entry.to_s => root_dir.id
|
31
|
-
}
|
32
|
-
|
33
|
-
Pathname.glob("#{input_dir}/**/**").each do |entry|
|
34
|
-
if entry.directory?
|
35
|
-
content_node = create_node do |dir|
|
36
|
-
dir.label = :directory
|
37
|
-
# dir.props[:name] = entry.basename.to_s
|
38
|
-
# dir.props[:slug] = entry.basename.to_s
|
39
|
-
# dir.props[:path] = entry.to_s
|
40
|
-
# dir.props[:entry] = entry
|
41
|
-
dir.props = {
|
42
|
-
name: entry.basename.to_s,
|
43
|
-
path: entry.to_s,
|
44
|
-
entry: entry
|
45
|
-
}
|
46
|
-
end
|
47
|
-
|
48
|
-
directories[entry.to_s] = content_node.id
|
49
|
-
else
|
50
|
-
content_node = create_node do |file|
|
51
|
-
file.label = :file
|
52
|
-
# file.props[:name] = entry.basename.to_s
|
53
|
-
# file.props[:slug] = entry.basename.sub_ext('').to_s
|
54
|
-
# file.props[:path] = entry.to_s
|
55
|
-
# file.props[:entry] = entry
|
56
|
-
|
57
|
-
file.props = {
|
58
|
-
name: entry.basename.to_s,
|
59
|
-
ext: entry.extname.to_s,
|
60
|
-
path: entry.to_s,
|
61
|
-
entry: entry
|
62
|
-
}
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
if directories.key?(entry.dirname.to_s)
|
67
|
-
create_edge do |edge|
|
68
|
-
edge.label = :child
|
69
|
-
edge.from = directories[entry.dirname.to_s]
|
70
|
-
edge.to = content_node
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|