yarrow 0.8.7 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89667b38c1ebf2da983a381e20be9d8e0904bdb69137bb86a1bce6ee9507fbf1
4
- data.tar.gz: 364c196cefad90398b10c387edb925b055490163be854b814f3522a6333234ef
3
+ metadata.gz: 96b575b4a93031397d38c09ab442ce577a98378114941c6be3dcaf4c2a71a3ac
4
+ data.tar.gz: 15d01eb7b8f710aee890c8879dc202a12ecab3f3cca0030cdc7df8e61619ed75
5
5
  SHA512:
6
- metadata.gz: 113ce8dc745ec3e83121d5229beeed755671a7902e1330373ef7b129d7138048d090bbdb1abedd2458f0faca3f86f4433c30a855cc6a53119ba59576699f7002
7
- data.tar.gz: 6b24b087284b3be6f2d55a8aa3afc25d0e3cc38dbbb2bd80bd9005487b9b95b3bf65d218a47c65b6ac9f4fd54e830a01984292bc1f0c4c8d1455b7af3a5b5049
6
+ metadata.gz: be90c39d718849c20db481c2f90d105400af237c7232d4cdd091e7260fffe3d09e4828c4d50f473424daa1d223d2cf5259ed55f4120534ad884ea7ca4d2124dc
7
+ data.tar.gz: b4e5150cd8a3da147489fe0f8077520df328ae2fc5018e4ea102ca171625ada864da3a08aeb59860a98d04b7515e8f0868a023f85fb10c1718592a066d818b1b
data/CONTENT.md ADDED
@@ -0,0 +1,228 @@
1
+ # Content Management
2
+
3
+ ## Source Map
4
+
5
+ The source map specifies how content is expanded into addressable resources that define the information structure of a project.
6
+
7
+ Currently only website projects are supported, so the organisation is slightly skewed towards URL-centric representations (this can be tidied up in future in order to represent e-books and print publications).
8
+
9
+ The source map is made up of a set of expansion policies, listed in the configuration as key-value pairs.
10
+
11
+ ### Shorthand Format
12
+
13
+ The configuration format supports a shorthand for declaring policies based on defaults without needing to provide a specification with every single attribute filled in.
14
+
15
+ Although it’s not quite implemented this way in Ruby (yet?), you can think of the policy spec in the config as being a union of `String | Symbol | SpecStruct` where all `SpecStruct` attributes are also optional.
16
+
17
+ The simplest possible config shorthand assumes that the policy label is a plural reference to the collection type that gets expanded.
18
+
19
+ If the spec value is a symbol, this is interpreted as a singular reference to the entity type that gets attached to the parent collection for each matched content object. In this case, the policy label is overloaded to match the source directory root.
20
+
21
+ ```yaml
22
+ content:
23
+ source_map:
24
+ pages: :page
25
+ blog: :post
26
+ gallery: :photo
27
+ ```
28
+
29
+ This source map will be interpreted as:
30
+
31
+ - Expand a `Pages` collection of `Page` objects from the source directory `./pages`
32
+ - Expand a `Blog` collection of `Post` objects from the source directory `./blog`
33
+ - Expand a `Gallery` collection of `Photo` objects from the source directory `./gallery`
34
+
35
+ If the spec value is a string, this is interpreted as matching the source directory root for the traversal with the policy label referring to the collection type only and the entity type being a singular conversion of the plural policy label.
36
+
37
+ ```yaml
38
+ content:
39
+ source_map:
40
+ pages: "about"
41
+ notes: "archive"
42
+ photos: "gallery"
43
+ ```
44
+
45
+ This source map will be interpreted as:
46
+
47
+ - Expand a `Pages` collection of `Page` objects from the source directory `./about`
48
+ - Expand a `Notes` collection of `Note` objects from the source directory `./archive`
49
+ - Expand a `Photos` collection of `Photo` objects from the source directory `./gallery`
50
+
51
+ ### Attribute Precedence
52
+
53
+ The shorthand format offers limited possibilities for customisation, so in many situations it’s best to provide a more detailed policy spec. All attributes specified directly take precedence over shorthand conventions and defaults.
54
+
55
+ ```yaml
56
+ content:
57
+ source_map:
58
+ site:
59
+ collection: :pages
60
+ entity: :page
61
+ source_path: "about"
62
+ blog:
63
+ entity: :post
64
+ source_path: "archive"
65
+ gallery:
66
+ collection: :photos
67
+ ```
68
+
69
+ This will be interpreted as:
70
+
71
+ - Expand a `Pages` collection of `Page` objects from the source directory `./about`
72
+ - Expand a `Blog` collection of `Post` objects from the source directory `./archive`
73
+ - Expand a `Photos` collection of `Photo` objects from the source directory `./gallery`
74
+
75
+ ### Default Spec
76
+
77
+ ```yaml
78
+ content:
79
+ source_map:
80
+ site:
81
+ container: :pages
82
+ collection: :pages
83
+ entity: :page
84
+ source_path: "."
85
+
86
+ ```
87
+
88
+ ## Expansion Strategies
89
+
90
+ Each policy specified in the source map triggers a traversal of the source content graph using an event-driven pattern to report each relevant file and directory to an aggregator component which expands these into collections and entity resources.
91
+
92
+ Changing the aggregator enables a variety of different structures of nested directories and files to be expanded into a well-organised content model, providing a more flexible and creative foundation for publishing and editorial design than the rigid conventions of most other static-site generators.
93
+
94
+ ### Filename Map
95
+
96
+ ```yml
97
+ aggregator: :filename_map
98
+ ```
99
+
100
+ Expands nested directories and files with a simple 1:1 mapping to collections and files. Each matching filename becomes a single item of content.
101
+
102
+ #### Example
103
+
104
+ Source:
105
+
106
+ ```
107
+ 🖿 content
108
+ └──🖿 pages
109
+ ├──🗎page1.md
110
+ ├──🗎page2.md
111
+ ├──🗎page3.md
112
+ └──🖿 children
113
+ ├──🗎page4.md
114
+ └──🗎page5.md
115
+ ```
116
+
117
+ Policy:
118
+
119
+ ```
120
+ content:
121
+ source_map:
122
+ pages:
123
+ aggregator: :filename_map
124
+ ```
125
+
126
+ Expansion:
127
+
128
+ ```mermaid
129
+ graph TD;
130
+ pages((Pages: pages))-->page1(Page: page1);
131
+ pages-->page2(Page: page2);
132
+ pages-->page3(Page: page3);
133
+ pages-->children((Pages: children));
134
+ children-->page4(Page: page4);
135
+ children-->page5(Page: page5);
136
+ ```
137
+
138
+ ### Directory Merge
139
+
140
+ ```yml
141
+ aggregator: :directory_merge
142
+ ```
143
+
144
+ Expands from a list of directories, with each directory representing a single item of content.
145
+
146
+ This is a good choice for rich-content websites with more complex content layouts for articles or interactive essays where a variety of assets are developed alongside the main manuscript file.
147
+
148
+ #### Example
149
+
150
+ Source:
151
+
152
+ ```
153
+ 🖿 content
154
+ └──🖿 essays
155
+ └──🖿 concept1
156
+ ├──🗎concept-1.md
157
+ ├──🖻image1.png
158
+ ├──🖻image2.svg
159
+ ├──🖻image3.jpg
160
+ ├──🗎data.json
161
+ └──🗎loop.mp3
162
+ ```
163
+
164
+ Policy:
165
+
166
+ ```yml
167
+ content:
168
+ source_map:
169
+ essays:
170
+ collection: :essays
171
+ aggregator: :directory_merge
172
+ source_path: "essays"
173
+ match_entities: [.md]
174
+ match_assets: [.png, .jpg, .svg, .json, .mp3]
175
+ ```
176
+
177
+ Expansion:
178
+
179
+ ```mermaid
180
+ graph TD;
181
+ essays((Essays: essays))-->concept1(Essay: concept-1);
182
+ concept1-->image1(Asset: image1.png);
183
+ concept1-->image2(Asset: image2.png);
184
+ concept1-->image3(Asset: image3.jpg);
185
+ concept1-->data(Asset: data.json);
186
+ concept1-->loop(Asset: loop.mp3);
187
+ ```
188
+
189
+ ### Directory
190
+
191
+ ```
192
+ expansion_strategy:
193
+ aggregator: :directory_source
194
+ match_source: "essays"
195
+ match_entities: "*.md"
196
+ match_assets: "*.jpg"
197
+ ```
198
+
199
+ ## Collection Containers
200
+
201
+ Some content models may require collection types to be nested children of a top-level parent type. To define this in a policy you can manually specify a `container` which will override the default `collection` at the root level of the content hierarchy. Containers represent ‘collections of collections’, offering more modelling flexibility and applicability to wider variety of website use cases.
202
+
203
+ ```
204
+ 🖿 content
205
+ └──🖿 gallery
206
+ ├──🖿 exhibition-a
207
+ │ ├──🗎1.htm
208
+ │ ├──🗎2.htm
209
+ │ ├──🗎3.htm
210
+ │ ├──🗎a-1.jpg
211
+ │ ├──🗎a-2.jpg
212
+ │ └──🗎a-3.jpg
213
+ └──🖿 exhibition-b
214
+ ├──🗎1.htm
215
+ ├──🗎2.htm
216
+ ├──🗎b-1.jpg
217
+ └──🗎b-2.jpg
218
+ ```
219
+
220
+ The following policy spec allows us to model a gallery type which holds a list of exhibitions, with each exhibition in turn holding a list of artworks.
221
+
222
+ ```yml
223
+ container: :gallery
224
+ collection: :exhibitions
225
+ entity: :artwork
226
+ ```
227
+
228
+ In many cases it might be fine to just nest instances of a single collection type, but this more fine-grained content modelling can be useful when it comes to templating and editorial design.
data/README.md CHANGED
@@ -38,14 +38,14 @@ Roadmap
38
38
 
39
39
  A rough sketch of the project direction.
40
40
 
41
- | Version | Features |
42
- |---------|----------|
43
- | `0.7` | Content model/object mapping, template/site context |
44
- | `0.8` | HTML publishing workflow |
45
- | `0.9` | PDF publishing workflow |
46
- | `0.10` | Media and video publishing workflow |
47
- | `0.11` | Generic command line runner |
48
- | `1.0` | Refactoring, performance fixes, lock down API |
41
+ | Version | Features |
42
+ |---------|-------------------------------------------------|
43
+ | `0.9` | Support standard text formats and linked assets |
44
+ | `0.10` | Custom Markdown components |
45
+ | `0.11` | Publishing support for S3 and GitHub/Netlify |
46
+ | `0.12` | Clean up local web server and watcher |
47
+ | `0.13` | Content structure transformations |
48
+ | `1.0` | Reintroduce generic command line runner |
49
49
 
50
50
  License
51
51
  -------
@@ -7,6 +7,13 @@ module Mementus
7
7
  # methods. Mostly used for #map. API needs to be fixed in the gem itself.
8
8
  include Enumerable
9
9
 
10
+ def traverse_by(traversal)
11
+ case traversal
12
+ when :tree then depth_first
13
+ when :list then nodes
14
+ end
15
+ end
16
+
10
17
  def to
11
18
  Step.new(map { |edge| edge.to }, Pipe.new(graph), graph)
12
19
  end
@@ -36,6 +43,28 @@ module Mementus
36
43
  "<Mementus::Graph @structure=#{@structure.inspect} " +
37
44
  "nodes_count=#{nodes_count} edges_count=#{edges_count}>"
38
45
  end
46
+
47
+ def to_dot
48
+ statements = []
49
+
50
+ nodes.each do |node|
51
+ label = if node.props.key?(:type)
52
+ "#{node.label}: #{node.props[:type]}:#{node.props[:resource].name}"
53
+ elsif node.props.key?(:name)
54
+ "#{node.label}: #{node.props[:name]}"
55
+ else
56
+ node.label
57
+ end
58
+
59
+ statements << "#{node.id} [label=\"#{label}\"]"
60
+ end
61
+
62
+ edges.each do |edge|
63
+ statements << "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.label}\"];"
64
+ end
65
+
66
+ "digraph {\n#{statements.join("\n")}\n}"
67
+ end
39
68
  end
40
69
 
41
70
  class Node
@@ -0,0 +1,88 @@
1
+ module Yarrow
2
+ module Content
3
+ module Expansion
4
+ class Aggregator
5
+ attr_reader :graph
6
+
7
+ def initialize(graph)
8
+ @graph = graph
9
+ @collections = {}
10
+ end
11
+
12
+ def before_traversal(policy)
13
+ end
14
+
15
+ def expand_container(container, policy)
16
+ end
17
+
18
+ def expand_collection(collection, policy)
19
+ end
20
+
21
+ def expand_entity(entity, policy)
22
+ end
23
+
24
+ def after_traversal(policy)
25
+ end
26
+
27
+ private
28
+
29
+ def create_collection(source_node, type, collection_const)
30
+ # Create a collection node with attached resource model
31
+ index = graph.create_node do |collection_node|
32
+ attributes = {
33
+ name: source_node.props[:name],
34
+ title: source_node.props[:name].capitalize,
35
+ body: ""
36
+ }
37
+ collection_node.label = :collection
38
+ collection_node.props[:type] = type
39
+ collection_node.props[:resource] = collection_const.new(attributes)
40
+ end
41
+
42
+ # Add this collection id to the lookup table for edge construction
43
+ @collections[source_node.props[:path]] = index
44
+
45
+ # Join the collection to its parent
46
+ if @collections.key?(source_node.props[:entry].parent.to_s)
47
+ graph.create_edge do |edge|
48
+ edge.label = :child
49
+ edge.from = @collections[source_node.props[:entry].parent.to_s].id
50
+ edge.to = index.id
51
+ end
52
+ end
53
+ end
54
+
55
+ def create_entity(source_node, parent_path, type, entity_const)
56
+ # Create an entity node with attached resource model
57
+ entity = graph.create_node do |entity_node|
58
+ attributes = {
59
+ name: source_node.props[:basename],
60
+ title: source_node.props[:basename].capitalize,
61
+ body: ""
62
+ }
63
+ entity_node.label = :entity
64
+ entity_node.props[:type] = type
65
+ entity_node.props[:resource] = entity_const.new(attributes)
66
+ end
67
+
68
+ graph.create_edge do |edge|
69
+ edge.label = :source
70
+ edge.from = entity.id
71
+ edge.to = source_node.id
72
+ end
73
+
74
+ if @collections.key?(parent_path)
75
+ graph.create_edge do |edge|
76
+ edge.label = :child
77
+ edge.from = @collections[parent_path].id
78
+ edge.to = entity.id
79
+ end
80
+ end
81
+ end
82
+
83
+ def connect_entity(entity, collection)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,44 @@
1
+ module Yarrow
2
+ module Content
3
+ module Expansion
4
+ class BasenameMerge < Aggregator
5
+ def before_traversal(policy)
6
+ @bundles = {}
7
+ @container_collection = nil
8
+ @entity_bundles = {}
9
+ @entity_collections = {}
10
+ end
11
+
12
+ def expand_container(container, policy)
13
+ puts "create_node label=:collection type=:#{policy.collection} name='#{container.props[:basename]}'"
14
+ @container_collection = container.props[:basename]
15
+ end
16
+
17
+ def expand_collection(collection, policy)
18
+ if @container_collection == collection.props[:entry].parent.to_s
19
+ puts "create_node label=:collection type=:#{policy.entity} name='#{collection.props[:basename]}' collection='#{@container_collection}'"
20
+ end
21
+ end
22
+
23
+ def expand_entity(entity, policy)
24
+ unless @entity_bundles.key?(entity.props[:basename])
25
+ @entity_bundles[entity.props[:basename]] = []
26
+ end
27
+
28
+ @entity_bundles[entity.props[:basename]] << entity
29
+ @entity_collections[entity.props[:basename]] = @container_collection
30
+ end
31
+
32
+ def after_traversal(policy)
33
+ @entity_bundles.each do |basename, bundle|
34
+ puts "create_node label=:resource type=:#{policy.entity} name='#{basename}' collection='#{@entity_collections[basename]}'"
35
+
36
+ bundle.each do |asset|
37
+ puts "create_node label=:asset extension='#{asset.props[:ext]}' resource='#{basename}'"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ module Yarrow
2
+ module Content
3
+ module Expansion
4
+ class DirectoryMap < Aggregator
5
+ def expand_container(container, policy)
6
+ puts "create_node label=:collection type=:#{policy.container} name='#{container.props[:basename]}'"
7
+ @current_collection = container.props[:basename]
8
+ end
9
+
10
+ def expand_collection(collection, policy)
11
+ puts "create_node label=:collection type=:#{policy.collection} name='#{collection.props[:basename]}' collection=#{@current_collection}"
12
+ @current_collection = collection.props[:basename]
13
+ end
14
+
15
+ def expand_entity(entity, policy)
16
+ puts "create_node label=:entity type=:#{policy.entity} name='#{entity.props[:basename]}' collection='#{@current_collection}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ module Yarrow
2
+ module Content
3
+ module Expansion
4
+ class DirectoryMerge < Aggregator
5
+ def before_traversal(policy)
6
+ @bundles = {}
7
+ @current_collection = nil
8
+ @current_entity = nil
9
+ end
10
+
11
+ def expand_container(container, policy)
12
+ create_collection(container, policy.container, policy.container_const)
13
+ @current_collection = container.props[:path]
14
+ end
15
+
16
+ def expand_collection(collection, policy)
17
+ @current_entity = collection.props[:basename]
18
+ end
19
+
20
+ def expand_entity(entity, policy)
21
+ if entity.props[:basename] == @current_entity && entity.props[:ext] == ".md"
22
+ create_entity(entity, @current_collection, policy.entity, policy.entity_const)
23
+ else
24
+ # TODO: attach static assets to the entity as well
25
+ #puts "--> create_node label=:asset type=:asset name='#{entity.props[:basename]}' entity='#{@current_entity}'"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module Yarrow
2
+ module Content
3
+ module Expansion
4
+ class FileList < Strategy
5
+ def expand(policy)
6
+ start_node = graph.n(:root).out(name: policy.container.to_s)
7
+
8
+ start_node.out(:files).each do |file_node|
9
+
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ module Yarrow
2
+ module Content
3
+ module Expansion
4
+ class FilenameMap < Aggregator
5
+ def expand_container(container, policy)
6
+ create_collection(container, policy.container, policy.container_const)
7
+ @current_collection = container.props[:path]
8
+ end
9
+
10
+ def expand_collection(collection, policy)
11
+ create_collection(collection, policy.collection, policy.collection_const)
12
+ @current_collection = collection.props[:path]
13
+ end
14
+
15
+ def expand_entity(entity, policy)
16
+ if policy.match_by_extension(entity.props[:ext])
17
+ parent_path = entity.incoming(:directory).first.props[:path]
18
+ create_entity(entity, parent_path, policy.entity, policy.entity_const)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -8,6 +8,36 @@ module Yarrow
8
8
 
9
9
  def initialize(graph)
10
10
  @graph = graph
11
+ @subcollections = {}
12
+ @entity_links = []
13
+ @index_links = []
14
+ @index = nil
15
+ end
16
+
17
+ # Expand a directory to a collection node representing a collection of entities
18
+ def expand_directory(policy, node)
19
+ index = graph.create_node do |collection_node|
20
+
21
+ collection_attrs = {
22
+ name: node.props[:name],
23
+ title: node.props[:name].capitalize,
24
+ body: ""
25
+ }
26
+
27
+ populate_collection(collection_node, policy, collection_attrs)
28
+ end
29
+
30
+ # Add this collection id to the lookup table for edge construction
31
+ @subcollections[node.props[:path]] = index
32
+
33
+ # Join the collection to its parent
34
+ unless node.props[:slug] == policy.collection.to_s || !@subcollections.key?(node.props[:entry].parent.to_s)
35
+ graph.create_edge do |edge|
36
+ edge.label = :child
37
+ edge.from = @subcollections[node.props[:entry].parent.to_s].id
38
+ edge.to = index.id
39
+ end
40
+ end
11
41
  end
12
42
 
13
43
  # Extract collection level configuration/metadata from the root node for
@@ -65,6 +95,23 @@ module Yarrow
65
95
  end
66
96
  # TODO: Raise error if unsupported extname reaches here
67
97
  end
98
+
99
+ def connect_expanded_entities
100
+ # Once all files and directories have been expanded, connect all the child
101
+ # edges between collections and entities
102
+ @entity_links.each do |entity_link|
103
+ graph.create_edge do |edge|
104
+ edge.label = :child
105
+ edge.from = entity_link[:parent_id].id
106
+ edge.to = entity_link[:child_id].id
107
+ end
108
+ end
109
+
110
+ # Merge index page body and metadata with their parent collections
111
+ @index_links.each do |index_link|
112
+ merge_collection_index(index_link[:parent_id], policy, index_link[:index_attrs])
113
+ end
114
+ end
68
115
  end
69
116
  end
70
117
  end
@@ -0,0 +1,67 @@
1
+ module Yarrow
2
+ module Content
3
+ module Expansion
4
+ class Traversal
5
+ attr_reader :graph, :policy, :aggregator
6
+
7
+ def initialize(graph, policy)
8
+ @graph = graph
9
+ @policy = policy
10
+ @aggregator = policy.aggregator_const.new(graph)
11
+ end
12
+
13
+ # If source path represents entire content dir, then include the entire
14
+ # content dir instead of scanning from a subfolder matching the name of
15
+ # the collection.
16
+ def source_node
17
+ if policy.source_path == "."
18
+ graph.n(:root).out(:directory)
19
+ else
20
+ graph.n(name: policy.source_path)
21
+ end
22
+ end
23
+
24
+ def on_root_visited(root_node)
25
+ aggregator.expand_container(root_node, policy)
26
+ end
27
+
28
+ def on_directory_visited(dir_node)
29
+ # TODO: match on potential directory extension/filter
30
+ aggregator.expand_collection(dir_node, policy)
31
+ end
32
+
33
+ def on_file_visited(file_node)
34
+ # TODO: dispatch underscore prefix or index files separately
35
+ # TODO: match on file extension
36
+ aggregator.expand_entity(file_node, policy)
37
+ end
38
+
39
+ def on_traversal_initiated
40
+ aggregator.before_traversal(policy)
41
+ end
42
+
43
+ def on_traversal_completed
44
+ aggregator.after_traversal(policy)
45
+ end
46
+
47
+ def expand
48
+ on_traversal_initiated
49
+
50
+ traversal = source_node.depth_first.each
51
+
52
+ on_root_visited(traversal.next)
53
+
54
+ loop do
55
+ node = traversal.next
56
+ case node.label
57
+ when :directory then on_directory_visited(node)
58
+ when :file then on_file_visited(node)
59
+ end
60
+ end
61
+
62
+ on_traversal_completed
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end