yarrow 0.7.1 → 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/lib/yarrow/config.rb +9 -7
- data/lib/yarrow/configuration.rb +4 -4
- data/lib/yarrow/console_runner.rb +5 -8
- data/lib/yarrow/content/expansion.rb +16 -0
- data/lib/yarrow/content/expansion_strategy.rb +51 -0
- data/lib/yarrow/content/graph.rb +16 -3
- data/lib/yarrow/content/manifest.rb +61 -0
- 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 +92 -0
- data/lib/yarrow/generator.rb +49 -19
- data/lib/yarrow/output/result.rb +13 -0
- data/lib/yarrow/output/web/indexed_file.rb +7 -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 +73 -0
- data/lib/yarrow/schema/value.rb +46 -0
- data/lib/yarrow/schema.rb +28 -119
- data/lib/yarrow/symbols.rb +15 -0
- data/lib/yarrow/version.rb +1 -1
- data/lib/yarrow.rb +15 -4
- data/yarrow.gemspec +2 -2
- metadata +22 -12
- data/lib/yarrow/content/collection_expander.rb +0 -218
- data/lib/yarrow/content/content_type.rb +0 -42
- data/lib/yarrow/content/source_collector.rb +0 -55
@@ -1,218 +0,0 @@
|
|
1
|
-
module Yarrow
|
2
|
-
module Content
|
3
|
-
class CollectionExpander
|
4
|
-
include Yarrow::Tools::FrontMatter
|
5
|
-
|
6
|
-
def initialize(content_types=nil)
|
7
|
-
@content_types = content_types || [
|
8
|
-
Yarrow::Content::ContentType.from_name(:pages)
|
9
|
-
]
|
10
|
-
end
|
11
|
-
|
12
|
-
def expand(graph)
|
13
|
-
@content_types.each do |content_type|
|
14
|
-
expand_nested(graph, content_type)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def expand_nested(graph, content_type)
|
19
|
-
type = content_type.collection
|
20
|
-
exts = content_type.extensions
|
21
|
-
|
22
|
-
# If match path represents entire content dir, then include the entire
|
23
|
-
# content dir instead of scanning from a subfolder matching the name of
|
24
|
-
# the collection.
|
25
|
-
start_node = if content_type.match_path == "."
|
26
|
-
graph.n(:root)
|
27
|
-
else
|
28
|
-
graph.n(:root).out(name: type.to_s)
|
29
|
-
end
|
30
|
-
|
31
|
-
# Extract metadata from given start node
|
32
|
-
data = extract_metadata(start_node, type)
|
33
|
-
|
34
|
-
# Collects all nested collections in the subgraph for this content type
|
35
|
-
subcollections = {}
|
36
|
-
index = nil
|
37
|
-
|
38
|
-
# Define alias for accessing metadata in the loop
|
39
|
-
metadata = data
|
40
|
-
|
41
|
-
# Scan and collect all nested directories under the top level source
|
42
|
-
start_node.depth_first.each do |node|
|
43
|
-
if node.label == :directory
|
44
|
-
# Check if this entry has metadata defined at the top level
|
45
|
-
if data[:collections]
|
46
|
-
item = data[:collections].find { |c| c[:slug] == node.props[:slug] }
|
47
|
-
metadata = item if item
|
48
|
-
end
|
49
|
-
|
50
|
-
# Create a collection node representing a collection of documents
|
51
|
-
index = graph.create_node do |collection_node|
|
52
|
-
collection_node.label = :collection
|
53
|
-
collection_node.props[:type] = type
|
54
|
-
collection_node.props[:name] = node.props[:name]
|
55
|
-
collection_node.props[:slug] = node.props[:slug]
|
56
|
-
collection_node.props[:title] = metadata[:title]
|
57
|
-
|
58
|
-
# Override default status so that mapped index collections always show
|
59
|
-
# up in the resulting document manifest, when they don’t have
|
60
|
-
# associated metadata. This is the opposite of how individual pieces
|
61
|
-
# of content behave (default to draft status if one isn’t supplied).
|
62
|
-
collection_node.props[:status] = if data[:status]
|
63
|
-
data[:status]
|
64
|
-
else
|
65
|
-
"published"
|
66
|
-
end
|
67
|
-
|
68
|
-
# TODO: URL generation might need to happen elsewhere
|
69
|
-
collection_node.props[:url] = if data[:url]
|
70
|
-
data[:url]
|
71
|
-
else
|
72
|
-
"#{node.props[:path].split('./content').last}/"
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# Add this collection id to the lookup table for edge construction
|
77
|
-
subcollections[node.props[:path]] = index
|
78
|
-
|
79
|
-
# Join the collection to its parent
|
80
|
-
unless node.props[:slug] == type.to_s || !subcollections.key?(node.props[:entry].parent.to_s)
|
81
|
-
graph.create_edge do |edge|
|
82
|
-
edge.label = :child
|
83
|
-
edge.from = subcollections[node.props[:entry].parent.to_s].id
|
84
|
-
edge.to = index.id
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
subcollections.each do |path, index|
|
91
|
-
# Group files matching the same slug under a common key
|
92
|
-
objects = graph.n(path: path).out(:file).all.select do |file|
|
93
|
-
file.props[:name].end_with?(*exts)
|
94
|
-
end.group_by do |file|
|
95
|
-
file.props[:slug]
|
96
|
-
end
|
97
|
-
|
98
|
-
build_content_nodes(graph, objects, type, index)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
# Extract collection level configuration/metadata from the root node for
|
103
|
-
# this content type.
|
104
|
-
def extract_metadata(node, type)
|
105
|
-
# TODO: support _index or _slug convention as well
|
106
|
-
meta_file = node.out(slug: type.to_s).first
|
107
|
-
|
108
|
-
if meta_file
|
109
|
-
# Process metadata and add it to the collection node
|
110
|
-
# TODO: pass in content converter object
|
111
|
-
# TODO: index/body content by default if extracted from frontmatter
|
112
|
-
body, data = process_content(meta_file.props[:entry])
|
113
|
-
else
|
114
|
-
# Otherwise, assume default collection behaviour
|
115
|
-
data = {}
|
116
|
-
end
|
117
|
-
|
118
|
-
# Generate a default title if not provided in metadata
|
119
|
-
unless data.key?(:title)
|
120
|
-
data[:title] = type.to_s.capitalize
|
121
|
-
end
|
122
|
-
|
123
|
-
data
|
124
|
-
end
|
125
|
-
|
126
|
-
def build_content_nodes(graph, objects, type, parent_index)
|
127
|
-
# TODO: this may need to use a strategy that can be overriden
|
128
|
-
content_type = Yarrow::Symbols.to_singular(type)
|
129
|
-
|
130
|
-
# Process collected content objects and generate entity nodes
|
131
|
-
objects.each do |name, sources|
|
132
|
-
item_node = graph.create_node do |node|
|
133
|
-
# TODO: Rename this to :entry and support similar fields to Atom
|
134
|
-
node.label = :item
|
135
|
-
node.props[:name] = name
|
136
|
-
node.props[:type] = content_type
|
137
|
-
|
138
|
-
meta = {}
|
139
|
-
content = ""
|
140
|
-
|
141
|
-
sources.each do |source|
|
142
|
-
body, data = process_content(source.props[:entry])
|
143
|
-
meta.merge!(data) unless data.nil?
|
144
|
-
content << body unless body.nil?
|
145
|
-
end
|
146
|
-
|
147
|
-
if meta[:url]
|
148
|
-
# If a URL is explicitly proided in metadata then use it
|
149
|
-
node.props[:url] = meta[:url]
|
150
|
-
elsif meta[:permalink]
|
151
|
-
# Support for legacy permalink attribute
|
152
|
-
node.props[:url] = meta[:permalink]
|
153
|
-
else
|
154
|
-
# Default URL generation strategy when no explicit URL is provided
|
155
|
-
# TODO: collection nodes need URL generation too
|
156
|
-
# TODO: replace this with URL generation strategy
|
157
|
-
# TODO: slug vs name - why do some nodes have 2 and some 3 props?
|
158
|
-
node.props[:url] = if parent_index.props[:name].to_sym == parent_index.props[:type]
|
159
|
-
"/#{parent_index.props[:type]}/#{name}"
|
160
|
-
else
|
161
|
-
"/#{parent_index.props[:type]}/#{parent_index.props[:slug]}/#{name}"
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
# For now, we are storing title, url, etc on the top-level item.
|
166
|
-
node.props[:title] = meta[:title]
|
167
|
-
|
168
|
-
# TODO: What belongs on the entity and what belongs on the item?
|
169
|
-
entity_props = meta.merge(body: content, name: meta[:id], url: node.props[:url])
|
170
|
-
|
171
|
-
|
172
|
-
# TODO: consider whether to provide `body` on the item/document or at
|
173
|
-
# the custom content type level.
|
174
|
-
begin
|
175
|
-
content_struct = Yarrow::Symbols.to_const(content_type)
|
176
|
-
rescue
|
177
|
-
# No immutable struct found: fall back to slower dynamically typed open struct
|
178
|
-
require "ostruct"
|
179
|
-
content_struct = OpenStruct
|
180
|
-
end
|
181
|
-
|
182
|
-
node.props[:entity] = content_struct.new(entity_props)
|
183
|
-
end
|
184
|
-
|
185
|
-
# Connect entity with source content
|
186
|
-
sources.each do |source|
|
187
|
-
graph.create_edge do |edge|
|
188
|
-
edge.label = :source
|
189
|
-
edge.from = item_node
|
190
|
-
edge.to = source.id
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
# Connect entity with parent collection
|
195
|
-
graph.create_edge do |edge|
|
196
|
-
edge.label = :child
|
197
|
-
edge.from = parent_index
|
198
|
-
edge.to = item_node
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
# Workaround for handling meta and content source in multiple files or a single
|
204
|
-
# file with front matter.
|
205
|
-
def process_content(path)
|
206
|
-
case path.extname
|
207
|
-
when '.htm', '.md'
|
208
|
-
read_split_content(path.to_s, symbolize_keys: true)
|
209
|
-
# when '.md'
|
210
|
-
# body, data = read_split_content(path.to_s, symbolize_keys: true)
|
211
|
-
# [Kramdown::Document.new(body).to_html, data]
|
212
|
-
when '.yml'
|
213
|
-
[nil, YAML.load(File.read(path.to_s), symbolize_names: true)]
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
217
|
-
end
|
218
|
-
end
|
@@ -1,42 +0,0 @@
|
|
1
|
-
gem "strings-inflection"
|
2
|
-
|
3
|
-
module Yarrow
|
4
|
-
module Content
|
5
|
-
class ContentType
|
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,55 +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
|
-
directories = {
|
13
|
-
Pathname.new(input_dir).to_s => root.id
|
14
|
-
}
|
15
|
-
|
16
|
-
Pathname.glob("#{input_dir}/**/**").each do |entry|
|
17
|
-
if entry.directory?
|
18
|
-
#puts "Reading directory: #{entry}"
|
19
|
-
|
20
|
-
content_node = create_node do |dir|
|
21
|
-
dir.label = :directory
|
22
|
-
dir.props[:name] = entry.basename.to_s
|
23
|
-
dir.props[:slug] = entry.basename.to_s
|
24
|
-
dir.props[:path] = entry.to_s
|
25
|
-
dir.props[:entry] = entry
|
26
|
-
end
|
27
|
-
|
28
|
-
directories[entry.to_s] = content_node.id
|
29
|
-
else
|
30
|
-
#puts "Reading file: #{entry} (#{entry.basename.sub_ext('')})"
|
31
|
-
|
32
|
-
content_node = create_node do |file|
|
33
|
-
file.label = :file
|
34
|
-
file.props[:name] = entry.basename.to_s
|
35
|
-
file.props[:slug] = entry.basename.sub_ext('').to_s
|
36
|
-
file.props[:path] = entry.to_s
|
37
|
-
file.props[:entry] = entry
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
if directories.key?(entry.dirname.to_s)
|
42
|
-
#puts "Create parent edge: #{directories[entry.dirname.to_s]}"
|
43
|
-
|
44
|
-
create_edge do |edge|
|
45
|
-
edge.label = :child
|
46
|
-
edge.from = directories[entry.dirname.to_s]
|
47
|
-
edge.to = content_node
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|