yarrow 0.4.3 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +23 -0
- data/.gitignore +8 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +18 -0
- data/SERVER.md +41 -0
- data/lib/yarrow.rb +18 -5
- data/lib/yarrow/config.rb +59 -0
- data/lib/yarrow/configuration.rb +35 -63
- data/lib/yarrow/content/collection_expander.rb +218 -0
- data/lib/yarrow/content/content_type.rb +42 -0
- data/lib/yarrow/content/graph.rb +33 -0
- data/lib/yarrow/content/source.rb +11 -0
- data/lib/yarrow/content/source_collector.rb +55 -0
- data/lib/yarrow/extensions.rb +1 -0
- data/lib/yarrow/extensions/mementus.rb +24 -0
- data/lib/yarrow/output/context.rb +0 -6
- data/lib/yarrow/output/generator.rb +2 -2
- data/lib/yarrow/output/web/indexed_file.rb +39 -0
- data/lib/yarrow/process/expand_content.rb +12 -0
- data/lib/yarrow/process/extract_source.rb +12 -0
- data/lib/yarrow/process/project_manifest.rb +20 -0
- data/lib/yarrow/process/step_processor.rb +43 -0
- data/lib/yarrow/process/workflow.rb +36 -0
- data/lib/yarrow/schema.rb +132 -0
- data/lib/yarrow/schema/validations/array.rb +0 -0
- data/lib/yarrow/schema/validations/object.rb +0 -0
- data/lib/yarrow/schema/validations/string.rb +0 -0
- data/lib/yarrow/server.rb +8 -5
- data/lib/yarrow/source/graph.rb +6 -0
- data/lib/yarrow/symbols.rb +19 -0
- data/lib/yarrow/tools/content_utils.rb +66 -0
- data/lib/yarrow/tools/front_matter.rb +4 -2
- data/lib/yarrow/version.rb +3 -2
- data/lib/yarrow/web/html_document.rb +9 -0
- data/lib/yarrow/web/manifest.rb +9 -0
- data/lib/yarrow/web/static_asset.rb +9 -0
- data/lib/yarrow/web/template.rb +9 -0
- data/yarrow.gemspec +30 -0
- metadata +61 -47
- data/lib/yarrow/html.rb +0 -1
- data/lib/yarrow/html/asset_tags.rb +0 -59
- data/lib/yarrow/html/content_tags.rb +0 -7
- 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
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Yarrow
|
2
|
+
module Content
|
3
|
+
# A directed graph of every element of content in the project.
|
4
|
+
class Graph
|
5
|
+
# Construct a graph collected from source content files.
|
6
|
+
def self.from_source(config)
|
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
|
+
end
|
21
|
+
|
22
|
+
# List of source files.
|
23
|
+
def files
|
24
|
+
graph.nodes(:file)
|
25
|
+
end
|
26
|
+
|
27
|
+
# List of source directories.
|
28
|
+
def directories
|
29
|
+
graph.nodes(:directory)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Yarrow
|
2
|
+
module Content
|
3
|
+
# Collects a digraph of all directories and files underneath the given input
|
4
|
+
# directory.
|
5
|
+
class SourceCollector
|
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
|
11
|
+
|
12
|
+
directories = {
|
13
|
+
Pathname.new(input_dir).to_s => root.id
|
14
|
+
}
|
15
|
+
|
16
|
+
Pathname.glob("#{input_dir}/**/**").each do |entry|
|
17
|
+
if entry.directory?
|
18
|
+
#puts "Reading directory: #{entry}"
|
19
|
+
|
20
|
+
content_node = create_node do |dir|
|
21
|
+
dir.label = :directory
|
22
|
+
dir.props[:name] = entry.basename.to_s
|
23
|
+
dir.props[:slug] = entry.basename.to_s
|
24
|
+
dir.props[:path] = entry.to_s
|
25
|
+
dir.props[:entry] = entry
|
26
|
+
end
|
27
|
+
|
28
|
+
directories[entry.to_s] = content_node.id
|
29
|
+
else
|
30
|
+
#puts "Reading file: #{entry} (#{entry.basename.sub_ext('')})"
|
31
|
+
|
32
|
+
content_node = create_node do |file|
|
33
|
+
file.label = :file
|
34
|
+
file.props[:name] = entry.basename.to_s
|
35
|
+
file.props[:slug] = entry.basename.sub_ext('').to_s
|
36
|
+
file.props[:path] = entry.to_s
|
37
|
+
file.props[:entry] = entry
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if directories.key?(entry.dirname.to_s)
|
42
|
+
#puts "Create parent edge: #{directories[entry.dirname.to_s]}"
|
43
|
+
|
44
|
+
create_edge do |edge|
|
45
|
+
edge.label = :child
|
46
|
+
edge.from = directories[entry.dirname.to_s]
|
47
|
+
edge.to = content_node
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
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,12 +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
|
-
#
|
10
7
|
class Context
|
11
|
-
include Yarrow::HTML::AssetTags
|
12
|
-
|
13
8
|
def initialize(attributes)
|
14
9
|
metaclass = class << self; self; end
|
15
10
|
attributes.each do |name, value|
|
@@ -17,6 +12,5 @@ module Yarrow
|
|
17
12
|
end
|
18
13
|
end
|
19
14
|
end
|
20
|
-
|
21
15
|
end
|
22
16
|
end
|
@@ -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
|
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,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
|