yarrow 0.5.0 → 0.6.3

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