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
data/lib/lifer/entry.rb CHANGED
@@ -1,65 +1,166 @@
1
- require "date"
2
- require "kramdown"
3
- require "time"
1
+ require "digest/sha1"
2
+
3
+ # An entry is a Lifer file that will be built into the output directory.
4
+ # There are more than one subclass of entry: Markdown entries are the most
5
+ # traditional, but HTML and text files are also very valid entries.
6
+ #
7
+ # This class provides a baseline of the functionality that all entry subclasses
8
+ # should implement. It also provides the entry generator for *all* entry
9
+ # subclasses.
10
+ #
11
+ # FIXME: Markdown entries are able to provide metadata via frontmatter, but
12
+ # other entry types do not currently support frontmatter. Should they? Or is
13
+ # there some nicer way to provide entry metadata for non-Markdown files in
14
+ # 2024?
15
+ #
16
+ class Lifer::Entry
17
+ class << self
18
+ attr_accessor :include_in_feeds
19
+ attr_accessor :input_extensions
20
+ attr_accessor :output_extension
21
+ end
4
22
 
5
- require_relative "utilities"
23
+ self.include_in_feeds = false
24
+ self.input_extensions = []
25
+ self.output_extension = nil
26
+
27
+ require_relative "entry/html"
28
+ require_relative "entry/markdown"
29
+ require_relative "entry/txt"
30
+
31
+ # We provide a default date for entries that have no date and entry types that
32
+ # otherwise could not have a date due to no real way of getting that metadata.
33
+ #
34
+ DEFAULT_DATE = Time.new(1900, 01, 01, 0, 0, 0, "+00:00")
35
+
36
+ attr_reader :file, :collection
37
+
38
+ class << self
39
+ # The entrypoint for generating entry objects. We should never end up with
40
+ # `Lifer::Entry` records: only subclasses.
41
+ #
42
+ # @param file [String] The absolute filename of an entry file.
43
+ # @param collection [Lifer::Collection] The collection for the entry.
44
+ # @return [Lifer::Entry::HTML, Lifer::Entry::Markdown]
45
+ def generate(file:, collection:)
46
+ error!(file) unless File.exist?(file)
47
+
48
+ if (new_entry = subclass_for(file)&.new(file:, collection:))
49
+ Lifer.entry_manifest << new_entry
50
+ end
51
+ new_entry
52
+ end
6
53
 
7
- class Lifer::Entry
8
- FILENAME_DATE_FORMAT = /^(\d{4}-\d{1,2}-\d{1,2})-/
9
- FRONTMATTER_REGEX = /^---\n(.*)---\n/m
54
+ # Whenever an entry is generated it should be added to the entry manifest.
55
+ # This lets us get a list of all generated entries.
56
+ #
57
+ # @return [Array<Lifer::Entry>] A list of all entries that currently exist.
58
+ def manifest
59
+ return Lifer.entry_manifest if self == Lifer::Entry
10
60
 
11
- attr_reader :file
61
+ Lifer.entry_manifest.select { |entry| entry.class == self }
62
+ end
12
63
 
13
- def initialize(file:)
14
- @file = File.exist?(file) ? Pathname(file) : nil
15
- end
64
+ # Checks whether the given filename is supported entry type (using only its
65
+ # file extension).
66
+ #
67
+ # @param filename [String] The absolute filename to an entry.
68
+ # @param file_extensions [Array<String>] An array of file extensions to
69
+ # check against.
70
+ # @return [Boolean]
71
+ def supported?(filename, file_extensions= supported_file_extensions)
72
+ file_extensions.any? { |ext| filename.end_with? ext }
73
+ end
16
74
 
17
- def body
18
- return full_text.strip unless frontmatter?
75
+ private
19
76
 
20
- full_text.gsub(FRONTMATTER_REGEX, "").strip
21
- end
77
+ def supported_file_extensions
78
+ @supported_file_extensions ||= subclasses.flat_map(&:input_extensions)
79
+ end
22
80
 
23
- def date
24
- date_data = frontmatter[:date] || filename_date
81
+ # @private
82
+ # Retrieve the entry subclass based on the current filename.
83
+ #
84
+ # @param filename [String] The current entry's filename.
85
+ # @return [Class] The entry subclass for the current entry.
86
+ def subclass_for(filename)
87
+ Lifer::Entry.subclasses.detect { |klass|
88
+ klass.input_extensions.any? { |ext| filename.end_with? ext }
89
+ }
90
+ end
25
91
 
26
- case date_data
27
- when Time then date_data
28
- when String then DateTime.parse(date_data).to_time
29
- else
30
- puts "[%s]: no date metadata" % [file]
31
- nil
92
+ # @private
93
+ def error!(file)
94
+ raise StandardError, I18n.t("entry.not_found", file:)
32
95
  end
33
- rescue ArgumentError => error
34
- puts "[%s]: %s" % [file, error]
35
- nil
36
96
  end
37
97
 
38
- def frontmatter
39
- return {} unless frontmatter?
98
+ # When a new Markdown entry is initialized we expect the file to already
99
+ # exist, and we expect to know which `Lifer::Collection` it belongs to.
100
+ #
101
+ # @param file [String] An absolute path to a file.
102
+ # @param collection [Lifer::Collection] A collection.
103
+ # @return [Lifer::Entry]
104
+ def initialize(file:, collection:)
105
+ @file = Pathname file
106
+ @collection = collection
107
+ end
108
+
109
+ def feedable?
110
+ if (setting = self.class.include_in_feeds).nil?
111
+ raise NotImplementedError,
112
+ I18n.t("entry.feedable_error", entry_class: self.class)
113
+ end
40
114
 
41
- Lifer::Utilities.symbolize_keys(
42
- YAML.load(full_text[FRONTMATTER_REGEX, 1], permitted_classes: [Time])
43
- )
115
+ setting
44
116
  end
45
117
 
118
+ # The full text of the entry.
119
+ #
120
+ # @return [String]
46
121
  def full_text
47
- File.readlines(file).join if file
122
+ @full_text ||= File.readlines(file).join if file
48
123
  end
49
124
 
50
- def to_html
51
- Kramdown::Document.new(body).to_html
125
+ # Using the current Lifer configuration, we can calculate the expected
126
+ # permalink for the entry. For example:
127
+ #
128
+ # https://example.com/index.html
129
+ # https://example.com/blog/my-trip-to-toronto.html
130
+ #
131
+ # This would be useful for indexes and feeds and so on.
132
+ #
133
+ # @return [String] A permalink to the current entry.
134
+ def permalink(host: Lifer.setting(:global, :host))
135
+ cached_permalink_variable =
136
+ "@entry_permalink_" + Digest::SHA1.hexdigest(host)
137
+
138
+ instance_variable_get(cached_permalink_variable) ||
139
+ instance_variable_set(
140
+ cached_permalink_variable,
141
+ File.join(
142
+ host,
143
+ Lifer::URIStrategy.find(collection.setting :uri_strategy)
144
+ .new(root: Lifer.root)
145
+ .output_file(self)
146
+ )
147
+ )
52
148
  end
53
149
 
54
- private
55
-
56
- def filename_date
57
- return unless file && File.basename(file).match?(FILENAME_DATE_FORMAT)
150
+ # The expected, absolute URI path to the entry. For example:
151
+ #
152
+ # /index.html
153
+ # /blog/my-trip-to-toronto.html
154
+ #
155
+ # @return [String] The absolute URI path to the entry.
156
+ def path = permalink(host: "/")
58
157
 
59
- File.basename(file).match(FILENAME_DATE_FORMAT)[1]
158
+ def title
159
+ raise NotImplementedError, I18n.t("shared.not_implemented_method")
60
160
  end
61
161
 
62
- def frontmatter?
63
- full_text && full_text.match?(FRONTMATTER_REGEX)
162
+ def to_html
163
+ raise NotImplementedError, I18n.t("shared.not_implemented_method")
64
164
  end
65
165
  end
166
+
@@ -0,0 +1,58 @@
1
+ # This message class lets us output rich messages to STDOUT without muddying up
2
+ # the Lifer source code with ad hoc `puts` statements. Using this interface
3
+ # helps us ensure that translations are accounted for, and it lets us format
4
+ # errors and log messages in a consistent way.
5
+ #
6
+ # If the program is in test mode, this means the message should not be output
7
+ # to STDOUT.
8
+ #
9
+ class Lifer::Message
10
+ # ANSI colour codes for colours used by different messages.
11
+ #
12
+ ANSI_COLOURS = {red: "31"}
13
+
14
+ class << self
15
+ # Outputs a red error message into STDOUT for higher visibility. Note that
16
+ # this is still just a message, and the program might not exit due to an
17
+ # exception having been raised.
18
+ #
19
+ # @param translation_key [String] A translation key that can be read by the
20
+ # `I18n` library.
21
+ # @param test_mode [boolean] Whether the message should be output as if the
22
+ # program were in test mode.
23
+ # @param args [**Hash] A catch-all keyword arguments to be passed on to
24
+ # `I18n.t!`.
25
+ # @return [void] The message is pushed to STDOUT.
26
+ def error(translation_key, test_mode: test_mode?, **args)
27
+ return if test_mode
28
+
29
+ prefix = I18n.t("message.prefix.error")
30
+ error_message = I18n.t!(translation_key, **args)
31
+
32
+ puts colorize(("%s: %s" % [prefix, error_message]), :red)
33
+ end
34
+
35
+ # Outputs a log message into STDOUT.
36
+ #
37
+ # @param translation_key [String] A translation key that can be read by the
38
+ # `I18n` library.
39
+ # @param test_mode [boolean] Whether the message should be output as if the
40
+ # program were in test mode.
41
+ # @param args [**Hash] A catch-all keyword arguments to be passed on to
42
+ # `I18n.t!`.
43
+ # @return [void] The message is pushed to STDOUT.
44
+ def log(translation_key, test_mode: test_mode?, **args)
45
+ return if test_mode
46
+
47
+ puts I18n.t!(translation_key, **args)
48
+ end
49
+
50
+ private
51
+
52
+ def colorize(text, ansi_colour_name)
53
+ "\e[#{ANSI_COLOURS[ansi_colour_name]}m#{text}\e[0m"
54
+ end
55
+
56
+ def test_mode? = (ENV["LIFER_ENV"] == "test")
57
+ end
58
+ end
@@ -0,0 +1,16 @@
1
+ # This selection provides a list of all Markdown entries from all
2
+ # collections.
3
+ #
4
+ # To provide this to Lifer, configure it in your selections:
5
+ #
6
+ # selections:
7
+ # - lifer/selection/all_markdown
8
+ #
9
+ class Lifer::Selection::AllMarkdown < Lifer::Selection
10
+ self.name = :all_markdown
11
+
12
+ # @!visibility private
13
+ def entries
14
+ Lifer::Entry::Markdown.manifest
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # This selection provides a list of all entries that can be included in feeds.
2
+ #
3
+ # To provide this to Lifer, configure it in your selections:
4
+ #
5
+ # selections:
6
+ # - lifer/selection/included_in_feeds
7
+ #
8
+ class Lifer::Selection::IncludedInFeeds < Lifer::Selection
9
+ self.name = :included_in_feeds
10
+
11
+ # @!visibility private
12
+ def entries
13
+ Lifer::Entry.manifest.select { |entry| entry.class.include_in_feeds }
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ # A selection is a group of entries that belong to any collection.
2
+ # Lifer includes some selection classes by default, but the intent here is to
3
+ # let users bring their own selections.
4
+ #
5
+ # A selection subclass can be added to the Lifer project as a Ruby file. Any
6
+ # detected Ruby files are dynamically loaded when `Lifer::Brain` is initialized.
7
+ #
8
+ # Implementing a selection is simple. Just implement the `#entries` method and
9
+ # rovide a name. The `#entries` method can be used to filter down
10
+ # `Lifer.entry_manifest` in whichever way one needs. To see examples of this,
11
+ # check out the source code of any of the included selections.
12
+ #
13
+ class Lifer::Selection < Lifer::Collection
14
+ class << self
15
+ attr_accessor :name
16
+
17
+ # The constructor method for selections. Unlike collections:
18
+ #
19
+ # 1. Selections never have a unique instance name. (There is only one
20
+ # instance of each selection, and it's inherited from the class.)
21
+ # 2. Selections never have a directory. Selections are virtual,
22
+ # pseudo-collections that paste together entries across collections.
23
+ #
24
+ # Thus, the generator method here takes no arguments.
25
+ #
26
+ # @return [Lifer::Selection]
27
+ def generate
28
+ new(name: name, directory: nil)
29
+ end
30
+
31
+ private
32
+
33
+ # @private
34
+ # This callback is invoked whenever a subclass of the current class is
35
+ # created. It ensures that each subclass, at least, as a default name that
36
+ # isn't `nil`.
37
+ #
38
+ # @return [void]
39
+ def inherited(klass)
40
+ klass.name ||= :unnamed_selection
41
+ end
42
+ end
43
+
44
+ # The `#entries` method should be implemented on every selection subclass.
45
+ #
46
+ # @raise [NotImplementedError]
47
+ def entries
48
+ raise NotImplementedError, I18n.t("selection.entries_not_implemented")
49
+ end
50
+
51
+ # FIXME:
52
+ # Getting selection settings may actually need to be different than getting
53
+ # collection settings. But for now let's just inherit the superclass method.
54
+ #
55
+ # A getter for selection settings. See `Lifer::Collection#setting` for more
56
+ # information.
57
+ #
58
+ # @return [String, Symbol, NilClass] The setting for the collection (or a
59
+ # fallback setting, or a default setting).
60
+ def setting(...)
61
+ super(...)
62
+ end
63
+
64
+ private
65
+
66
+ # @private
67
+ # Selections do not support layout files. Entries only have permalinks and
68
+ # layouts via their collections. If something were ever to ask for the layout
69
+ # file of a selection, it would be wrong to. So we would raise an error.
70
+ #
71
+ def layout_file
72
+ raise I18n.t("selection.layouts_not_allowed")
73
+ end
74
+
75
+ self.name = :selection
76
+ end
77
+
78
+ require_relative "selection/all_markdown"
79
+ require_relative "selection/included_in_feeds"
@@ -0,0 +1,35 @@
1
+ # This module provides simple finder methods to classes that need to keep track
2
+ # of their descendant classes.
3
+ #
4
+ # Example usage:
5
+ #
6
+ # class MyClass
7
+ # included Lifer::Shared::FinderMethods
8
+ # # ...
9
+ # end
10
+ #
11
+ module Lifer::Shared::FinderMethods
12
+ # @!visibility private
13
+ def self.included(klass)
14
+ klass.extend ClassMethods
15
+ end
16
+
17
+ # This module contains the class methods to be included in other classes.
18
+ #
19
+ module ClassMethods
20
+ # A simple finder.
21
+ #
22
+ # @param name [string] The configured name of the builder you want to find.
23
+ # @return [Class] A builder class.
24
+ def find(name)
25
+ result = subclasses.detect { |klass| klass.name == name.to_sym }
26
+
27
+ if result.nil?
28
+ raise StandardError, I18n.t("shared.finder_methods.unknown_class", name:)
29
+ return
30
+ end
31
+
32
+ result
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ # We prefer to put any shared modules within the `Lifer::Shared` module.
2
+ #
3
+ module Lifer::Shared
4
+ end
5
+
6
+ require_relative "shared/finder_methods"
@@ -0,0 +1,10 @@
1
+ <%= I18n.t "cli.banner.description", program_name: "Lifer" %>
2
+ <%= I18n.t "cli.banner.usage" %>:
3
+ lifer [subcommand] [options]
4
+
5
+ <%= I18n.t "cli.banner.subcommands" %>:
6
+ <%= Lifer::CLI::SUBCOMMANDS
7
+ .map { [Lifer::Utilities.bold_text(_1), _2].join(": ") }
8
+ .join("\n ") %>
9
+
10
+ <%= I18n.t "cli.banner.options" %>:
@@ -0,0 +1,77 @@
1
+ # This file provides the default configuration settings for Lifer.
2
+
3
+ title: My Lifer Weblog
4
+ description: Just another Lifer weblog, lol...
5
+ language: en
6
+ author: Admin
7
+
8
+ entries:
9
+ default_title: Untitled Entry
10
+
11
+ rss: false
12
+ uri_strategy: simple
13
+
14
+ # You can optionally set a path to the layout file. If this setting is left
15
+ # unset, Lifer will use its built-in, simple-but-usable layout file instead.
16
+ #
17
+ ### layout_file: path/to/my_layout.html.erb
18
+
19
+ # Collections
20
+ #
21
+ # In addition to the root collection configured above, your configuration file
22
+ # can include any number of collections. Collections use the root collections
23
+ # configuration by default, or they can have their own values.
24
+ #
25
+ ### my_collection:
26
+ ### title: My Collection
27
+ ### description: A collection separate from the root collection.
28
+ ### language: fr
29
+ ### author: Benjamin
30
+ ###
31
+ ### rss: my-collection.xml
32
+ ### uri_strategy: pretty
33
+ ###
34
+ ### layout_file: path/to/my_other_layout.html.erb
35
+
36
+ # Selections
37
+ #
38
+ # Selections are pseudo-collections of entries. You may want to group disparate
39
+ # entries together across many collections. Selections is a way to do that.
40
+ # Lifer includes some basic selections but you can also create your own by
41
+ # writing a simple Ruby class. See the `Lifer::Selection` class documentation for
42
+ # more information.
43
+ #
44
+ selections:
45
+ - lifer/selection/all_markdown
46
+ - lifer/selection/included_in_feeds
47
+
48
+ # Global settings
49
+ #
50
+ # These settings are special, and they're used for all of your collections and
51
+ # cannot be set per collection.
52
+ #
53
+ # Note that the `build:` and `prebuild:` keys can be configured to work
54
+ # differently per environment (`build` or `serve`):
55
+ #
56
+ # Valid:
57
+ #
58
+ # # global:
59
+ # # prebuild:
60
+ # # serve:
61
+ # # - watch_and_rebuild_assets_command
62
+ # # build:
63
+ # # - final_minified_build_assets_command
64
+ #
65
+ # Also valid:
66
+ #
67
+ # # global:
68
+ # # prebuild:
69
+ # # - final_minified_build_assets_command
70
+ #
71
+ global:
72
+ build:
73
+ - html
74
+ - rss
75
+ - txt
76
+ host: https://example.com
77
+ output_directory: _build
@@ -2,6 +2,6 @@
2
2
  <head>
3
3
  </head>
4
4
  <body>
5
- <%= yield %>
5
+ <%= content %>
6
6
  </body>
7
7
  </html>
@@ -1,13 +1,21 @@
1
- class Lifer::URIStrategy::Pretty < Lifer::URIStrategy::Base
2
- def name
3
- "pretty"
4
- end
1
+ # The pretty URI strategy ensures that all entries are indexified, making their
2
+ # URLs "pretty" in the browser. "Pretty" because browsers often do not show
3
+ # "index.html" at the end of the URL because it's implicit.
4
+ #
5
+ # For example:
6
+ #
7
+ # entry.md ---> entry/index.html
8
+ #
9
+ class Lifer::URIStrategy::Pretty < Lifer::URIStrategy
10
+ self.name = :pretty
5
11
 
12
+ # @see Lifer::URIStrategy#output_file
6
13
  def output_file(entry)
7
- basename = File.basename(entry.file, ".*")
14
+ basename = File.basename entry.file,
15
+ Lifer::Utilities.file_extension(entry.file)
8
16
 
9
17
  Pathname entry.file.to_s
10
18
  .gsub(/#{root}[\/]{0,1}/, "")
11
- .gsub(/#{basename}(\..+)/, "#{basename}/index.html")
19
+ .gsub(/#{basename}(\..+)/, "#{basename}/index.#{file_extension(entry)}")
12
20
  end
13
21
  end
@@ -0,0 +1,24 @@
1
+ class Lifer::URIStrategy
2
+ # This URI strategy follows the "pretty" strategy of indexifying all entries
3
+ # (i.e. `entry.md` outputs to `entry/index.html`) and ensuring that, no matter
4
+ # what collection the entry is in, the entry is output to the root of the Lifer
5
+ # build directory. For example:
6
+ #
7
+ # subdir/entry.md ---> entry/index.html
8
+ #
9
+ class PrettyRoot < Lifer::URIStrategy
10
+ self.name = :pretty_root
11
+
12
+ # @see Lifer::URIStrategy#output_file
13
+ def output_file(entry)
14
+ basename = File.basename entry.file,
15
+ Lifer::Utilities.file_extension(entry.file)
16
+
17
+ if basename == "index"
18
+ Pathname "index.html"
19
+ else
20
+ Pathname "#{basename}/index.#{file_extension(entry)}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # This URI strategy ensures that entries with dates in the filename (i.e.
2
+ # `1990-12-12-my-trip-to-hamilton.md`) will always be have output filenames
3
+ # without the date included. It also follows the "pretty" URI strategy of
4
+ # outputting each file to an `index` file in a subdirectory.
5
+ #
6
+ # For example:
7
+ #
8
+ # 1990-12-12-my-trip-to-hamilton.md ---> my-trip-to-hamilton/index.html
9
+ #
10
+ class Lifer::URIStrategy::PrettyYYYYMMDD < Lifer::URIStrategy
11
+ self.name = :pretty_yyyy_mm_dd
12
+
13
+ # We expect date separators to fall into this regular expression.
14
+ #
15
+ DATE_SEPARATORS = "[\-\._]{1}"
16
+
17
+ # The date regular expression we expect entry filenames to follow.
18
+ #
19
+ DATE_REGEXP =
20
+ /\d{4}#{DATE_SEPARATORS}\d{1,2}#{DATE_SEPARATORS}\d{1,2}#{DATE_SEPARATORS}/
21
+
22
+ # @see Lifer::URIStrategy#output_file
23
+ def output_file(entry)
24
+ basename = File.basename entry.file,
25
+ Lifer::Utilities.file_extension(entry.file)
26
+
27
+ Pathname entry.file.to_s
28
+ .gsub(/#{root}[\/]{0,1}/, "")
29
+ .gsub(/#{basename}(\..+)/, "#{basename}/index.#{file_extension(entry)}")
30
+ .gsub(DATE_REGEXP, "")
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ class Lifer::URIStrategy
2
+ # No matter what collection an entry is in, this URI strategy ensures that the
3
+ # entries are output to the root of the output directory (for example:
4
+ # `_build/<your-entry>.html`, never `_build/collection_name/<your-entry>.html`.
5
+ #
6
+ class Root < Lifer::URIStrategy
7
+ self.name = :root
8
+
9
+ # @see Lifer::URIStrategy#output_file
10
+ def output_file(entry)
11
+ basename = File.basename entry.file,
12
+ Lifer::Utilities.file_extension(entry.file)
13
+
14
+ Pathname "#{basename}.#{file_extension(entry)}"
15
+ end
16
+ end
17
+ end
@@ -1,13 +1,17 @@
1
- class Lifer::URIStrategy::Simple < Lifer::URIStrategy::Base
2
- def name
3
- "simple"
4
- end
1
+ # The default URI strategy. It simply takes an input filename (i.e. "entry.md")
2
+ # and outputs a mirrorring output filename with the correct output format (i.e.
3
+ # "entry.html").
4
+ #
5
+ class Lifer::URIStrategy::Simple < Lifer::URIStrategy
6
+ self.name = :simple
5
7
 
8
+ # @see Lifer::URIStrategy#output_file
6
9
  def output_file(entry)
7
- basename = File.basename(entry.file, ".*")
10
+ basename = File.basename entry.file,
11
+ Lifer::Utilities.file_extension(entry.file)
8
12
 
9
13
  Pathname entry.file.to_s
10
14
  .gsub(/#{root}[\/]{0,1}/, "")
11
- .gsub(/#{basename}(\..+)/, "#{basename}.html")
15
+ .gsub(/#{basename}(\..+)/, "#{basename}.#{file_extension(entry)}")
12
16
  end
13
17
  end