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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/CONTENT.md +228 -0
  4. data/README.md +8 -8
  5. data/lib/extensions/mementus.rb +29 -0
  6. data/lib/yarrow/config.rb +10 -0
  7. data/lib/yarrow/content/expansion/aggregator.rb +88 -0
  8. data/lib/yarrow/content/expansion/basename_merge.rb +44 -0
  9. data/lib/yarrow/content/expansion/directory_map.rb +21 -0
  10. data/lib/yarrow/content/expansion/directory_merge.rb +31 -0
  11. data/lib/yarrow/content/expansion/file_list.rb +15 -0
  12. data/lib/yarrow/content/expansion/filename_map.rb +24 -0
  13. data/lib/yarrow/content/expansion/strategy.rb +47 -0
  14. data/lib/yarrow/content/expansion/traversal.rb +67 -0
  15. data/lib/yarrow/content/expansion/tree.rb +53 -73
  16. data/lib/yarrow/content/model.rb +3 -2
  17. data/lib/yarrow/content/policy.rb +77 -21
  18. data/lib/yarrow/content/source.rb +4 -0
  19. data/lib/yarrow/format/asciidoc.rb +0 -0
  20. data/lib/yarrow/format/html.rb +0 -0
  21. data/lib/yarrow/format/json.rb +0 -0
  22. data/lib/yarrow/format/markdown.rb +67 -0
  23. data/lib/yarrow/format/mediawiki.rb +0 -0
  24. data/lib/yarrow/format/methods/front_matter.rb +58 -0
  25. data/lib/yarrow/format/methods/metadata.rb +22 -0
  26. data/lib/yarrow/format/orgmode.rb +0 -0
  27. data/lib/yarrow/format/text.rb +0 -0
  28. data/lib/yarrow/format/xml.rb +0 -0
  29. data/lib/yarrow/format/yaml.rb +19 -0
  30. data/lib/yarrow/format.rb +71 -0
  31. data/lib/yarrow/schema/types.rb +41 -0
  32. data/lib/yarrow/version.rb +1 -1
  33. data/lib/yarrow/web/manifest.rb +15 -9
  34. data/lib/yarrow.rb +6 -9
  35. data/yarrow.gemspec +1 -0
  36. metadata +22 -3
  37. 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 true
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(:root).out(name: policy.container.to_s)
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
- # Create a collection node representing a collection of documents
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
- body, meta = process_content(node.props[:entry])
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
@@ -14,8 +14,9 @@ module Yarrow
14
14
 
15
15
  def expand(graph)
16
16
  @policies.each_value do |policy|
17
- strategy = Expansion::Tree.new(graph)
18
- strategy.expand(policy)
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
- DEFAULT_HOME_NESTING = false
5
-
6
- DEFAULT_EXPANSION = :tree
4
+ DEFAULT_EXPANSION = :filename_map
7
5
 
8
6
  DEFAULT_EXTENSIONS = [".md", ".yml", ".htm"]
9
7
 
10
- DEFAULT_MATCH_PATH = "."
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 a container => entity mapping
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(policy_label, policy_props, DEFAULT_EXPANSION, DEFAULT_EXTENSIONS, DEFAULT_MATCH_PATH, module_prefix)
21
-
22
- # Otherwise scan through all the props and fill in any gaps
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 container name if provided
25
- container = if policy_props.key?(:container)
26
- policy_props[:container]
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?(:container)
41
- Yarrow::Symbols.to_singular(policy_props[:container])
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
- # TODO: handle this in expansion strategies
62
- match_path = DEFAULT_MATCH_PATH
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(container, entity, expansion, extensions, match_path, module_prefix)
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, :match_path, :module_prefix
106
+ attr_reader :container, :collection, :entity, :expansion, :extensions, :source_path, :module_prefix
70
107
 
71
- def initialize(container, entity, expansion, extensions, match_path, module_prefix)
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
- @match_path = match_path
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
- alias_method :collection, :container
85
- alias_method :collection_const, :container_const
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"