yarrow 0.8.7 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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