yarrow 0.8.6 → 0.9.0
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/.github/workflows/ruby.yml +1 -1
- data/CONTENT.md +228 -0
- data/README.md +8 -8
- data/lib/extensions/mementus.rb +29 -0
- data/lib/yarrow/config.rb +10 -0
- data/lib/yarrow/content/expansion/aggregator.rb +88 -0
- data/lib/yarrow/content/expansion/basename_merge.rb +44 -0
- data/lib/yarrow/content/expansion/directory_map.rb +21 -0
- data/lib/yarrow/content/expansion/directory_merge.rb +31 -0
- data/lib/yarrow/content/expansion/file_list.rb +15 -0
- data/lib/yarrow/content/expansion/filename_map.rb +24 -0
- data/lib/yarrow/content/expansion/strategy.rb +47 -0
- data/lib/yarrow/content/expansion/traversal.rb +67 -0
- data/lib/yarrow/content/expansion/tree.rb +53 -73
- data/lib/yarrow/content/model.rb +3 -2
- data/lib/yarrow/content/policy.rb +77 -21
- data/lib/yarrow/content/source.rb +4 -0
- data/lib/yarrow/format/asciidoc.rb +0 -0
- data/lib/yarrow/format/html.rb +0 -0
- data/lib/yarrow/format/json.rb +0 -0
- data/lib/yarrow/format/markdown.rb +67 -0
- data/lib/yarrow/format/mediawiki.rb +0 -0
- data/lib/yarrow/format/methods/front_matter.rb +58 -0
- data/lib/yarrow/format/methods/metadata.rb +22 -0
- data/lib/yarrow/format/orgmode.rb +0 -0
- data/lib/yarrow/format/text.rb +0 -0
- data/lib/yarrow/format/xml.rb +0 -0
- data/lib/yarrow/format/yaml.rb +19 -0
- data/lib/yarrow/format.rb +71 -0
- data/lib/yarrow/schema/types.rb +41 -0
- data/lib/yarrow/version.rb +1 -1
- data/lib/yarrow/web/manifest.rb +15 -9
- data/lib/yarrow.rb +6 -9
- data/yarrow.gemspec +1 -0
- metadata +22 -3
- data/lib/yarrow/tools/content_utils.rb +0 -74
@@ -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
|
@@ -3,98 +3,35 @@ module Yarrow
|
|
3
3
|
module Expansion
|
4
4
|
class Tree < Strategy
|
5
5
|
def expand(policy)
|
6
|
-
#p graph.n(:root).out(:directory).to_a.count
|
7
|
-
#policy.match()
|
8
|
-
|
9
|
-
#p graph.n(:root).out(:directory).first.props[:name]
|
10
|
-
type = policy.container
|
11
|
-
|
12
6
|
# If match path represents entire content dir, then include the entire
|
13
7
|
# content dir instead of scanning from a subfolder matching the name of
|
14
8
|
# the collection.
|
15
9
|
#start_node = if policy.match_path == "."
|
16
|
-
start_node = if
|
10
|
+
start_node = if policy.match_path == "."
|
17
11
|
# TODO: match against source_dir
|
18
12
|
graph.n(:root).out(:directory)
|
19
13
|
else
|
20
|
-
graph.n(
|
14
|
+
graph.n(name: policy.match_path)
|
21
15
|
end
|
22
16
|
|
23
|
-
# Extract metadata from given start node
|
24
|
-
#collection_metadata = extract_metadata(start_node, policy.container)
|
25
|
-
|
26
17
|
# Collect all nested collections in the subgraph for this content type
|
27
|
-
subcollections = {}
|
28
|
-
entity_links = []
|
29
|
-
index_links = []
|
30
|
-
index = nil
|
18
|
+
@subcollections = {}
|
19
|
+
@entity_links = []
|
20
|
+
@index_links = []
|
21
|
+
@index = nil
|
31
22
|
|
32
23
|
# Scan and collect all nested files from the root
|
33
24
|
start_node.depth_first.each do |node|
|
34
25
|
if node.label == :directory
|
35
|
-
|
36
|
-
index = graph.create_node do |collection_node|
|
37
|
-
|
38
|
-
collection_attrs = {
|
39
|
-
name: node.props[:name],
|
40
|
-
title: node.props[:name].capitalize,
|
41
|
-
body: ""
|
42
|
-
}
|
43
|
-
|
44
|
-
populate_collection(collection_node, policy, collection_attrs)
|
45
|
-
end
|
46
|
-
|
47
|
-
# Add this collection id to the lookup table for edge construction
|
48
|
-
subcollections[node.props[:path]] = index
|
49
|
-
|
50
|
-
# Join the collection to its parent
|
51
|
-
unless node.props[:slug] == type.to_s || !subcollections.key?(node.props[:entry].parent.to_s)
|
52
|
-
graph.create_edge do |edge|
|
53
|
-
edge.label = :child
|
54
|
-
edge.from = subcollections[node.props[:entry].parent.to_s].id
|
55
|
-
edge.to = index.id
|
56
|
-
end
|
57
|
-
end
|
26
|
+
expand_directory(policy, node)
|
58
27
|
elsif node.label == :file
|
59
|
-
|
60
|
-
meta = {} if !meta
|
61
|
-
|
62
|
-
# TODO: document mapping convention for index pages and collection metadata
|
63
|
-
# TODO: underscore _index pattern?
|
64
|
-
bare_basename = node.props[:entry].basename(node.props[:entry].extname)
|
65
|
-
if bare_basename.to_s == "index"
|
66
|
-
index_links << {
|
67
|
-
parent_id: subcollections[node.props[:entry].parent.to_s],
|
68
|
-
index_attrs: meta.merge({ body: body})
|
69
|
-
}
|
70
|
-
else
|
71
|
-
# Create an entity node representing a file mapped to a unique content object
|
72
|
-
entity = graph.create_node do |entity_node|
|
73
|
-
|
74
|
-
entity_slug = node.props[:entry].basename(node.props[:entry].extname).to_s
|
75
|
-
|
76
|
-
entity_attrs = {
|
77
|
-
name: entity_slug,
|
78
|
-
title: entity_slug.gsub("-", " ").capitalize,
|
79
|
-
body: body
|
80
|
-
}
|
81
|
-
|
82
|
-
populate_entity(entity_node, policy, entity_attrs.merge(meta || {}))
|
83
|
-
end
|
84
|
-
|
85
|
-
# We may not have an expanded node for the parent collection if this is a
|
86
|
-
# preorder traversal so save it for later
|
87
|
-
entity_links << {
|
88
|
-
parent_id: subcollections[node.props[:entry].parent.to_s],
|
89
|
-
child_id: entity
|
90
|
-
}
|
91
|
-
end
|
28
|
+
expand_file_by_extension(policy, node)
|
92
29
|
end
|
93
30
|
end
|
94
31
|
|
95
32
|
# Once all files and directories have been expanded, connect all the child
|
96
33
|
# edges between collections and entities
|
97
|
-
entity_links.each do |entity_link|
|
34
|
+
@entity_links.each do |entity_link|
|
98
35
|
graph.create_edge do |edge|
|
99
36
|
edge.label = :child
|
100
37
|
edge.from = entity_link[:parent_id].id
|
@@ -103,10 +40,53 @@ module Yarrow
|
|
103
40
|
end
|
104
41
|
|
105
42
|
# Merge index page body and metadata with their parent collections
|
106
|
-
index_links.each do |index_link|
|
43
|
+
@index_links.each do |index_link|
|
107
44
|
merge_collection_index(index_link[:parent_id], policy, index_link[:index_attrs])
|
108
45
|
end
|
109
46
|
end
|
47
|
+
|
48
|
+
def expand_file_by_basename(policy, node)
|
49
|
+
body, meta = process_content(node.props[:entry])
|
50
|
+
meta = {} if !meta
|
51
|
+
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
def expand_file_by_extension(policy, node)
|
56
|
+
body, meta = process_content(node.props[:entry])
|
57
|
+
meta = {} if !meta
|
58
|
+
|
59
|
+
# TODO: document mapping convention for index pages and collection metadata
|
60
|
+
# TODO: underscore _index pattern?
|
61
|
+
bare_basename = node.props[:entry].basename(node.props[:entry].extname)
|
62
|
+
if bare_basename.to_s == "index"
|
63
|
+
@index_links << {
|
64
|
+
parent_id: @subcollections[node.props[:entry].parent.to_s],
|
65
|
+
index_attrs: meta.merge({ body: body})
|
66
|
+
}
|
67
|
+
else
|
68
|
+
# Create an entity node representing a file mapped to a unique content object
|
69
|
+
entity = graph.create_node do |entity_node|
|
70
|
+
|
71
|
+
entity_slug = node.props[:entry].basename(node.props[:entry].extname).to_s
|
72
|
+
|
73
|
+
entity_attrs = {
|
74
|
+
name: entity_slug,
|
75
|
+
title: entity_slug.gsub("-", " ").capitalize,
|
76
|
+
body: body
|
77
|
+
}
|
78
|
+
|
79
|
+
populate_entity(entity_node, policy, entity_attrs.merge(meta || {}))
|
80
|
+
end
|
81
|
+
|
82
|
+
# We may not have an expanded node for the parent collection if this is a
|
83
|
+
# preorder traversal so save it for later
|
84
|
+
@entity_links << {
|
85
|
+
parent_id: @subcollections[node.props[:entry].parent.to_s],
|
86
|
+
child_id: entity
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
110
90
|
end
|
111
91
|
end
|
112
92
|
end
|
data/lib/yarrow/content/model.rb
CHANGED
@@ -14,8 +14,9 @@ module Yarrow
|
|
14
14
|
|
15
15
|
def expand(graph)
|
16
16
|
@policies.each_value do |policy|
|
17
|
-
strategy =
|
18
|
-
|
17
|
+
#strategy = policy.expansion_strategy.new(graph)
|
18
|
+
traversal = Expansion::Traversal.new(graph, policy)
|
19
|
+
traversal.expand
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
@@ -1,13 +1,11 @@
|
|
1
1
|
module Yarrow
|
2
2
|
module Content
|
3
3
|
class Policy
|
4
|
-
|
5
|
-
|
6
|
-
DEFAULT_EXPANSION = :tree
|
4
|
+
DEFAULT_EXPANSION = :filename_map
|
7
5
|
|
8
6
|
DEFAULT_EXTENSIONS = [".md", ".yml", ".htm"]
|
9
7
|
|
10
|
-
|
8
|
+
DEFAULT_SOURCE_PATH = "."
|
11
9
|
|
12
10
|
MODULE_SEPARATOR = "::"
|
13
11
|
|
@@ -15,15 +13,35 @@ module Yarrow
|
|
15
13
|
def self.from_spec(policy_label, policy_props, module_prefix="")
|
16
14
|
# TODO: validate length, structure etc
|
17
15
|
|
18
|
-
# If the spec holds a symbol value then treat it as
|
16
|
+
# If the spec holds a symbol value then treat it as an entity mapping
|
19
17
|
if policy_props.is_a?(Symbol)
|
20
|
-
new(
|
21
|
-
|
22
|
-
|
18
|
+
new(
|
19
|
+
policy_label,
|
20
|
+
policy_label,
|
21
|
+
policy_props,
|
22
|
+
DEFAULT_EXPANSION,
|
23
|
+
DEFAULT_EXTENSIONS,
|
24
|
+
policy_label.to_s,
|
25
|
+
module_prefix
|
26
|
+
)
|
27
|
+
|
28
|
+
# If the spec holds a string value then treat it as a source path mapping
|
29
|
+
elsif policy_props.is_a?(String)
|
30
|
+
new(
|
31
|
+
policy_label,
|
32
|
+
policy_label,
|
33
|
+
Yarrow::Symbols.to_singular(policy_label),
|
34
|
+
DEFAULT_EXPANSION,
|
35
|
+
DEFAULT_EXTENSIONS,
|
36
|
+
policy_props,
|
37
|
+
module_prefix
|
38
|
+
)
|
39
|
+
|
40
|
+
# Otherwise scan through the spec and fill in any gaps
|
23
41
|
else
|
24
|
-
# Use explicit
|
25
|
-
|
26
|
-
policy_props[:
|
42
|
+
# Use explicit collection name if provided
|
43
|
+
collection = if policy_props.key?(:collection)
|
44
|
+
policy_props[:collection]
|
27
45
|
else
|
28
46
|
# If an entity name is provided use its plural for the container name
|
29
47
|
if policy_props.key?(:entity)
|
@@ -33,12 +51,19 @@ module Yarrow
|
|
33
51
|
end
|
34
52
|
end
|
35
53
|
|
54
|
+
# Use explicit container name if provided
|
55
|
+
container = if policy_props.key?(:container)
|
56
|
+
policy_props[:container]
|
57
|
+
else
|
58
|
+
collection
|
59
|
+
end
|
60
|
+
|
36
61
|
# Use explicit entity name if provided
|
37
62
|
entity = if policy_props.key?(:entity)
|
38
63
|
policy_props[:entity]
|
39
64
|
else
|
40
|
-
if policy_props.key?(:
|
41
|
-
Yarrow::Symbols.to_singular(policy_props[:
|
65
|
+
if policy_props.key?(:collection)
|
66
|
+
Yarrow::Symbols.to_singular(policy_props[:collection])
|
42
67
|
else
|
43
68
|
Yarrow::Symbols.to_singular(policy_label)
|
44
69
|
end
|
@@ -58,22 +83,35 @@ module Yarrow
|
|
58
83
|
DEFAULT_EXTENSIONS
|
59
84
|
end
|
60
85
|
|
61
|
-
#
|
62
|
-
|
86
|
+
# If match path is provided, treat it as a basename
|
87
|
+
source_path = if policy_props.key?(:source_path)
|
88
|
+
policy_props[:source_path]
|
89
|
+
else
|
90
|
+
DEFAULT_SOURCE_PATH
|
91
|
+
end
|
63
92
|
|
64
93
|
# Construct the new policy
|
65
|
-
new(
|
94
|
+
new(
|
95
|
+
container,
|
96
|
+
collection,
|
97
|
+
entity,
|
98
|
+
expansion,
|
99
|
+
extensions,
|
100
|
+
source_path,
|
101
|
+
module_prefix
|
102
|
+
)
|
66
103
|
end
|
67
104
|
end
|
68
105
|
|
69
|
-
attr_reader :container, :entity, :expansion, :extensions, :
|
106
|
+
attr_reader :container, :collection, :entity, :expansion, :extensions, :source_path, :module_prefix
|
70
107
|
|
71
|
-
def initialize(container, entity, expansion, extensions,
|
108
|
+
def initialize(container, collection, entity, expansion, extensions, source_path, module_prefix)
|
72
109
|
@container = container
|
110
|
+
@collection = collection
|
73
111
|
@entity = entity
|
74
112
|
@expansion = expansion
|
75
113
|
@extensions = extensions
|
76
|
-
@
|
114
|
+
@source_path = source_path
|
77
115
|
@module_prefix = module_prefix.split(MODULE_SEPARATOR)
|
78
116
|
end
|
79
117
|
|
@@ -81,12 +119,30 @@ module Yarrow
|
|
81
119
|
@container_const ||= Yarrow::Symbols.to_module_const([*module_prefix, container])
|
82
120
|
end
|
83
121
|
|
84
|
-
|
85
|
-
|
122
|
+
def collection_const
|
123
|
+
begin
|
124
|
+
@collection_const ||= Yarrow::Symbols.to_module_const([*module_prefix, collection])
|
125
|
+
rescue NameError
|
126
|
+
raise NameError, "cannot map undefined entity `#{collection}`"
|
127
|
+
end
|
128
|
+
end
|
86
129
|
|
87
130
|
def entity_const
|
88
131
|
@entity_const ||= Yarrow::Symbols.to_module_const([*module_prefix, entity])
|
89
132
|
end
|
133
|
+
|
134
|
+
def aggregator_const
|
135
|
+
case expansion
|
136
|
+
when :filename_map then Expansion::FilenameMap
|
137
|
+
when :directory_merge then Expansion::DirectoryMerge
|
138
|
+
else
|
139
|
+
raise "No match strategy exists for :#{expansion}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def match_by_extension(candidate)
|
144
|
+
extensions.include?(candidate)
|
145
|
+
end
|
90
146
|
end
|
91
147
|
end
|
92
148
|
end
|
@@ -15,6 +15,7 @@ module Yarrow
|
|
15
15
|
dir.label = :directory
|
16
16
|
dir.props = {
|
17
17
|
name: root_dir_entry.basename.to_s,
|
18
|
+
basename: root_dir_entry.basename.to_s,
|
18
19
|
path: root_dir_entry.to_s,
|
19
20
|
entry: root_dir_entry
|
20
21
|
}
|
@@ -30,6 +31,7 @@ module Yarrow
|
|
30
31
|
root_dir_entry.to_s => root_dir.id
|
31
32
|
}
|
32
33
|
|
34
|
+
# TODO: merge entry and path props
|
33
35
|
Pathname.glob("#{input_dir}/**/**").each do |entry|
|
34
36
|
if entry.directory?
|
35
37
|
content_node = create_node do |dir|
|
@@ -40,6 +42,7 @@ module Yarrow
|
|
40
42
|
# dir.props[:entry] = entry
|
41
43
|
dir.props = {
|
42
44
|
name: entry.basename.to_s,
|
45
|
+
basename: entry.basename.to_s,
|
43
46
|
path: entry.to_s,
|
44
47
|
entry: entry
|
45
48
|
}
|
@@ -56,6 +59,7 @@ module Yarrow
|
|
56
59
|
|
57
60
|
file.props = {
|
58
61
|
name: entry.basename.to_s,
|
62
|
+
basename: entry.basename.sub_ext('').to_s,
|
59
63
|
ext: entry.extname.to_s,
|
60
64
|
path: entry.to_s,
|
61
65
|
entry: entry
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Yarrow
|
2
|
+
module Format
|
3
|
+
class Markdown < ContentType[".md", ".markdown"]
|
4
|
+
include Methods::FrontMatter
|
5
|
+
|
6
|
+
def initialize(source)
|
7
|
+
@source = source.to_s
|
8
|
+
@document = Kramdown::Document.new(@source)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@source
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_dom
|
16
|
+
@document.root
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_html
|
20
|
+
@document.to_html
|
21
|
+
end
|
22
|
+
|
23
|
+
def links
|
24
|
+
@links ||= select_links
|
25
|
+
end
|
26
|
+
|
27
|
+
def title
|
28
|
+
@title ||= select_title
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def select_links
|
34
|
+
stack = to_dom.children
|
35
|
+
hrefs = [] # TODO: distinguish between internal and external
|
36
|
+
|
37
|
+
while !stack.empty?
|
38
|
+
next_el = stack.pop
|
39
|
+
|
40
|
+
if next_el.type == :a
|
41
|
+
hrefs << next_el.attr["href"]
|
42
|
+
else
|
43
|
+
stack.concat(next_el.children) if next_el.children
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
hrefs.reverse
|
48
|
+
end
|
49
|
+
|
50
|
+
def select_title
|
51
|
+
stack = to_dom.children
|
52
|
+
|
53
|
+
while !stack.empty?
|
54
|
+
next_el = stack.pop
|
55
|
+
|
56
|
+
if next_el.type == :header and next_el.options[:level] == 1
|
57
|
+
return next_el.options[:raw_text]
|
58
|
+
else
|
59
|
+
stack.concat(next_el.children) if next_el.children
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
File without changes
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Yarrow
|
2
|
+
module Format
|
3
|
+
module Methods
|
4
|
+
# Utility methods for working with text formats containing frontmatter separators.
|
5
|
+
module FrontMatter
|
6
|
+
module ClassMethods
|
7
|
+
def read(path)
|
8
|
+
text = File.read(path, :encoding => "utf-8")
|
9
|
+
source, metadata = read_yfm(path)
|
10
|
+
Yarrow::Format::Contents.new(new(source), metadata)
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_yfm(path)
|
14
|
+
text = File.read(path, :encoding => 'utf-8')
|
15
|
+
extract_yfm(text, symbolize_keys: true)
|
16
|
+
end
|
17
|
+
|
18
|
+
def extract_yfm(text, options={})
|
19
|
+
pattern = /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
20
|
+
if text =~ pattern
|
21
|
+
content = text.sub(pattern, "")
|
22
|
+
|
23
|
+
begin
|
24
|
+
if options.key?(:symbolize_keys)
|
25
|
+
meta = YAML.load($1, symbolize_names: true)
|
26
|
+
else
|
27
|
+
meta = YAML.load($1)
|
28
|
+
end
|
29
|
+
return [content, meta]
|
30
|
+
rescue Psych::SyntaxError => error
|
31
|
+
if defined? ::Logger
|
32
|
+
# todo: application wide logger
|
33
|
+
#logger = ::Logger.new(STDOUT)
|
34
|
+
#logger.error "#{error.message}"
|
35
|
+
end
|
36
|
+
return [content, nil]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
[text, nil]
|
41
|
+
end
|
42
|
+
|
43
|
+
def write_yfm(name, text, meta)
|
44
|
+
# Symbolized keys are better to deal with when manipulating data in
|
45
|
+
# Ruby but are an interop nightmare when serialized so here we do a
|
46
|
+
# round-trip through JSON encoding to ensure all keys are string
|
47
|
+
# encoded before dumping them to the front matter format.
|
48
|
+
File.write(name, [YAML.dump(meta.to_json), "---", text].join("\n"))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.included(base)
|
53
|
+
base.extend(ClassMethods)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Yarrow
|
2
|
+
module Format
|
3
|
+
module Methods
|
4
|
+
module Metadata
|
5
|
+
module ClassMethods
|
6
|
+
def read(path)
|
7
|
+
text = File.read(path, :encoding => "utf-8")
|
8
|
+
Yarrow::Format::Contents.new(nil, parse(text))
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(text)
|
12
|
+
new(text)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.extend(ClassMethods)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Yarrow
|
2
|
+
module Format
|
3
|
+
class Yaml < ContentType[".yml", ".yaml"]
|
4
|
+
include Methods::Metadata
|
5
|
+
|
6
|
+
def initialize(source)
|
7
|
+
@data = YAML.load(source, symbolize_names: true)
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](key)
|
11
|
+
@data[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
@data
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Yarrow
|
2
|
+
module Format
|
3
|
+
class Contents
|
4
|
+
attr_reader :document, :metadata
|
5
|
+
|
6
|
+
def initialize(document, metadata)
|
7
|
+
@document = document
|
8
|
+
@metadata = metadata
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Registry = {}
|
13
|
+
|
14
|
+
class ContentType
|
15
|
+
def self.[](*extensions)
|
16
|
+
@@extensions_cache = extensions
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.inherited(media_type)
|
21
|
+
return if @@extensions_cache.nil?
|
22
|
+
|
23
|
+
@@extensions_cache.each do |extname|
|
24
|
+
Registry[extname] = media_type
|
25
|
+
end
|
26
|
+
|
27
|
+
@@extensions_cache = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(source)
|
31
|
+
@source = source
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
@source
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Pass in a source path and get back a parsed representation of the
|
40
|
+
# content if it is in a known text format. Mostly used as a fallback if
|
41
|
+
# a custom parser or processing chain is not configured for a content
|
42
|
+
# type.
|
43
|
+
def self.read(name)
|
44
|
+
path = if name.is_a?(Pathname)
|
45
|
+
name
|
46
|
+
else
|
47
|
+
Pathname.new(name)
|
48
|
+
end
|
49
|
+
|
50
|
+
# case path.extname
|
51
|
+
# when '.htm', '.md', '.txt', '.yfm'
|
52
|
+
# Markdown.read(path)
|
53
|
+
# when '.yml'
|
54
|
+
# [nil, YAML.load(File.read(path.to_s), symbolize_names: true)]
|
55
|
+
# when '.json'
|
56
|
+
# [nil, JSON.parse(File.read(path.to_s))]
|
57
|
+
# end
|
58
|
+
|
59
|
+
unless Registry.key?(path.extname)
|
60
|
+
raise "Unsupported format: #{path.extname} (#{path})"
|
61
|
+
end
|
62
|
+
|
63
|
+
Registry[path.extname].read(path)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
require_relative "format/methods/front_matter"
|
69
|
+
require_relative "format/methods/metadata"
|
70
|
+
require_relative "format/markdown"
|
71
|
+
require_relative "format/yaml"
|