lifer 0.2.0 → 0.3.0

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