yarrow 0.8.7 → 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.
@@ -3,99 +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
- p policy
9
-
10
- #p graph.n(:root).out(:directory).first.props[:name]
11
- type = policy.container
12
-
13
6
  # If match path represents entire content dir, then include the entire
14
7
  # content dir instead of scanning from a subfolder matching the name of
15
8
  # the collection.
16
9
  #start_node = if policy.match_path == "."
17
- start_node = if true
10
+ start_node = if policy.match_path == "."
18
11
  # TODO: match against source_dir
19
12
  graph.n(:root).out(:directory)
20
13
  else
21
- graph.n(:root).out(name: policy.container.to_s)
14
+ graph.n(name: policy.match_path)
22
15
  end
23
16
 
24
- # Extract metadata from given start node
25
- #collection_metadata = extract_metadata(start_node, policy.container)
26
-
27
17
  # Collect all nested collections in the subgraph for this content type
28
- subcollections = {}
29
- entity_links = []
30
- index_links = []
31
- index = nil
18
+ @subcollections = {}
19
+ @entity_links = []
20
+ @index_links = []
21
+ @index = nil
32
22
 
33
23
  # Scan and collect all nested files from the root
34
24
  start_node.depth_first.each do |node|
35
25
  if node.label == :directory
36
- # Create a collection node representing a collection of documents
37
- index = graph.create_node do |collection_node|
38
-
39
- collection_attrs = {
40
- name: node.props[:name],
41
- title: node.props[:name].capitalize,
42
- body: ""
43
- }
44
-
45
- populate_collection(collection_node, policy, collection_attrs)
46
- end
47
-
48
- # Add this collection id to the lookup table for edge construction
49
- subcollections[node.props[:path]] = index
50
-
51
- # Join the collection to its parent
52
- unless node.props[:slug] == type.to_s || !subcollections.key?(node.props[:entry].parent.to_s)
53
- graph.create_edge do |edge|
54
- edge.label = :child
55
- edge.from = subcollections[node.props[:entry].parent.to_s].id
56
- edge.to = index.id
57
- end
58
- end
26
+ expand_directory(policy, node)
59
27
  elsif node.label == :file
60
- body, meta = process_content(node.props[:entry])
61
- meta = {} if !meta
62
-
63
- # TODO: document mapping convention for index pages and collection metadata
64
- # TODO: underscore _index pattern?
65
- bare_basename = node.props[:entry].basename(node.props[:entry].extname)
66
- if bare_basename.to_s == "index"
67
- index_links << {
68
- parent_id: subcollections[node.props[:entry].parent.to_s],
69
- index_attrs: meta.merge({ body: body})
70
- }
71
- else
72
- # Create an entity node representing a file mapped to a unique content object
73
- entity = graph.create_node do |entity_node|
74
-
75
- entity_slug = node.props[:entry].basename(node.props[:entry].extname).to_s
76
-
77
- entity_attrs = {
78
- name: entity_slug,
79
- title: entity_slug.gsub("-", " ").capitalize,
80
- body: body
81
- }
82
-
83
- populate_entity(entity_node, policy, entity_attrs.merge(meta || {}))
84
- end
85
-
86
- # We may not have an expanded node for the parent collection if this is a
87
- # preorder traversal so save it for later
88
- entity_links << {
89
- parent_id: subcollections[node.props[:entry].parent.to_s],
90
- child_id: entity
91
- }
92
- end
28
+ expand_file_by_extension(policy, node)
93
29
  end
94
30
  end
95
31
 
96
32
  # Once all files and directories have been expanded, connect all the child
97
33
  # edges between collections and entities
98
- entity_links.each do |entity_link|
34
+ @entity_links.each do |entity_link|
99
35
  graph.create_edge do |edge|
100
36
  edge.label = :child
101
37
  edge.from = entity_link[:parent_id].id
@@ -104,10 +40,53 @@ module Yarrow
104
40
  end
105
41
 
106
42
  # Merge index page body and metadata with their parent collections
107
- index_links.each do |index_link|
43
+ @index_links.each do |index_link|
108
44
  merge_collection_index(index_link[:parent_id], policy, index_link[:index_attrs])
109
45
  end
110
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
111
90
  end
112
91
  end
113
92
  end
@@ -2,8 +2,6 @@ module Yarrow
2
2
  module Content
3
3
  class Model
4
4
  def initialize(content_config)
5
- p content_config
6
-
7
5
  @policies = {}
8
6
  content_config.source_map.each_entry do |policy_label, policy_spec|
9
7
  @policies[policy_label] = Policy.from_spec(
@@ -16,8 +14,9 @@ module Yarrow
16
14
 
17
15
  def expand(graph)
18
16
  @policies.each_value do |policy|
19
- strategy = Expansion::Tree.new(graph)
20
- strategy.expand(policy)
17
+ #strategy = policy.expansion_strategy.new(graph)
18
+ traversal = Expansion::Traversal.new(graph, policy)
19
+ traversal.expand
21
20
  end
22
21
  end
23
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module Yarrow
3
3
  APP_NAME = "Yarrow"
4
- VERSION = "0.8.7"
4
+ VERSION = "0.9.0"
5
5
  end