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
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