yarrow 0.5.0 → 0.6.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +23 -0
  3. data/.gitignore +2 -1
  4. data/README.md +13 -19
  5. data/Rakefile +15 -1
  6. data/lib/yarrow.rb +17 -6
  7. data/lib/yarrow/config.rb +59 -0
  8. data/lib/yarrow/configuration.rb +35 -63
  9. data/lib/yarrow/content/collection_expander.rb +218 -0
  10. data/lib/yarrow/content/content_type.rb +42 -0
  11. data/lib/yarrow/content/graph.rb +13 -21
  12. data/lib/yarrow/content/source.rb +11 -0
  13. data/lib/yarrow/extensions.rb +1 -0
  14. data/lib/yarrow/extensions/mementus.rb +24 -0
  15. data/lib/yarrow/output/context.rb +0 -4
  16. data/lib/yarrow/output/generator.rb +2 -2
  17. data/lib/yarrow/output/web/indexed_file.rb +39 -0
  18. data/lib/yarrow/process/expand_content.rb +12 -0
  19. data/lib/yarrow/process/extract_source.rb +12 -0
  20. data/lib/yarrow/process/project_manifest.rb +20 -0
  21. data/lib/yarrow/process/step_processor.rb +43 -0
  22. data/lib/yarrow/process/workflow.rb +36 -0
  23. data/lib/yarrow/schema.rb +132 -0
  24. data/lib/yarrow/schema/validations/array.rb +0 -0
  25. data/lib/yarrow/schema/validations/object.rb +0 -0
  26. data/lib/yarrow/schema/validations/string.rb +0 -0
  27. data/lib/yarrow/server.rb +8 -5
  28. data/lib/yarrow/source/graph.rb +6 -0
  29. data/lib/yarrow/symbols.rb +19 -0
  30. data/lib/yarrow/tools/content_utils.rb +74 -0
  31. data/lib/yarrow/tools/front_matter.rb +4 -2
  32. data/lib/yarrow/version.rb +3 -2
  33. data/lib/yarrow/web/html_document.rb +9 -0
  34. data/lib/yarrow/web/manifest.rb +9 -0
  35. data/lib/yarrow/web/static_asset.rb +9 -0
  36. data/lib/yarrow/web/template.rb +9 -0
  37. data/yarrow.gemspec +6 -7
  38. metadata +52 -48
  39. data/.travis.yml +0 -16
  40. data/lib/yarrow/html.rb +0 -1
  41. data/lib/yarrow/html/asset_tags.rb +0 -59
  42. data/lib/yarrow/html/content_tags.rb +0 -7
  43. data/lib/yarrow/tools/output_file.rb +0 -40
@@ -0,0 +1,42 @@
1
+ gem "strings-inflection"
2
+
3
+ module Yarrow
4
+ module Content
5
+ class ContentType
6
+ Value = Yarrow::Schema::Value.new(:collection, :entity, :extensions)
7
+
8
+ DEFAULT_EXTENSIONS = [".md", ".yml", ".htm"]
9
+
10
+ def self.from_name(name)
11
+ new(Value.new(collection: name.to_sym))
12
+ end
13
+
14
+ def initialize(properties)
15
+ unless properties.respond_to?(:collection) || properties.respond_to?(:entity)
16
+ raise "Must provide a collection name or entity name"
17
+ end
18
+
19
+ @properties = properties
20
+ end
21
+
22
+ def collection
23
+ return @properties.collection if @properties.collection
24
+ Yarrow::Symbols.to_plural(@properties.entity)
25
+ end
26
+
27
+ def entity
28
+ return @properties.entity if @properties.entity
29
+ Yarrow::Symbols.to_singular(@properties.collection)
30
+ end
31
+
32
+ def extensions
33
+ return @properties.extensions if @properties.extensions
34
+ DEFAULT_EXTENSIONS
35
+ end
36
+
37
+ def match_path
38
+ "."
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,22 +1,22 @@
1
- require "mementus"
2
-
3
- module Mementus
4
- module Pipeline
5
- class Step
6
- # Monkeypatch extension to ensure each pipeline step supports enumerable
7
- # methods. Mostly used for #map. API needs to be fixed in the gem itself.
8
- include Enumerable
9
- end
10
- end
11
- end
12
-
13
1
  module Yarrow
14
2
  module Content
15
3
  # A directed graph of every element of content in the project.
16
4
  class Graph
17
5
  # Construct a graph collected from source content files.
18
6
  def self.from_source(config)
19
- new(SourceCollector.collect(config.input_dir))
7
+ new(SourceCollector.collect(config.source), config)
8
+ end
9
+
10
+ attr_reader :graph, :config
11
+
12
+ def initialize(graph, config)
13
+ @graph = graph
14
+ @config = config
15
+ end
16
+
17
+ def expand_pages
18
+ expander = Yarrow::Content::CollectionExpander.new
19
+ expander.expand(graph)
20
20
  end
21
21
 
22
22
  # List of source files.
@@ -28,14 +28,6 @@ module Yarrow
28
28
  def directories
29
29
  graph.nodes(:directory)
30
30
  end
31
-
32
- private
33
-
34
- def initialize(graph)
35
- @graph = graph
36
- end
37
-
38
- attr_reader :graph
39
31
  end
40
32
  end
41
33
  end
@@ -0,0 +1,11 @@
1
+ module Yarrow
2
+ module Content
3
+ class Source
4
+ attr_reader :input_dir
5
+
6
+ def initialize(config)
7
+ @input_dir = config[:input_dir]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1 @@
1
+ require "yarrow/extensions/mementus"
@@ -0,0 +1,24 @@
1
+ require "mementus"
2
+
3
+ module Mementus
4
+ module Pipeline
5
+ class Step
6
+ # Monkeypatch extension to ensure each pipeline step supports enumerable
7
+ # methods. Mostly used for #map. API needs to be fixed in the gem itself.
8
+ include Enumerable
9
+ end
10
+ end
11
+ module Structure
12
+ class IncidenceList
13
+ def inspect
14
+ "<Mementus::Structure::IncidenceList>"
15
+ end
16
+ end
17
+ end
18
+ class Graph
19
+ def inspect
20
+ "<Mementus::Graph @structure=#{@structure.inspect} " +
21
+ "nodes_count=#{nodes_count} edges_count=#{edges_count}>"
22
+ end
23
+ end
24
+ end
@@ -4,11 +4,7 @@ module Yarrow
4
4
  #
5
5
  # Methods provided by this class become available as named variables in
6
6
  # Mustache templates.
7
- #
8
- # Includes the library of helpers for dynamically generating HTML tags.
9
7
  class Context
10
- include Yarrow::HTML::AssetTags
11
-
12
8
  def initialize(attributes)
13
9
  metaclass = class << self; self; end
14
10
  attributes.each do |name, value|
@@ -8,12 +8,12 @@ module Yarrow
8
8
 
9
9
  # Mapping between template types and provided object model
10
10
  def object_map
11
- @config.output.object_map
11
+ @config[:output][:object_map]
12
12
  end
13
13
 
14
14
  # Mapping between template types and provided output templates.
15
15
  def template_map
16
-
16
+
17
17
  end
18
18
 
19
19
  # Template converter used by this generator instance.
@@ -0,0 +1,39 @@
1
+ module Yarrow
2
+ module Output
3
+ module Web
4
+ class IndexedFile
5
+ WRITE_MODE = 'w+:UTF-8'.freeze
6
+
7
+ # @return [String] Basename reflecting the server convention (usually: index.html)
8
+ def index_name
9
+ @index_name ||= config.index_name || 'index.html'
10
+ end
11
+
12
+ # @return [String] Docroot of the output target
13
+ def docroot
14
+ @docroot ||= config.output_dir || 'public'
15
+ end
16
+
17
+ # Write an output file to the specified path under the docroot.
18
+ #
19
+ # @param path [String]
20
+ # @param content [String]
21
+ def write(path, content)
22
+ # If the target path is a directory,
23
+ # generate a default index filename.
24
+ if path[path.length-1] == '/'
25
+ path = "#{path}#{index_name}"
26
+ end
27
+
28
+ target_path = Pathname.new("#{docroot}#{path}")
29
+
30
+ FileUtils.mkdir_p(target_path.dirname)
31
+
32
+ File.open(target_path.to_s, WRITE_MODE) do |file|
33
+ file.puts(content)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ module Yarrow
2
+ module Process
3
+ class ExpandContent < StepProcessor
4
+ accepts String
5
+ provides String
6
+
7
+ def step(source)
8
+ "#{source} | ExpandContent::Result"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Yarrow
2
+ module Process
3
+ class ExtractSource < StepProcessor
4
+ accepts String
5
+ provides String
6
+
7
+ def step(source)
8
+ "#{source} | ExtractSource::Result"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module Yarrow
2
+ module Process
3
+ class ProjectManifest < StepProcessor
4
+ accepts String
5
+ provides String
6
+
7
+ def before_step(content)
8
+
9
+ end
10
+
11
+ def step(content)
12
+ "#{content} | ProjectManifest::Result"
13
+ end
14
+
15
+ def after_step(content)
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ module Yarrow
2
+ module Process
3
+ class StepProcessor
4
+ attr_reader :source
5
+
6
+ class << self
7
+ attr_reader :accepted_input, :provided_output
8
+
9
+ def accepts(input_const)
10
+ @accepted_input = input_const.to_s
11
+ end
12
+
13
+ def provides(output_const)
14
+ @provided_output = output_const.to_s
15
+ end
16
+ end
17
+
18
+ def initialize
19
+ @source = nil
20
+ end
21
+
22
+ def accepts
23
+ self.class.accepted_input
24
+ end
25
+
26
+ def provides
27
+ self.class.provided_output
28
+ end
29
+
30
+ def can_accept?(provided)
31
+ accepts == provided
32
+ end
33
+
34
+ def process(source)
35
+ # begin
36
+ result = step(source)
37
+ # log.info("<Result source=#{result}>")
38
+ # rescue
39
+ result
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ module Yarrow
2
+ module Process
3
+ class Workflow
4
+ def initialize(input)
5
+ @input = input
6
+ @processors = []
7
+ end
8
+
9
+ def connect(processor)
10
+ provided_input = if @processors.any?
11
+ @processors.last.provides
12
+ else
13
+ @input.class.to_s
14
+ end
15
+
16
+ if processor.can_accept?(provided_input)
17
+ @processors << processor
18
+ else
19
+ raise ArgumentError.new(
20
+ "`#{processor.class}` accepts `#{processor.accepts}` but was connected to `#{provided_input}`"
21
+ )
22
+ end
23
+ end
24
+
25
+ def process(&block)
26
+ result = @input
27
+
28
+ @processors.each do |processor|
29
+ result = processor.process(result)
30
+ end
31
+
32
+ block.call(result)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,132 @@
1
+ module Yarrow
2
+ module Schema
3
+ module Type
4
+ class Any
5
+ end
6
+ end
7
+
8
+ ##
9
+ # Checks values plugged into each slot and runs any required validations
10
+ # (validations not yet implemented).
11
+ #
12
+ # Current design throws on error rather than returns a boolean result.
13
+ class Validator
14
+ # @param fields_spec [Hash] defines the slots in the schema to validate against
15
+ def initialize(fields_spec)
16
+ @spec = fields_spec
17
+ end
18
+
19
+ def check(fields)
20
+ missing_fields = @spec.keys.difference(fields.keys)
21
+
22
+ if missing_fields.any?
23
+ missing_fields.each do |field|
24
+ raise "wrong number of args" unless @spec[field].eql?(Type::Any)
25
+ end
26
+ end
27
+
28
+ mismatching_fields = fields.keys.difference(@spec.keys)
29
+
30
+ raise "key does not exist" if mismatching_fields.any?
31
+
32
+ fields.each do |(field, value)|
33
+ raise "wrong data type" unless value.is_a?(@spec[field]) || @spec[field].eql?(Type::Any)
34
+ end
35
+
36
+ true
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Value object (with comparison by value equality). This just chucks back a
42
+ # Ruby struct but wraps the constructor with method advice that handles
43
+ # validation (and eventually type coercion if !yagni).
44
+ class Value
45
+ def self.new(*slots, **fields, &block)
46
+ factory(*slots, **fields, &block)
47
+ end
48
+
49
+ def self.factory(*slots, **fields, &block)
50
+ if slots.empty? && fields.empty?
51
+ raise ArgumentError.new("missing attribute definition")
52
+ end
53
+
54
+ slots_spec, fields_spec = if fields.any?
55
+ raise ArgumentError.new("cannot use slots when field map is supplied") if slots.any?
56
+ [fields.keys, fields]
57
+ else
58
+ [slots, Hash[slots.map { |s| [s, Type::Any]}]]
59
+ end
60
+
61
+ validator = Validator.new(fields_spec)
62
+
63
+ struct = Struct.new(*slots_spec, keyword_init: true, &block)
64
+
65
+ struct.define_method :initialize do |*args, **kwargs|
66
+ attr_values = if args.any?
67
+ raise ArgumentError.new("cannot mix slots and kwargs") if kwargs.any?
68
+ Hash[slots.zip(args)]
69
+ else
70
+ kwargs
71
+ end
72
+
73
+ validator.check(attr_values)
74
+ # TODO: type coercion or mapping decision goes here
75
+ super(**attr_values)
76
+
77
+ freeze
78
+ end
79
+
80
+ struct
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Entity with comparison by reference equality. Generates attribute helpers
86
+ # for a declared set of props. Used to replace Hashie::Mash without dragging
87
+ # in a whole new library.
88
+ class Entity
89
+ class << self
90
+ def attribute(name, value_type)
91
+ # define_method("map_#{name}".to_sym) do |input|
92
+ # value_type.coerce(input)
93
+ # end
94
+ dictionary[name] = value_type
95
+ attr_reader(name)
96
+ end
97
+
98
+ def dictionary
99
+ @dictionary ||= Hash.new
100
+ end
101
+ end
102
+
103
+ def dictionary
104
+ self.class.dictionary
105
+ end
106
+
107
+ def initialize(config)
108
+ dictionary.each_key do |name|
109
+ raise "missing declared attribute #{name}" unless dictionary.key?(name)
110
+ end
111
+
112
+ config.each_pair do |key, value|
113
+ raise "#{key} not a declared attribute" unless dictionary.key?(key)
114
+
115
+ defined_type = dictionary[key]
116
+
117
+ unless value.is_a?(defined_type)
118
+ raise "#{key} accepts #{defined_type} but #{value.class} given"
119
+ end
120
+
121
+ instance_variable_set("@#{key}", value)
122
+ end
123
+ end
124
+ end
125
+
126
+ def to_h
127
+ dictionary.keys.reduce({}) do |h, name|
128
+ h[name] = instance_variable_get("@#{name}")
129
+ end
130
+ end
131
+ end
132
+ end