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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +26 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +110 -25
  7. data/LICENSE +18 -0
  8. data/README.md +79 -14
  9. data/Rakefile +2 -4
  10. data/bin/lifer +4 -2
  11. data/lib/lifer/brain.rb +171 -21
  12. data/lib/lifer/builder/html/from_erb.rb +92 -0
  13. data/lib/lifer/builder/html/from_liquid/drops/collection_drop.rb +40 -0
  14. data/lib/lifer/builder/html/from_liquid/drops/collections_drop.rb +40 -0
  15. data/lib/lifer/builder/html/from_liquid/drops/entry_drop.rb +63 -0
  16. data/lib/lifer/builder/html/from_liquid/drops/frontmatter_drop.rb +45 -0
  17. data/lib/lifer/builder/html/from_liquid/drops/settings_drop.rb +42 -0
  18. data/lib/lifer/builder/html/from_liquid/drops.rb +15 -0
  19. data/lib/lifer/builder/html/from_liquid/filters.rb +27 -0
  20. data/lib/lifer/builder/html/from_liquid/layout_tag.rb +67 -0
  21. data/lib/lifer/builder/html/from_liquid.rb +116 -0
  22. data/lib/lifer/builder/html.rb +107 -51
  23. data/lib/lifer/builder/rss.rb +113 -0
  24. data/lib/lifer/builder/txt.rb +60 -0
  25. data/lib/lifer/builder.rb +100 -1
  26. data/lib/lifer/cli.rb +105 -0
  27. data/lib/lifer/collection.rb +87 -8
  28. data/lib/lifer/config.rb +159 -31
  29. data/lib/lifer/dev/response.rb +61 -0
  30. data/lib/lifer/dev/router.rb +44 -0
  31. data/lib/lifer/dev/server.rb +97 -0
  32. data/lib/lifer/entry/html.rb +39 -0
  33. data/lib/lifer/entry/markdown.rb +162 -0
  34. data/lib/lifer/entry/txt.rb +41 -0
  35. data/lib/lifer/entry.rb +142 -41
  36. data/lib/lifer/message.rb +58 -0
  37. data/lib/lifer/selection/all_markdown.rb +16 -0
  38. data/lib/lifer/selection/included_in_feeds.rb +15 -0
  39. data/lib/lifer/selection.rb +79 -0
  40. data/lib/lifer/shared/finder_methods.rb +35 -0
  41. data/lib/lifer/shared.rb +6 -0
  42. data/lib/lifer/templates/cli.txt.erb +10 -0
  43. data/lib/lifer/templates/config.yaml +77 -0
  44. data/lib/lifer/templates/its-a-living.png +0 -0
  45. data/lib/lifer/templates/layout.html.erb +1 -1
  46. data/lib/lifer/uri_strategy/pretty.rb +14 -6
  47. data/lib/lifer/uri_strategy/pretty_root.rb +24 -0
  48. data/lib/lifer/uri_strategy/pretty_yyyy_mm_dd.rb +32 -0
  49. data/lib/lifer/uri_strategy/root.rb +17 -0
  50. data/lib/lifer/uri_strategy/simple.rb +10 -6
  51. data/lib/lifer/uri_strategy.rb +46 -6
  52. data/lib/lifer/utilities.rb +117 -0
  53. data/lib/lifer/version.rb +3 -0
  54. data/lib/lifer.rb +130 -23
  55. data/lifer.gemspec +12 -6
  56. data/locales/en.yml +54 -0
  57. metadata +142 -9
  58. data/lib/lifer/layout.rb +0 -25
  59. data/lib/lifer/templates/config +0 -4
  60. data/lib/lifer/uri_strategy/base.rb +0 -15
@@ -1,29 +1,64 @@
1
1
  require "fileutils"
2
2
 
3
- class Lifer::Builder::HTML
4
- DEFAULT_OUTPUT_DIRECTORY_NAME = "_build"
5
- FALLBACK_URI_STRATEGY = "simple"
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
- new(root: root).execute
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
- # Remove any existing output directory.
15
- #
16
- FileUtils.rm_r(output_directory)
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
- # Go into the fresh build directory and generate each HTML file from the
19
- # given directory.
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
- def generate_output_directories_for(entry, current_collection:)
40
- dirname =
41
- Pathname File.dirname(uri_strategy(current_collection).output_file(entry))
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
- def generate_output_file_for(entry, current_collection:)
46
- File.open(uri_strategy(current_collection).output_file(entry), "w") { |file|
47
- file.write(
48
- Lifer::Layout.build(
49
- entry: entry,
50
- template: layout_for(current_collection)
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
- def layout_for(collection)
57
- if (collection_settings = Lifer.settings[collection.name])
58
- collection_settings[:layout_file]
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
- Lifer.settings[:layout_file]
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
- def output_directory
65
- dir = "%s/%s" % [
66
- root,
67
- Lifer.settings[:output_directory] || DEFAULT_OUTPUT_DIRECTORY_NAME
68
- ]
69
-
70
- return Pathname(dir) if Dir.exist? dir
71
-
72
- Dir.mkdir(dir)
73
- Pathname(dir)
74
- end
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
- module Lifer::Builder
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
@@ -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, :entries
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, entries: entries_from(directory))
22
+ collection = new(name: name, directory:)
23
+ build_collection_entries!(collection, directory:)
24
+ collection
7
25
  end
8
26
 
9
- def entries_from(directory)
10
- Dir.glob("#{directory}/**/*.md").select { |entry|
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.new(file: 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
- def initialize(name:, entries:)
26
- @name = name
27
- @entries = entries
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"