lifer 0.2.0 → 0.3.0
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 +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"
|