lifer 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +110 -25
- data/LICENSE +18 -0
- data/README.md +79 -14
- data/Rakefile +2 -4
- data/bin/lifer +4 -2
- data/lib/lifer/brain.rb +171 -21
- data/lib/lifer/builder/html/from_erb.rb +92 -0
- data/lib/lifer/builder/html/from_liquid/drops/collection_drop.rb +40 -0
- data/lib/lifer/builder/html/from_liquid/drops/collections_drop.rb +40 -0
- data/lib/lifer/builder/html/from_liquid/drops/entry_drop.rb +63 -0
- data/lib/lifer/builder/html/from_liquid/drops/frontmatter_drop.rb +45 -0
- data/lib/lifer/builder/html/from_liquid/drops/settings_drop.rb +42 -0
- data/lib/lifer/builder/html/from_liquid/drops.rb +15 -0
- data/lib/lifer/builder/html/from_liquid/filters.rb +27 -0
- data/lib/lifer/builder/html/from_liquid/layout_tag.rb +67 -0
- data/lib/lifer/builder/html/from_liquid.rb +116 -0
- data/lib/lifer/builder/html.rb +107 -51
- data/lib/lifer/builder/rss.rb +113 -0
- data/lib/lifer/builder/txt.rb +60 -0
- data/lib/lifer/builder.rb +100 -1
- data/lib/lifer/cli.rb +105 -0
- data/lib/lifer/collection.rb +87 -8
- data/lib/lifer/config.rb +159 -31
- data/lib/lifer/dev/response.rb +61 -0
- data/lib/lifer/dev/router.rb +44 -0
- data/lib/lifer/dev/server.rb +97 -0
- data/lib/lifer/entry/html.rb +39 -0
- data/lib/lifer/entry/markdown.rb +162 -0
- data/lib/lifer/entry/txt.rb +41 -0
- data/lib/lifer/entry.rb +142 -41
- data/lib/lifer/message.rb +58 -0
- data/lib/lifer/selection/all_markdown.rb +16 -0
- data/lib/lifer/selection/included_in_feeds.rb +15 -0
- data/lib/lifer/selection.rb +79 -0
- data/lib/lifer/shared/finder_methods.rb +35 -0
- data/lib/lifer/shared.rb +6 -0
- data/lib/lifer/templates/cli.txt.erb +10 -0
- data/lib/lifer/templates/config.yaml +77 -0
- data/lib/lifer/templates/its-a-living.png +0 -0
- data/lib/lifer/templates/layout.html.erb +1 -1
- data/lib/lifer/uri_strategy/pretty.rb +14 -6
- data/lib/lifer/uri_strategy/pretty_root.rb +24 -0
- data/lib/lifer/uri_strategy/pretty_yyyy_mm_dd.rb +32 -0
- data/lib/lifer/uri_strategy/root.rb +17 -0
- data/lib/lifer/uri_strategy/simple.rb +10 -6
- data/lib/lifer/uri_strategy.rb +46 -6
- data/lib/lifer/utilities.rb +117 -0
- data/lib/lifer/version.rb +3 -0
- data/lib/lifer.rb +130 -23
- data/lifer.gemspec +12 -6
- data/locales/en.yml +54 -0
- metadata +142 -9
- data/lib/lifer/layout.rb +0 -25
- data/lib/lifer/templates/config +0 -4
- data/lib/lifer/uri_strategy/base.rb +0 -15
data/lib/lifer/builder/html.rb
CHANGED
@@ -1,29 +1,64 @@
|
|
1
1
|
require "fileutils"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
# This builder makes HTML documents out of any entry type that responds to
|
4
|
+
# `#to_html` and writes them to the configured Lifer output directory.
|
5
|
+
#
|
6
|
+
# The HTML builder depends on the collection's layout file. Layout files can be
|
7
|
+
# ERB[1] or Liquid[2] template files. The layout file yields entry contents
|
8
|
+
# via a `content` call that is parsed by ERB or Liquid.
|
9
|
+
#
|
10
|
+
# Layout files can also include other contextual information about the current
|
11
|
+
# Lifer project to provide "normal website features" like navigation links,
|
12
|
+
# indexes, and so on. Context is provided via:
|
13
|
+
#
|
14
|
+
# - `my_collection_name`: Or, any collection by name.
|
15
|
+
#
|
16
|
+
# For example, you can iterate over the entries of any named collection by
|
17
|
+
# accessing the collection like this:
|
18
|
+
#
|
19
|
+
# my_collection.entries
|
20
|
+
#
|
21
|
+
# - `settings`: Serialized Lifer settings from the configuration file.
|
22
|
+
#
|
23
|
+
# - `collections`: A list of collections.
|
24
|
+
#
|
25
|
+
# - `content`: The content of the current entry.
|
26
|
+
#
|
27
|
+
# The `:content` variable is especially powerful, as it also parses any
|
28
|
+
# given entry that's an ERB file with the same local variables in context.
|
29
|
+
#
|
30
|
+
# [1]: https://docs.ruby-lang.org/en/3.3/ERB.html
|
31
|
+
# [2]: https://shopify.github.io/liquid/
|
32
|
+
#
|
33
|
+
class Lifer::Builder::HTML < Lifer::Builder
|
34
|
+
self.name = :html
|
35
|
+
|
36
|
+
require_relative "html/from_erb"
|
37
|
+
require_relative "html/from_liquid"
|
6
38
|
|
7
39
|
class << self
|
40
|
+
# Traverses and renders each entry for each collection in the configured
|
41
|
+
# output directory for the Lifer project.
|
42
|
+
#
|
43
|
+
# @param root [String] The Lifer root.
|
44
|
+
# @return [void]
|
8
45
|
def execute(root:)
|
9
|
-
|
46
|
+
Dir.chdir Lifer.output_directory do
|
47
|
+
new(root: root).execute
|
48
|
+
end
|
10
49
|
end
|
11
50
|
end
|
12
51
|
|
52
|
+
# Traverses and renders each entry for each collection.
|
53
|
+
#
|
54
|
+
# @return [void]
|
13
55
|
def execute
|
14
|
-
|
15
|
-
|
16
|
-
|
56
|
+
Lifer.collections(without_selections: true).each do |collection|
|
57
|
+
collection.entries.each do |entry|
|
58
|
+
next unless entry.class.output_extension == :html
|
17
59
|
|
18
|
-
|
19
|
-
|
20
|
-
#
|
21
|
-
Dir.chdir(output_directory) do
|
22
|
-
Lifer.collections.each do |collection|
|
23
|
-
collection.entries.each do |entry|
|
24
|
-
generate_output_directories_for entry, current_collection: collection
|
25
|
-
generate_output_file_for entry, current_collection: collection
|
26
|
-
end
|
60
|
+
generate_output_directories_for entry
|
61
|
+
generate_output_file_for entry
|
27
62
|
end
|
28
63
|
end
|
29
64
|
end
|
@@ -32,54 +67,75 @@ class Lifer::Builder::HTML
|
|
32
67
|
|
33
68
|
attr_reader :root
|
34
69
|
|
70
|
+
# @private
|
71
|
+
# @param root [String] The Lifer root.
|
72
|
+
# @return [void]
|
35
73
|
def initialize(root:)
|
36
74
|
@root = root
|
37
75
|
end
|
38
76
|
|
39
|
-
|
40
|
-
|
41
|
-
|
77
|
+
# @private
|
78
|
+
# For the given entry, ensure all of the paths to the file exist so the file
|
79
|
+
# can be safely written to.
|
80
|
+
#
|
81
|
+
# @param entry [Lifer::Entry] An entry.
|
82
|
+
# @return [Array<String>] An array containing the directories that were just
|
83
|
+
# created (or already existed).
|
84
|
+
def generate_output_directories_for(entry)
|
85
|
+
dirname = Pathname File.dirname(output_file entry)
|
42
86
|
FileUtils.mkdir_p dirname unless Dir.exist?(dirname)
|
43
87
|
end
|
44
88
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
89
|
+
# @private
|
90
|
+
# For the given entry, generate the production entry.
|
91
|
+
#
|
92
|
+
# @param entry [Lifer::Entry] An entry.
|
93
|
+
# @return [Integer] The length of the written file. We should not care about
|
94
|
+
# this return value.
|
95
|
+
def generate_output_file_for(entry)
|
96
|
+
relative_path = output_file entry
|
97
|
+
absolute_path = File.join(Lifer.output_directory, relative_path)
|
98
|
+
|
99
|
+
if File.exist?(absolute_path)
|
100
|
+
raise I18n.t("builder.file_conflict_error", path: absolute_path)
|
101
|
+
end
|
102
|
+
|
103
|
+
File.open(relative_path, "w") { |file|
|
104
|
+
file.write layout_class_for(entry).build(entry: entry)
|
53
105
|
}
|
54
106
|
end
|
55
107
|
|
56
|
-
|
57
|
-
|
58
|
-
|
108
|
+
# @private
|
109
|
+
# Given the path to a layout file, this method determines what layout builder
|
110
|
+
# will be used. The builder class must implement a `.build` class method.
|
111
|
+
#
|
112
|
+
# @param entry [Lifer::Entry] An entry
|
113
|
+
# @return [Class] A layout builder class name.
|
114
|
+
def layout_class_for(entry)
|
115
|
+
case entry.collection.setting(:layout_file)
|
116
|
+
when /.*\.erb$/ then FromERB
|
117
|
+
when /.*\.liquid$/ then FromLiquid
|
59
118
|
else
|
60
|
-
|
119
|
+
file = entry.collection.setting(:layout_file)
|
120
|
+
puts I18n.t(
|
121
|
+
"builder.html.no_builder_error",
|
122
|
+
file:,
|
123
|
+
type: File.extname(file)
|
124
|
+
)
|
125
|
+
exit
|
61
126
|
end
|
62
127
|
end
|
63
128
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
def uri_strategy(current_collection)
|
77
|
-
collection_settings =
|
78
|
-
Lifer.settings[current_collection.name] || Lifer.settings[:root]
|
79
|
-
current_uri_strategy =
|
80
|
-
collection_settings && collection_settings[:uri_strategy] ||
|
81
|
-
FALLBACK_URI_STRATEGY
|
82
|
-
|
83
|
-
Lifer::URIStrategy.find_by_name(current_uri_strategy).new(root: root)
|
129
|
+
# @private
|
130
|
+
# Using the URI strategy configured for the entry's collection, generate a
|
131
|
+
# permalink (or output filename).
|
132
|
+
#
|
133
|
+
# @param entry [Lifer::Entry] The entry.
|
134
|
+
# @return [String] The permalink to the entry.
|
135
|
+
def output_file(entry)
|
136
|
+
Lifer::URIStrategy
|
137
|
+
.find(entry.collection.setting :uri_strategy)
|
138
|
+
.new(root: root)
|
139
|
+
.output_file(entry)
|
84
140
|
end
|
85
141
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "rss"
|
3
|
+
|
4
|
+
# Builds a simple RSS 2.0[1] feed using the Ruby standard library's RSS
|
5
|
+
# features.
|
6
|
+
#
|
7
|
+
# [1]: https://www.rssboard.org/rss-specification
|
8
|
+
#
|
9
|
+
class Lifer::Builder::RSS < Lifer::Builder
|
10
|
+
self.name = :rss
|
11
|
+
self.settings = [:rss]
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Traverses and renders an RSS feed for each feedable collection in the
|
15
|
+
# configured output directory for the Lifer project.
|
16
|
+
#
|
17
|
+
# @param root [String] The Lifer root.
|
18
|
+
# @return [void]
|
19
|
+
def execute(root:)
|
20
|
+
Dir.chdir Lifer.output_directory do
|
21
|
+
new(root: root).execute
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Traverses and renders an RSS feed for feedable collection.
|
27
|
+
#
|
28
|
+
# @return [void]
|
29
|
+
def execute
|
30
|
+
collections_with_feeds.each do |collection|
|
31
|
+
next unless (filename = output_filename(collection))
|
32
|
+
|
33
|
+
FileUtils.mkdir_p File.dirname(filename)
|
34
|
+
|
35
|
+
File.open filename, "w" do |file|
|
36
|
+
file.puts(
|
37
|
+
rss_feed_for(collection) do |current_feed|
|
38
|
+
collection.entries
|
39
|
+
.select { |entry| entry.feedable? }
|
40
|
+
.each { |entry| rss_entry current_feed, entry }
|
41
|
+
end.to_feed
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :collections_with_feeds, :root
|
50
|
+
|
51
|
+
def initialize(root:)
|
52
|
+
@collections_with_feeds =
|
53
|
+
Lifer.collections.select { |collection| collection.setting :rss }
|
54
|
+
@root = root
|
55
|
+
end
|
56
|
+
|
57
|
+
def output_filename(collection)
|
58
|
+
strict_mode = !collection.root?
|
59
|
+
|
60
|
+
case collection.setting(:rss, strict: strict_mode)
|
61
|
+
when FalseClass, NilClass then nil
|
62
|
+
when TrueClass then File.join(Dir.pwd, "#{collection.name}.xml")
|
63
|
+
else
|
64
|
+
File.join Dir.pwd, collection.setting(:rss, strict: strict_mode)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def rss_entry(rss_feed, lifer_entry)
|
69
|
+
rss_feed.maker.items.new_item do |rss_feed_item|
|
70
|
+
if (authors = lifer_entry.authors).any?
|
71
|
+
rss_feed_item.author = authors.join(", ")
|
72
|
+
end
|
73
|
+
rss_feed_item.id = lifer_entry.permalink
|
74
|
+
rss_feed_item.link = lifer_entry.permalink
|
75
|
+
rss_feed_item.title = lifer_entry.title
|
76
|
+
rss_feed_item.summary = lifer_entry.summary
|
77
|
+
rss_feed_item.updated = Time.now.to_s
|
78
|
+
rss_feed_item.content_encoded = lifer_entry.to_html
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def rss_feed_for(collection, &block)
|
83
|
+
feed_object = nil
|
84
|
+
|
85
|
+
::RSS::Maker.make "rss2.0" do |feed|
|
86
|
+
feed.channel.description =
|
87
|
+
Lifer.setting(:description, collection: collection) ||
|
88
|
+
Lifer.setting(:site_title, collection: collection)
|
89
|
+
|
90
|
+
feed.channel.language = Lifer.setting(:language, collection: collection)
|
91
|
+
|
92
|
+
feed.channel.lastBuildDate = Time.now.to_s
|
93
|
+
|
94
|
+
feed.channel.link = "%s/%s" % [
|
95
|
+
Lifer.setting(:global, :host),
|
96
|
+
Lifer.setting(:rss, collection: collection)
|
97
|
+
]
|
98
|
+
|
99
|
+
feed.channel.managingEditor =
|
100
|
+
Lifer.setting(:site_default_author, collection: collection)
|
101
|
+
|
102
|
+
feed.channel.title = Lifer.setting(:title, collection: collection)
|
103
|
+
|
104
|
+
feed.channel.webMaster =
|
105
|
+
Lifer.setting(:site_default_author, collection: collection)
|
106
|
+
|
107
|
+
yield feed
|
108
|
+
|
109
|
+
feed_object = feed
|
110
|
+
end
|
111
|
+
feed_object
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
# Builds a text file from a text file.
|
4
|
+
#
|
5
|
+
# Note that the collection's URI strategy is still in play here, so the output
|
6
|
+
# path may be different than the input path.
|
7
|
+
#
|
8
|
+
class Lifer::Builder::TXT < Lifer::Builder
|
9
|
+
self.name = :txt
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Builds text files within the Lifer project's build directory.
|
13
|
+
#
|
14
|
+
# @param root [String] The Lifer root directory.
|
15
|
+
# @return [void]
|
16
|
+
def execute(root:)
|
17
|
+
Dir.chdir Lifer.output_directory do
|
18
|
+
new(root:).execute
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Builds each entry in each collection, including any requirements (like
|
24
|
+
# subdirectories) those entries have.
|
25
|
+
#
|
26
|
+
# @return [void]
|
27
|
+
def execute
|
28
|
+
Lifer.collections(without_selections: true).each do |collection|
|
29
|
+
collection.entries.each do |entry|
|
30
|
+
next unless entry.class.output_extension == :txt
|
31
|
+
|
32
|
+
relative_path = output_file entry
|
33
|
+
absolute_path = File.join(Lifer.output_directory, relative_path)
|
34
|
+
|
35
|
+
FileUtils.mkdir_p File.dirname(relative_path)
|
36
|
+
|
37
|
+
if File.exist?(absolute_path)
|
38
|
+
raise I18n.t("builder.file_conflict_error", path: absolute_path)
|
39
|
+
end
|
40
|
+
|
41
|
+
File.open(relative_path, "w") { |file| file.write entry.full_text }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :root
|
49
|
+
|
50
|
+
def initialize(root:)
|
51
|
+
@root = root
|
52
|
+
end
|
53
|
+
|
54
|
+
def output_file(entry)
|
55
|
+
Lifer::URIStrategy
|
56
|
+
.find(entry.collection.setting :uri_strategy)
|
57
|
+
.new(root:)
|
58
|
+
.output_file(entry)
|
59
|
+
end
|
60
|
+
end
|
data/lib/lifer/builder.rb
CHANGED
@@ -1,4 +1,103 @@
|
|
1
|
-
|
1
|
+
require "open3"
|
2
|
+
|
3
|
+
# Builders are a core part of Lifer. Builders are configured by users in the
|
4
|
+
# configuration file and take the content of a Lifer project and turn it into
|
5
|
+
# built stuff. That could be feeds, HTML documents, or other weird files.
|
6
|
+
#
|
7
|
+
# This base class includes some special functionality for running all builders
|
8
|
+
# when a Lifer build is executed. But it also sets in stone the public API for
|
9
|
+
# implementing builder classes. (In short: an `.execute` class method and an
|
10
|
+
# `#execute` instance method.)
|
11
|
+
#
|
12
|
+
class Lifer::Builder
|
13
|
+
include Lifer::Shared::FinderMethods
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :name, :settings
|
17
|
+
|
18
|
+
# Every builder class must have execute method. This is the entrypoint for
|
19
|
+
# instantiating *and* executing any builder.
|
20
|
+
#
|
21
|
+
# @param root [string] An absolute path to the Lifer project root directory.
|
22
|
+
# @return [NotImplementedError] A builder subclass must implement this
|
23
|
+
# method.
|
24
|
+
def execute(root:) = (raise NotImplementedError)
|
25
|
+
|
26
|
+
# Given a list of builder names, we execute every builder based on the
|
27
|
+
# configured Lifer project root.
|
28
|
+
#
|
29
|
+
# @param builder_names [Array<string>] A list of builder names.
|
30
|
+
# @param root [string] The absolute path to the Lifer root directory.
|
31
|
+
# @return [void]
|
32
|
+
def build!(*builder_names, root:)
|
33
|
+
builder_names.each do |builder|
|
34
|
+
Lifer::Builder.find(builder).execute root: root
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Given a list of prebuild commands, execute each one in the shell.
|
39
|
+
# This is meant to be run once before `.build!` is run once.
|
40
|
+
#
|
41
|
+
# @param commands [Array<string>] A list of executable commands.
|
42
|
+
# @param root [string] The absolute path to the Lifer root directory.
|
43
|
+
# @return [void]
|
44
|
+
def prebuild!(*commands, root:)
|
45
|
+
commands.each do |command|
|
46
|
+
puts command
|
47
|
+
|
48
|
+
_stdin, stdout, stderr, _wait_thread = Open3.popen3(command)
|
49
|
+
|
50
|
+
if (error_messages = stderr.readlines).any?
|
51
|
+
raise error_messages.join("\n")
|
52
|
+
end
|
53
|
+
|
54
|
+
stdout.readlines.each { puts _1 }
|
55
|
+
end
|
56
|
+
rescue => exception
|
57
|
+
raise I18n.t("builder.prebuild_failure", exception:)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# @private
|
63
|
+
# We use the `Class#inherited` hook to add functionality to our builder
|
64
|
+
# subclasses as they're being initialized. This makes them more ergonomic to
|
65
|
+
# configure.
|
66
|
+
#
|
67
|
+
# @param klass [Class] The superclass.
|
68
|
+
# @return [void]
|
69
|
+
def inherited(klass)
|
70
|
+
klass.prepend InitializeBuilder
|
71
|
+
|
72
|
+
klass.name ||= :unnamed_builder
|
73
|
+
klass.settings ||= []
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Every builder class must have execute instance method. This is where the
|
78
|
+
# core logic of the builder runs from after initialization.
|
79
|
+
#
|
80
|
+
# @return [NotImplementedError] A builder subclass must implement this
|
81
|
+
# method.
|
82
|
+
def execute = (raise NotImplementedError)
|
83
|
+
|
84
|
+
# When any new builder instance is initialized, we need to ensure that any
|
85
|
+
# settings specific to the builder are registered. We can do this automatically
|
86
|
+
# by wrapping the `#initialize` method. This module provides the wrapper
|
87
|
+
# functionality--we just need to ensure that this module is included.
|
88
|
+
#
|
89
|
+
module InitializeBuilder
|
90
|
+
# @!visibility private
|
91
|
+
def initialize(...)
|
92
|
+
Lifer.register_settings(*self.class.settings) if self.class.settings.any?
|
93
|
+
|
94
|
+
super(...)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
self.name = :builder
|
2
99
|
end
|
3
100
|
|
101
|
+
require_relative "builder/rss"
|
4
102
|
require_relative "builder/html"
|
103
|
+
require_relative "builder/txt"
|
data/lib/lifer/cli.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
require "lifer/dev/server"
|
4
|
+
|
5
|
+
module Lifer
|
6
|
+
# This class is the entrypoint for Lifer's commandline interface.
|
7
|
+
#
|
8
|
+
class CLI
|
9
|
+
# The core CLI help text lives in a template file.
|
10
|
+
#
|
11
|
+
BANNER_ERB =
|
12
|
+
File.read("%s/lib/lifer/templates/cli.txt.erb" % Lifer.gem_root)
|
13
|
+
|
14
|
+
# This constant tracks the supported Lifer CLI subcommands.
|
15
|
+
#
|
16
|
+
# Key: name
|
17
|
+
# Value: description
|
18
|
+
#
|
19
|
+
SUBCOMMANDS = {
|
20
|
+
build: I18n.t("cli.subcommands.build"),
|
21
|
+
help: I18n.t("cli.subcommands.help"),
|
22
|
+
serve: I18n.t("cli.subcommands.serve")
|
23
|
+
}
|
24
|
+
|
25
|
+
class << self
|
26
|
+
# This method parses the given CLI subcommands and arguments, and then
|
27
|
+
# starts the Lifer program as requested.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def start! = self.new.start!
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :args, :subcommand, :parser
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@subcommand, @args = user_input
|
37
|
+
@parser =
|
38
|
+
OptionParser.new do |parser|
|
39
|
+
parser.banner = ERB.new(BANNER_ERB).result
|
40
|
+
|
41
|
+
parser.on "-cCONFIG", "--config=CONFIG", topt(:config) do |config|
|
42
|
+
@config_file = config
|
43
|
+
end
|
44
|
+
|
45
|
+
parser.on "-d", "--dump-default-config", topt(:dump_default_config) do |_|
|
46
|
+
puts File.read(Lifer::Config::DEFAULT_CONFIG_FILE)
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
parser.on "-pPORT", "--port=PORT", topt(:port) do |port|
|
51
|
+
@dev_server_port = Integer port
|
52
|
+
rescue => exception
|
53
|
+
raise I18n.t("cli.bad_port", exception:)
|
54
|
+
end
|
55
|
+
|
56
|
+
parser.on "-rROOT", "--root=ROOT", topt(:root) do |root|
|
57
|
+
@root = root
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Parses the user-provided CLI arguments and handles any requested commands.
|
63
|
+
#
|
64
|
+
# @return [void]
|
65
|
+
def start!
|
66
|
+
parser.parse! args
|
67
|
+
|
68
|
+
brain = Lifer.brain(**{root: @root, config_file: @config_file}.compact)
|
69
|
+
brain.require_user_provided_ruby_files!
|
70
|
+
|
71
|
+
case subcommand
|
72
|
+
when :build then Lifer.build!
|
73
|
+
when :help then parser.parse!(["--help"])
|
74
|
+
when :serve then Lifer::Dev::Server.start!(port: @dev_server_port)
|
75
|
+
else
|
76
|
+
puts I18n.t(
|
77
|
+
"cli.no_subcommand",
|
78
|
+
subcommand: Lifer::Utilities.bold_text(subcommand),
|
79
|
+
default_command: Lifer::Utilities.bold_text("lifer build")
|
80
|
+
)
|
81
|
+
|
82
|
+
parser.parse!(args) && Lifer.build!
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# @private
|
89
|
+
# Convenience method to get translated option documentation.
|
90
|
+
#
|
91
|
+
def topt(i18n_key)
|
92
|
+
I18n.t("cli.options.#{i18n_key}")
|
93
|
+
end
|
94
|
+
|
95
|
+
# @private
|
96
|
+
# Pre-parse the given CLI arguments to check for subcommands.
|
97
|
+
#
|
98
|
+
def user_input
|
99
|
+
return [:build, ARGV] if ARGV.empty?
|
100
|
+
return [:build, ARGV] if ARGV.first.start_with?("-")
|
101
|
+
|
102
|
+
[ARGV.first.to_sym, ARGV[1..]]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/lifer/collection.rb
CHANGED
@@ -1,13 +1,37 @@
|
|
1
|
+
# A collection collects entries. Every entry can only be included in a single
|
2
|
+
# collection. Collections let the user group entries together into logical
|
3
|
+
# units. For example, if the user wants to present blog posts in one way and
|
4
|
+
# wiki pages in a different way, it would make sense to create separate "blog" and
|
5
|
+
# "wiki" collections.
|
6
|
+
#
|
7
|
+
# Each collection can have its own feeds and settings. The only special
|
8
|
+
# collection is the "root" collection, which is where all entries that don't fall
|
9
|
+
# into other collections end up.
|
10
|
+
#
|
1
11
|
class Lifer::Collection
|
2
|
-
attr_reader :name
|
12
|
+
attr_reader :name
|
3
13
|
|
4
14
|
class << self
|
15
|
+
# Generate a new collection.
|
16
|
+
#
|
17
|
+
# @param name [String] The name of the new collection.
|
18
|
+
# @param directory [String] The absolute path to the root directory of the
|
19
|
+
# collection.
|
20
|
+
# @return [Lifer::Collection]
|
5
21
|
def generate(name:, directory:)
|
6
|
-
new(name: name,
|
22
|
+
collection = new(name: name, directory:)
|
23
|
+
build_collection_entries!(collection, directory:)
|
24
|
+
collection
|
7
25
|
end
|
8
26
|
|
9
|
-
|
10
|
-
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_collection_entries!(collection, directory:)
|
30
|
+
entry_glob = Dir.glob("#{directory}/**/*")
|
31
|
+
.select { |candidate| File.file? candidate }
|
32
|
+
.select { |candidate| Lifer::Entry.supported? candidate }
|
33
|
+
|
34
|
+
entries = entry_glob.select { |entry|
|
11
35
|
if Lifer.manifest.include? entry
|
12
36
|
false
|
13
37
|
elsif Lifer.ignoreable? entry.gsub("#{directory}/", "")
|
@@ -16,14 +40,69 @@ class Lifer::Collection
|
|
16
40
|
Lifer.manifest << entry
|
17
41
|
true
|
18
42
|
end
|
19
|
-
}.map { |entry| Lifer::Entry.
|
43
|
+
}.map { |entry| Lifer::Entry.generate file: entry, collection: }
|
44
|
+
|
45
|
+
collection.instance_variable_set("@entries_collection", entries)
|
20
46
|
end
|
21
47
|
end
|
22
48
|
|
49
|
+
# Each collection has a collection of entries. An entry only belongs to one
|
50
|
+
# collection.
|
51
|
+
#
|
52
|
+
# @return [Array<Lifer::Entry>] A collection of entries.
|
53
|
+
def entries(order: :latest)
|
54
|
+
cached_entries_variable = "@collection_entries_#{order}"
|
55
|
+
instance_variable_get(cached_entries_variable) ||
|
56
|
+
instance_variable_set(
|
57
|
+
cached_entries_variable,
|
58
|
+
case order
|
59
|
+
when :latest
|
60
|
+
@entries_collection.sort_by { |entry| entry.date }.reverse
|
61
|
+
when :oldest
|
62
|
+
@entries_collection.sort_by { |entry| entry.date }
|
63
|
+
end
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
# To allow for flexible configuration, a layout file may be set by users to
|
68
|
+
# either an absolute path or a path relative to the configuration file's
|
69
|
+
# location. This method, though, always returns the absolute path.
|
70
|
+
#
|
71
|
+
# @return [String] The absolute path to the collection's layout file.
|
72
|
+
def layout_file
|
73
|
+
return setting :layout_file if setting(:layout_file).include?(Lifer.gem_root)
|
74
|
+
return setting :layout_file if setting(:layout_file).include?(Lifer.root)
|
75
|
+
|
76
|
+
config_directory = File.dirname Lifer.config_file
|
77
|
+
|
78
|
+
[config_directory, setting(:layout_file)].join "/"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check whether the current collection is the root collection.
|
82
|
+
#
|
83
|
+
# @return [boolean]
|
84
|
+
def root?
|
85
|
+
name == :root
|
86
|
+
end
|
87
|
+
|
88
|
+
# Gets a Lifer setting, scoped to the current collection.
|
89
|
+
#
|
90
|
+
# @param name [*Symbol] A list of symbols that map to a nested Lifer
|
91
|
+
# setting (for the current collection).
|
92
|
+
# @return [String, Nil] The setting as set in the Lifer project's
|
93
|
+
# configuration file.
|
94
|
+
def setting(*name, strict: false)
|
95
|
+
Lifer.setting *name, collection: self, strict: strict
|
96
|
+
end
|
97
|
+
|
23
98
|
private
|
24
99
|
|
25
|
-
|
26
|
-
|
27
|
-
|
100
|
+
attr_reader :directory
|
101
|
+
|
102
|
+
def initialize(name:, directory:)
|
103
|
+
@name = name.to_sym
|
104
|
+
@directory = directory
|
28
105
|
end
|
29
106
|
end
|
107
|
+
|
108
|
+
require_relative "selection"
|