yarrow 0.7.1 → 0.7.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95557525739422605b678c010cad87b67b3e01e1382a9a7b65b032cd7ccfdceb
4
- data.tar.gz: 90bd1dfc26fc41c8aee75586f760b05d0b122fe1868cb061b29a7344a0627165
3
+ metadata.gz: 0c49c3ae295fd127f7533a0af4c543d5315a2929a580a21b6b4c26379e519490
4
+ data.tar.gz: 6867e3004c89dc7362a83378f3b0d55a14803d513cc77445c7893a31ffb4e4bd
5
5
  SHA512:
6
- metadata.gz: cb128a24a28ad30bef33b1db635e952a3c4930d75094ce5434fe3cbeb890644fb1704a4d619144a915b0e71cc41c333d8ce3186fb2b8d5e3eee334bb68aa8b9d
7
- data.tar.gz: ee6767873b4afa6270b7a8e21dc096eac492624d47063e7d742d81cdc3e5b2608595f8ce945f299e036cb19245b980df56322a768d225731f16275088bd54dd3
6
+ metadata.gz: '0056694b64f9ac4d000e85341570b4a5b630f27b5de15d5dcf12eeae1dc3625368abfd1d65a6c0a26f9793cbd6993e076c7e46d76ef97007874e8dddfd75c7af'
7
+ data.tar.gz: fa5469997f6e674f3f2b1daf8bbc525637b2cbb2db7b9a8a1d494c85b1ce4d2725753ece2a2d114a905b994fe96f99e9db09178fa6f92483d4ee5d6208a500ad
@@ -10,7 +10,7 @@ jobs:
10
10
  fail-fast: false
11
11
  matrix:
12
12
  os: [ubuntu-latest, macos-latest]
13
- ruby: [2.7, '3.0', jruby-head]
13
+ ruby: [2.7, '3.0', 3.1, head]
14
14
  runs-on: ${{ matrix.os }}
15
15
  steps:
16
16
  - uses: actions/checkout@v2
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ Gemfile.lock
6
6
  yarrow-*.gem
7
7
  coverage
8
8
  bin/scripts
9
+ .DS_Store
data/lib/yarrow/config.rb CHANGED
@@ -42,19 +42,21 @@ module Yarrow
42
42
  :root_dir
43
43
  )
44
44
 
45
- # Top level root config namespace. Source, content and output are directory
46
- # paths and should be the only required defaults for a complete batch run.
45
+ # Top level root config namespace.
46
+ #
47
+ # `content_dir` and `output_dir` are placeholders and should be overriden
48
+ # with more fine-grained config for web and book outputs in future.
47
49
  #
48
50
  # Additional server config is optional and only needed if running the dev
49
51
  # server locally.
50
52
  #
51
53
  # TODO: meta should be union of Type::Optional and Config::Meta
52
54
  Instance = Yarrow::Schema::Value.new(
53
- source: Pathname,
54
- content: Pathname,
55
- output_dir: Pathname,
56
- meta: Yarrow::Schema::Type::Any,
57
- server: Yarrow::Schema::Type::Any
55
+ project_dir: :path,
56
+ content_dir: :path,
57
+ output_dir: :path,
58
+ meta: :any,
59
+ server: :any
58
60
  )
59
61
  end
60
62
  end
@@ -52,13 +52,13 @@ module Yarrow
52
52
  # automated as part of the schema types or a default value should be
53
53
  # generated here (eg: `"#{Dir.pwd}/docs"`)
54
54
  out_dir_or_string = config[:output_dir] || ""
55
- source_dir_or_string = config[:source] || ""
56
- content_dir_or_string = config[:content] || ""
55
+ source_dir_or_string = config[:project_dir] || ""
56
+ content_dir_or_string = config[:content_dir] || ""
57
57
 
58
58
  Yarrow::Config::Instance.new(
59
59
  output_dir: Pathname.new(File.expand_path(out_dir_or_string)),
60
- source: Pathname.new(File.expand_path(source_dir_or_string)),
61
- content: Pathname.new(File.expand_path(content_dir_or_string)),
60
+ project_dir: Pathname.new(File.expand_path(source_dir_or_string)),
61
+ content_dir: Pathname.new(File.expand_path(content_dir_or_string)),
62
62
  meta: meta_obj,
63
63
  server: server_obj
64
64
  )
@@ -1,5 +1,4 @@
1
1
  module Yarrow
2
-
3
2
  class ConsoleRunner
4
3
 
5
4
  SUCCESS = 0
@@ -100,7 +99,6 @@ module Yarrow
100
99
  end
101
100
 
102
101
  def normalize_theme_path
103
-
104
102
  # noop
105
103
  end
106
104
 
@@ -139,12 +137,11 @@ module Yarrow
139
137
  @options.has_key? option
140
138
  end
141
139
 
142
- def run_input_process
143
- # noop
144
- end
145
-
146
- def run_output_process
147
- # noop
140
+ def run_generation_process
141
+ generator = Generator.new(@config)
142
+ generator.process do |manifest|
143
+ p manifest
144
+ end
148
145
  end
149
146
 
150
147
  def print_header
@@ -0,0 +1,16 @@
1
+ module Yarrow
2
+ module Content
3
+ class Expansion
4
+ def initialize(model)
5
+ @model = model
6
+ end
7
+
8
+ def expand(graph)
9
+ @model.each_policy do |policy|
10
+ strategy = TreeExpansion.new(graph)
11
+ strategy.expand(policy)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,51 @@
1
+ module Yarrow
2
+ module Content
3
+ class ExpansionStrategy
4
+ include Yarrow::Tools::FrontMatter
5
+
6
+ attr_reader :graph
7
+
8
+ def initialize(graph)
9
+ @graph = graph
10
+ end
11
+
12
+ # Extract collection level configuration/metadata from the root node for
13
+ # this content type.
14
+ def extract_metadata(node, type)
15
+ # TODO: support _index or _slug convention as well
16
+ meta_file = node.out(slug: type.to_s).first
17
+
18
+ if meta_file
19
+ # Process metadata and add it to the collection node
20
+ # TODO: pass in content converter object
21
+ # TODO: index/body content by default if extracted from frontmatter
22
+ body, data = process_content(meta_file.props[:entry])
23
+ else
24
+ # Otherwise, assume default collection behaviour
25
+ data = {}
26
+ end
27
+
28
+ # Generate a default title if not provided in metadata
29
+ unless data.key?(:title)
30
+ data[:title] = type.to_s.capitalize
31
+ end
32
+
33
+ data
34
+ end
35
+
36
+ # Workaround for handling meta and content source in multiple files or a single
37
+ # file with front matter.
38
+ def process_content(path)
39
+ case path.extname
40
+ when '.htm', '.md'
41
+ read_split_content(path.to_s, symbolize_keys: true)
42
+ # when '.md'
43
+ # body, data = read_split_content(path.to_s, symbolize_keys: true)
44
+ # [Kramdown::Document.new(body).to_html, data]
45
+ when '.yml'
46
+ [nil, YAML.load(File.read(path.to_s), symbolize_names: true)]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,9 +2,12 @@ module Yarrow
2
2
  module Content
3
3
  # A directed graph of every element of content in the project.
4
4
  class Graph
5
- # Construct a graph collected from source content files.
5
+ # Construct a graph collected from files and directories in the configured
6
+ # content directory.
7
+ #
8
+ # @return [Yarrow::Content::Graph]
6
9
  def self.from_source(config)
7
- new(SourceCollector.collect(config.source), config)
10
+ new(Source.collect(config.content_dir), config)
8
11
  end
9
12
 
10
13
  attr_reader :graph, :config
@@ -15,7 +18,7 @@ module Yarrow
15
18
  end
16
19
 
17
20
  def expand_pages
18
- expander = Yarrow::Content::CollectionExpander.new
21
+ expander = Yarrow::Content::Expansion.new(Yarrow::Content::Model.new)
19
22
  expander.expand(graph)
20
23
  end
21
24
 
@@ -28,6 +31,16 @@ module Yarrow
28
31
  def directories
29
32
  graph.nodes(:directory)
30
33
  end
34
+
35
+ # List of mapped content object collections
36
+ def collections
37
+ graph.nodes(:collection)
38
+ end
39
+
40
+ # List of mapped content object items
41
+ def items
42
+ graph.nodes(:item)
43
+ end
31
44
  end
32
45
  end
33
46
  end
@@ -0,0 +1,61 @@
1
+ module Yarrow
2
+ module Content
3
+ class Manifest
4
+ def self.build(graph)
5
+ manifest = new
6
+
7
+ graph.n(:collection).each do |collection|
8
+
9
+ unless collection.props[:content_only]
10
+ manifest.add_document(collection_context(collection))
11
+ end
12
+
13
+ unless collection.props[:index_only]
14
+ collection.out(:item).each do |item|
15
+ #if item[:entity].status.to_sym == :published
16
+ manifest.add_document(item_context(item))
17
+ #end
18
+ end
19
+ end
20
+ end
21
+
22
+ manifest
23
+ end
24
+
25
+ attr_reader :documents, :assets
26
+
27
+ def initialize
28
+ @documents = []
29
+ @assets = []
30
+ end
31
+
32
+ def self.collection_context(collection)
33
+ Yarrow::Output::Context.new(
34
+ parent: collection.in(:collection).first,
35
+ name: collection.props[:name],
36
+ #url: collection.props[:url],
37
+ title: collection.props[:title],
38
+ type: collection.props[:type]
39
+ )
40
+ end
41
+
42
+ def self.item_context(item)
43
+ Yarrow::Output::Context.new(
44
+ parent: item.in(:collection).first,
45
+ name: item.props[:name],
46
+ #url: item.props[:url],
47
+ title: item.props[:title],
48
+ type: item.props[:type]
49
+ )
50
+ end
51
+
52
+ def add_document(document)
53
+ @documents << document
54
+ end
55
+
56
+ def add_asset(asset)
57
+ @assets << asset
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,46 @@
1
+ module Yarrow
2
+ module Content
3
+ ContentSpec = Yarrow::Schema::Value.new(:namespace, :model) do
4
+ def to_world
5
+ "world"
6
+ end
7
+ end
8
+
9
+ ContentPolicy = Yarrow::Schema::Value.new(
10
+ :dir,
11
+ :file,
12
+ :expansion,
13
+ :container,
14
+ :record
15
+ )
16
+
17
+ class Model
18
+ def initialize(spec=nil, namespace=nil)
19
+ @namespace = []
20
+ @namespace << namespace unless namespace.nil?
21
+
22
+ @policies = if spec.nil?
23
+ spec = {
24
+ root: ContentPolicy.new(
25
+ expansion: :tree,
26
+ dir: "*",
27
+ file: "*.md",
28
+ :container => :pages,
29
+ :record => :page
30
+ )
31
+ }
32
+ else
33
+ spec.model
34
+ end
35
+ end
36
+
37
+ def each_policy(&block)
38
+ @policies.each_value(&block)
39
+ end
40
+
41
+ def policy_for(policy_key)
42
+ @policies[policy_key]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ module Yarrow
2
+ module Content
3
+ class Policy
4
+ Options = Yarrow::Schema::Value.new(
5
+ :container,
6
+ :entity,
7
+ :extensions,
8
+ :match_path
9
+ )
10
+
11
+ DEFAULT_EXTENSIONS = [".md", ".yml", ".htm"]
12
+
13
+ DEFAULT_MATCH_PATH = "."
14
+
15
+ def self.from_name(name)
16
+ new(Options.new(container: name.to_sym))
17
+ end
18
+
19
+ def initialize(properties)
20
+ unless properties.respond_to?(:container) || properties.respond_to?(:entity)
21
+ raise "Must provide a container name or entity name"
22
+ end
23
+
24
+ @properties = properties
25
+ end
26
+
27
+ def container
28
+ return @properties.container if @properties.container
29
+ Yarrow::Symbols.to_plural(@properties.entity)
30
+ end
31
+
32
+ def entity
33
+ return @properties.entity if @properties.entity
34
+ Yarrow::Symbols.to_singular(@properties.container)
35
+ end
36
+
37
+ def extensions
38
+ return @properties.extensions if @properties.extensions
39
+ DEFAULT_EXTENSIONS
40
+ end
41
+
42
+ def match_path
43
+ return @properties.match_path if @properties.match_path
44
+ DEFAULT_MATCH_PATH
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ module Yarrow
2
+ module Content
3
+ class Resource < Schema::Entity
4
+ attribute :id, :string
5
+ attribute :name, :string
6
+ attribute :title, :string
7
+ attribute :url, :string
8
+ attribute :content, :string
9
+
10
+ def self.from_frontmatter_url(data, url_field)
11
+ new({
12
+ id: data[:id],
13
+ name: data[:name],
14
+ title: data[:title],
15
+ content: data[:content],
16
+ url: data[url_field]
17
+ })
18
+ end
19
+
20
+ def self.from_template_url(data, template, &block)
21
+
22
+ end
23
+
24
+ def self.from_source_path(data, &block)
25
+
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,10 +1,77 @@
1
1
  module Yarrow
2
2
  module Content
3
+ # Collects a digraph of all directories and files underneath the given input
4
+ # directory.
3
5
  class Source
4
- attr_reader :input_dir
6
+ def self.collect(input_dir)
7
+ Mementus::Graph.new(is_mutable: true) do
8
+ root = create_node do |root|
9
+ root.label = :root
10
+ end
5
11
 
6
- def initialize(config)
7
- @input_dir = config[:input_dir]
12
+ root_dir_entry = Pathname.new(input_dir)
13
+
14
+ root_dir = create_node do |dir|
15
+ dir.label = :directory
16
+ dir.props = {
17
+ name: root_dir_entry.basename.to_s,
18
+ path: root_dir_entry.to_s,
19
+ entry: root_dir_entry
20
+ }
21
+ end
22
+
23
+ create_edge do |child|
24
+ child.label = :child
25
+ child.from = root.id
26
+ child.to = root_dir.id
27
+ end
28
+
29
+ directories = {
30
+ root_dir_entry.to_s => root_dir.id
31
+ }
32
+
33
+ Pathname.glob("#{input_dir}/**/**").each do |entry|
34
+ if entry.directory?
35
+ content_node = create_node do |dir|
36
+ dir.label = :directory
37
+ # dir.props[:name] = entry.basename.to_s
38
+ # dir.props[:slug] = entry.basename.to_s
39
+ # dir.props[:path] = entry.to_s
40
+ # dir.props[:entry] = entry
41
+ dir.props = {
42
+ name: entry.basename.to_s,
43
+ path: entry.to_s,
44
+ entry: entry
45
+ }
46
+ end
47
+
48
+ directories[entry.to_s] = content_node.id
49
+ else
50
+ content_node = create_node do |file|
51
+ file.label = :file
52
+ # file.props[:name] = entry.basename.to_s
53
+ # file.props[:slug] = entry.basename.sub_ext('').to_s
54
+ # file.props[:path] = entry.to_s
55
+ # file.props[:entry] = entry
56
+
57
+ file.props = {
58
+ name: entry.basename.to_s,
59
+ ext: entry.extname.to_s,
60
+ path: entry.to_s,
61
+ entry: entry
62
+ }
63
+ end
64
+ end
65
+
66
+ if directories.key?(entry.dirname.to_s)
67
+ create_edge do |edge|
68
+ edge.label = :child
69
+ edge.from = directories[entry.dirname.to_s]
70
+ edge.to = content_node
71
+ end
72
+ end
73
+ end
74
+ end
8
75
  end
9
76
  end
10
77
  end
@@ -0,0 +1,92 @@
1
+ module Yarrow
2
+ module Content
3
+ class TreeExpansion < ExpansionStrategy
4
+ def expand(policy)
5
+ #p graph.n(:root).out(:directory).to_a.count
6
+ #policy.match()
7
+
8
+ #p graph.n(:root).out(:directory).first.props[:name]
9
+
10
+ expand_impl(policy)
11
+ end
12
+
13
+ def expand_impl(policy)
14
+ type = policy.container
15
+
16
+ # If match path represents entire content dir, then include the entire
17
+ # content dir instead of scanning from a subfolder matching the name of
18
+ # the collection.
19
+ #start_node = if policy.match_path == "."
20
+ start_node = if true
21
+ graph.n(:root)
22
+ else
23
+ graph.n(:root).out(name: type.to_s)
24
+ end
25
+
26
+ # Extract metadata from given start node
27
+ collection_metadata = extract_metadata(start_node, type)
28
+
29
+ # Collect all nested collections in the subgraph for this content type
30
+ subcollections = {}
31
+ item_links = []
32
+ index = nil
33
+
34
+ # Scan and collect all nested files from the root
35
+ start_node.depth_first.each do |node|
36
+ if node.label == :directory
37
+ # Create a collection node representing a collection of documents
38
+ index = graph.create_node do |collection_node|
39
+ collection_node.label = :collection
40
+ collection_node.props[:type] = type
41
+ collection_node.props[:name] = node.props[:name]
42
+
43
+ # TODO: title needs to be defined from metadata
44
+ collection_node.props[:title] = node.props[:name].capitalize
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
58
+ elsif node.label == :file
59
+ body, meta = process_content(node.props[:entry])
60
+
61
+ # Create an item node representing a file mapped to a unique content object
62
+ item = graph.create_node do |item_node|
63
+ item_node.label = :item
64
+ item_node.props[:type] = policy.record
65
+ item_node.props[:name] = node.props[:entry].basename(node.props[:entry].extname).to_s
66
+ item_node.props[:body] = body if body
67
+ item_node.props[:title] = meta[:title] if meta
68
+ # TODO: better handling of metadata on node props
69
+ end
70
+
71
+ # We may not have an expanded node for the parent collection if this is a
72
+ # preorder traversal so save it for later
73
+ item_links << {
74
+ parent_path: node.props[:entry].parent.to_s,
75
+ item_id: item.id
76
+ }
77
+ end
78
+ end
79
+
80
+ # Once all files and directories have been expanded, connect all the child
81
+ # edges between collections and items
82
+ item_links.each do |item_link|
83
+ graph.create_edge do |edge|
84
+ edge.label = :child
85
+ edge.from = subcollections[item_link[:parent_path]].id
86
+ edge.to = item_link[:item_id]
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,27 +1,57 @@
1
1
  module Yarrow
2
+ class ScanSource < Process::StepProcessor
3
+ accepts Config::Instance
4
+ provides Content::Graph
5
+
6
+ def step(config)
7
+ Yarrow::Content::Graph.from_source(config)
8
+ end
9
+ end
10
+
11
+ class ExpandCollections < Process::StepProcessor
12
+ accepts Content::Graph
13
+ provides Content::Graph
14
+
15
+ def step(content)
16
+ expander = Content::Expansion.new(Yarrow::Content::Model.new)
17
+ expander.expand(content.graph)
18
+ content
19
+ end
20
+ end
21
+
22
+ class FlattenManifest < Process::StepProcessor
23
+ accepts Content::Graph
24
+ provides Content::Manifest
25
+
26
+ def step(content)
27
+ Content::Manifest.build(content.graph)
28
+ end
29
+ end
30
+
31
+ class BuildOutput < Process::StepProcessor
32
+ accepts Content::Manifest
33
+ provides Output::Result
34
+ end
2
35
 
3
36
  # Generates documentation from a model.
4
- #
5
- # Subclasses of Generator need to override the template methods,
6
- # to specify a particular file structure to output.
7
37
  class Generator
8
-
9
- def initialize(target, site_tree)
10
- ensure_dir_exists! target
11
- @target = target
12
- @site_tree = site_tree
38
+ def initialize(config)
39
+ @config = config
40
+ @workflow = Process::Workflow.new(config)
13
41
  end
14
-
15
- def ensure_dir_exists!(target)
16
- unless File.directory? target
17
- Dir.mkdir target
42
+
43
+ def process(&block)
44
+ workflow.connect(ScanSource.new)
45
+ workflow.connect(ExpandCollections.new)
46
+ workflow.connect(FlattenManifest.new)
47
+
48
+ workflow.process do |result|
49
+ block.call(result)
18
50
  end
19
51
  end
20
-
21
- def build_docs
22
-
23
- end
24
-
25
- end
26
52
 
27
- end
53
+ private
54
+
55
+ attr_reader :config, :workflow
56
+ end
57
+ end
@@ -0,0 +1,13 @@
1
+ module Yarrow
2
+ module Output
3
+ class Log
4
+
5
+ end
6
+
7
+ class Result
8
+ def initialize
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -27,12 +27,18 @@ module Yarrow
27
27
 
28
28
  target_path = Pathname.new("#{docroot}#{path}")
29
29
 
30
- FileUtils.mkdir_p(target_path.dirname)
30
+ ensure_dir_exists!(target_path.dirname)
31
31
 
32
32
  File.open(target_path.to_s, WRITE_MODE) do |file|
33
33
  file.puts(content)
34
34
  end
35
35
  end
36
+
37
+ def ensure_dir_exists!(target)
38
+ unless File.directory?(target)
39
+ FileUtils.mkdir_p(target)
40
+ end
41
+ end
36
42
  end
37
43
  end
38
44
  end