lifer 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71407123cb005d99c9ffc1cde0c263921d0c45c22936c95b0ccced6bc189cb54
4
- data.tar.gz: 2b4a5ba58c99283e8df136e492c653bd04761c0b07ea643e7de7ed00216bfb37
3
+ metadata.gz: 9e8681438caf9356526a0af22c91ec3470c3ccba625c7d6602adab532d89a4b2
4
+ data.tar.gz: 9c2eefb99f760d50aac163f2dff6e0d06d3710d2a0682426ff45d75733be3640
5
5
  SHA512:
6
- metadata.gz: 4dfed0a714043b38b861c0dbdae8f12d22c0b3fb4dda11f494dfed56cb346e6abcdfb87ce270dd9b17b2259751ef99b05516eee039c933b2d4b53c45897c9456
7
- data.tar.gz: 42a12372430de5c6a2e96ddeb211b82e9c82fde78be316d4cc4b5fbbdfaa87877fce7940e32dfb49bbc69ceb0e64a4b57f228a6a53ba536c017e3c92aa06a83e
6
+ metadata.gz: e4411e369a5fab2ec20c0a9c6835279ecc3b0a9c8d522c8ad9b2f5e06223b60ccd3604f576bc9a06f051a6a6e1d33d2990e6e601b1046488ef6812c9ca27182b
7
+ data.tar.gz: 45697944e8bb0c378083db6b4ec2c5fe0df67c70a51a5dad87cb6d57fade3f58a98d5dbc07086bd231126f49802c47930490febc85aa1b313bbe372b1a70e13c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## Next
2
2
 
3
+ ## v0.13.0
4
+
5
+ This release allows projects to build JSON Feed 1.1. Now, a project can
6
+ build both Atom (or RSS 2.0) and JSON Feed, which is nice. In order to
7
+ support JSON Feed, we modelled out `Lifer::Asset` and `Lifer::Author`. This
8
+ is because JSON Feed explicitly supports author objects (with name, URL,
9
+ and avatars URLs), and images and banner images per feed item.
10
+
11
+ This release also improves the documentation for the `Lifer::Builder::RSS`.
12
+
13
+ ## v0.12.4
14
+
15
+ This release resolves two issues:
16
+
17
+ 1. An issue where reading entry frontmatter (metadata within a `---` cage)
18
+ wasn't working properly when other `---` strings appeared later on in the
19
+ entry. We just needed to make the `Lifer::FRONTMATTER_REGEX` less greedy.
20
+ 2. It allows the development server to serve `.asc` files. :-)
21
+
3
22
  ## v0.12.3
4
23
 
5
24
  This release just resolves some issues I was having with the platform in
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lifer (0.12.3)
4
+ lifer (0.13.0)
5
5
  base64
6
6
  i18n (< 2)
7
7
  kramdown (~> 2.4)
data/guix.scm ADDED
@@ -0,0 +1,19 @@
1
+ (use-modules (guix)
2
+ (guix build-system gnu)
3
+ ((guix licenses) #:prefix license:)
4
+ (gnu packages ruby)
5
+ (gnu packages serialization))
6
+
7
+ (package
8
+ (name "ruby-lifer-dev")
9
+ (version "0.12.4-git")
10
+ (source #f)
11
+ (build-system gnu-build-system)
12
+ (inputs
13
+ (append (list ruby libyaml)))
14
+ (synopsis "Another Ruby-based static website generator")
15
+ (description
16
+ "A ruby-based static website generator focused on extensibility and having
17
+ few dependencies.")
18
+ (home-page "https://github.com/benjaminwil/lifer")
19
+ (license license:expat))
@@ -0,0 +1,82 @@
1
+ class Lifer::Asset
2
+ # An asset is a file, often a multimedia file, that belongs to one or
3
+ # many entries. Right now, assets don't have much functionality of their
4
+ # own, but it's valuable to know what entries an asset has a relationship
5
+ # with. In the future, it may be valuable for us to build out functionality
6
+ # that allows users to have one or more asset hosts. This allows files that
7
+ # aren't checked into version control still be treated as entry dependencies.
8
+ #
9
+ class << self
10
+ # Builds or updates a Lifer asset. On update, this list of an asset's
11
+ # entries would get freshened.
12
+ #
13
+ # @param url [String] An absolute URL or path relative to the host root.
14
+ # @param entries [Array<Lifer::Entry>] An array of entries that the asset
15
+ # belongs to.
16
+ # @return [Lifer::Asset] The new or updated asset.
17
+ def build_or_update(url: nil, entries: [])
18
+ update(url:, entries:) || build(url:, entries:)
19
+ end
20
+
21
+ # The default host for all assets.
22
+ #
23
+ # @return [String] A URL to a host. (Default: the configured global host.)
24
+ def default_host = Lifer.setting(:global, :host)
25
+
26
+ private
27
+
28
+ def build(url:, entries:)
29
+ if (new_asset = new(url:, entries:))
30
+ Lifer.asset_manifest << new_asset
31
+ end
32
+ new_asset || false
33
+ end
34
+
35
+ def update(url:, entries:)
36
+ normalized_url = Lifer::Utilities.uri_from url,
37
+ host: default_host,
38
+ object_type: self
39
+
40
+ if (asset = Lifer.asset_manifest.detect { _1.url == normalized_url })
41
+ asset.instance_variable_set :@entries,
42
+ (asset.instance_variable_get(:@entries) | entries)
43
+ end
44
+ asset || false
45
+ end
46
+
47
+ end
48
+
49
+ attr_reader :url, :entries
50
+
51
+ def initialize(url:, entries:)
52
+ normalized_url = Lifer::Utilities.uri_from url,
53
+ host: self.class.default_host,
54
+ object_type: self
55
+
56
+ @url = normalized_url
57
+ @entries = entries
58
+ end
59
+
60
+ # Checks whether a given URL matches the current asset's URL.
61
+ #
62
+ # @param url [String] A URL.
63
+ # @param host [String] The host URL. (Default: The configured global host
64
+ # URL.)
65
+ # @return [boolean] Whether the given URL matches the object's URL.
66
+ def match?(url:, host: self.class.default_host)
67
+ @url == Lifer::Utilities.uri_from(url, host:, object_type: self.class)
68
+ end
69
+
70
+ # Gets the current URL. If given a host, the asset's true host will be
71
+ # replaced with the given host.
72
+ #
73
+ # @param host [String] A host URL. (Default: The configured global host URL.)
74
+ # @return [String] The URL to the current asset.
75
+ def url(host: self.class.default_host)
76
+ return @url if host == self.class.default_host
77
+
78
+ path = URI(@url).path
79
+
80
+ Lifer::Utilities.uri_from(path, host:, object_type: self.class)
81
+ end
82
+ end
@@ -0,0 +1,106 @@
1
+ module Lifer
2
+ # An author is a representation of a unique author of entries in the current
3
+ # project. This allows us to refer to an author's name in an entry's
4
+ # frontmatter, i.e.:
5
+ #
6
+ # ---
7
+ # title: My blog post
8
+ # author: Nat McCartney
9
+ # ---
10
+ #
11
+ # And let that reference load in a bunch of other metadata about the
12
+ # author. While it's possible to add all of this metadata at the entry level,
13
+ # the first entry loaded will be the source of truth for every reference
14
+ # back to the author. So it's preferrable to set the author metadata in your
15
+ # global configuration:
16
+ #
17
+ # # my-lifer.conf
18
+ # authors:
19
+ # - name: Nat McCartney
20
+ # url: https://example.com/nat
21
+ # avatar: https://example.com/nat.png
22
+ #
23
+ # Within a Lifer project, this allows you to do powerful things like get
24
+ # a list of entries by a unique author (or set of authors). It also allows
25
+ # you to provide author-specific URLs and avatar images in your website's
26
+ # JSON Feeds.
27
+ #
28
+ # The author's name is used as the primary identifier. We have tried to be
29
+ # smart about this so that, for example, "Nat McCartney", "nat mccartney",
30
+ # and "nat-mccartney" will all load up the same author object.
31
+ #
32
+ class Author
33
+ class << self
34
+ # Builds or updates a Lifer author. On update, the list of an author's
35
+ # entries would get freshened.
36
+ #
37
+ # @param name [String] The name of the author.
38
+ # @param url [String] A relative or absolute URL to learn more about
39
+ # the author at.
40
+ # @param avatar [String] A relative or absolute URL to an image that
41
+ # represents the author.
42
+ # @return [Lifer::Author] The new or updated author.
43
+ def build_or_update(name:, url: nil, avatar: nil, entries: [])
44
+ update(name:, url:, avatar:, entries:) ||
45
+ build(name:, url:, avatar:, entries:)
46
+ end
47
+
48
+ private
49
+
50
+ def build(name:, url:, avatar:, entries:)
51
+ if (new_author = new(name:, url:, avatar:, entries:))
52
+ Lifer.author_manifest << new_author
53
+ end
54
+ new_author || false
55
+ end
56
+
57
+ def update(name:, url:, avatar:, entries:)
58
+ author_id = Lifer::Utilities.handleize(name)
59
+
60
+ if (author = Lifer.authors.detect { _1.id == author_id })
61
+ author.instance_variable_set :@entries,
62
+ (author.instance_variable_get(:@entries) | entries)
63
+ end
64
+ author || false
65
+ end
66
+ end
67
+
68
+ attr_reader :name, :entries
69
+
70
+ def initialize(name:, url:, avatar:, entries:)
71
+ @name = name
72
+ @url = url
73
+ @avatar = avatar
74
+ @entries = entries
75
+ end
76
+
77
+ # An avatar image URL that represents the author. The URL can either be
78
+ # relative from the website's root or an absolute URL. If the relative or
79
+ # absolute URL is ambiguous, it is sanitized and this method returns nil.
80
+ #
81
+ # @param host [String] The host to prefix to relative URLs. By default,
82
+ # this is the Lifer project's global host.
83
+ # @return [String] The absolute URL to the avatar image.
84
+ def avatar(host: Lifer.setting(:global, :host))
85
+ Lifer::Utilities.uri_from(@avatar, host:, object_type: self.class)
86
+ end
87
+
88
+ # An identifier built from the author's name. This uses our generic
89
+ # handle-izer function. So a name like "Nat McCartney" becomes
90
+ # "nat-mccartney".
91
+ #
92
+ # @return [String] The identifier for the author.
93
+ def id = (@id ||= Lifer::Utilities.handleize(name))
94
+
95
+ # A URL that provides more info about the author. The URL can either be
96
+ # relative from the website's root or an absolute URL. If the relative or
97
+ # absolute URL is ambiguous, it is sanitized and this method returns nil.
98
+ #
99
+ # @param host [String] The host to prefix to relative URLs. By default,
100
+ # this is the Lifer project's global host.
101
+ # @return [String] The absolute version of the URL.
102
+ def url(host: Lifer.setting(:global, :host))
103
+ Lifer::Utilities.uri_from(@url, host:, object_type: self.class)
104
+ end
105
+ end
106
+ end
data/lib/lifer/brain.rb CHANGED
@@ -25,6 +25,27 @@ class Lifer::Brain
25
25
  def init(root: Dir.pwd, config_file: nil) = new(root:, config_file:)
26
26
  end
27
27
 
28
+ # The asset manifest tracks the unique assets added to the project as they
29
+ # are added. The writer methods for this instanc evariable is used internally
30
+ # by Lifer when adding new assets.
31
+ #
32
+ # @return [Set<Lifer::Asset>]
33
+ def asset_manifest = (@asset_manifest ||= Set.new)
34
+
35
+ # Given the author manifest, this returns an array of all authors for the
36
+ # current project. This method is preferrable for accessing and querying for
37
+ # authors.
38
+ #
39
+ # @return [Array<Lifer::Author>]
40
+ def authors = author_manifest.to_a
41
+
42
+ # The author manifest tracks the unique authors added to the project as
43
+ # they are added. The writer method for this instance variable is used
44
+ # internally by Lifer when adding new authors.
45
+ #
46
+ # @return [Set<Lifer::Author>]
47
+ def author_manifest = (@author_manifest ||= Set.new)
48
+
28
49
  # Destroy any existing build output and then build the Lifer project with all
29
50
  # configured `Lifer::Builder`s.
30
51
  #
@@ -57,7 +57,7 @@ class Lifer::Builder
57
57
  return cached_value if cached_value
58
58
 
59
59
  contents = File.read layout_file
60
- contents = contents.gsub(Lifer::FRONTMATTER_REGEX, "") unless raw
60
+ contents = contents.sub(Lifer::FRONTMATTER_REGEX, "") unless raw
61
61
 
62
62
  instance_variable_set cache_variable, contents
63
63
  end
@@ -0,0 +1,33 @@
1
+ module Lifer::Builder::HTML::FromLiquid::Drops
2
+ class AuthorDrop < Liquid::Drop
3
+ attr_accessor :lifer_author
4
+
5
+ def initialize(lifer_author) = (@lifer_author = lifer_author)
6
+
7
+ def avatar = (@avatar ||= lifer_author.avatar)
8
+
9
+ def name = (@name ||= lifer_author.name)
10
+
11
+ def url = (@url ||= lifer_author.url)
12
+
13
+ def entries
14
+ @entries ||= lifer_author.entries.map {
15
+ EntryDrop.new _1, collection: _1.collection, tags: _1.tags
16
+ }
17
+ end
18
+ end
19
+
20
+ class AuthorsDrop < Liquid::Drop
21
+ attr_accessor :authors
22
+
23
+ def initialize(lifer_authors)
24
+ @authors = lifer_authors.map { AuthorDrop.new _1 }
25
+ end
26
+
27
+ def each(&block)
28
+ authors.each(&block)
29
+ end
30
+
31
+ def to_a = @authors
32
+ end
33
+ end
@@ -25,7 +25,9 @@ module Lifer::Builder::HTML::FromLiquid::Drops
25
25
  # The entry authors (or author).
26
26
  #
27
27
  # @return [String]
28
- def authors = (@authors ||= lifer_entry.authors.join(", "))
28
+ def authors
29
+ @authors ||= AuthorsDrop.new(lifer_entry.authors)
30
+ end
29
31
 
30
32
  # The entry content.
31
33
  #
@@ -8,6 +8,7 @@ class Lifer::Builder::HTML::FromLiquid
8
8
  module Drops; end
9
9
  end
10
10
 
11
+ require_relative "drops/authors_drop"
11
12
  require_relative "drops/collection_drop"
12
13
  require_relative "drops/collections_drop"
13
14
  require_relative "drops/entry_drop"
@@ -0,0 +1,214 @@
1
+ require "fileutils"
2
+
3
+ class Lifer::Builder
4
+ # This builds JSON feed compliant with the JSON Feed 1.1
5
+ # specification[1]. Note that we don't currently support *all* of the features
6
+ # of JSON Feed, but it shouldn't be too hard to add them.
7
+ #
8
+ # The JSON Feed builder can be configured in a number of ways:
9
+ #
10
+ # 1. Boolean
11
+ #
12
+ # Simply set `json_feed: true` or `json_feed: false` to enable or disable
13
+ # a feed for a collection. If `true`, a JSON feed will be build to
14
+ # `/name-of-collection.json` at the root of the Lifer output directory.
15
+ #
16
+ # 2. Simple
17
+ #
18
+ # Simply set `json_feed: name-of-output-file.json` to specify the name of
19
+ # the output JSON Feed file.
20
+ #
21
+ # 3. Fine-grained
22
+ #
23
+ # Provide an object under `json_feed:` for more fine-grained control over
24
+ # configuration. The following sub-settings are supported:
25
+ #
26
+ # - `authors:` A list of authors to fall back to if an entry does not
27
+ # have its own authors data. Each author can include the following fields:
28
+ # - `name:` The author's name.
29
+ # - `url:` A URL that represents the author.
30
+ # - `avatar:` The URL to an avatar that represents the author.
31
+ # - `content_format:` The format of the feed entries for the entire feed.
32
+ # Either `html` or `text`. (Default: `html`.)
33
+ # - `count:` - The limit of JSON Feed items that should be included in the
34
+ # output document. Leave unset or set to `0` to include all entries.
35
+ # - `expired:` Set the expired flag on the feed to broadcast whether
36
+ # the feed will continue to be updated.
37
+ # - `home_page_url:` A URL that represents the home page of the current
38
+ # feed.
39
+ # - `url:` The path to the filename of the output JSON Feed file.
40
+ #
41
+ # [1] https://www.jsonfeed.org/version/1.1/
42
+ #
43
+ class JSONFeed < Lifer::Builder
44
+ # As of this writing, we support the latest version of the JSON Feed
45
+ # specification.
46
+ #
47
+ JSON_FEED_VERSION = "1.1"
48
+
49
+ self.name = :json_feed
50
+ self.settings = [
51
+ json_feed: [
52
+ {authors: [:name, :url, :avatar]},
53
+ :content_format,
54
+ :count,
55
+ :expired,
56
+ :home_page_url,
57
+ :url
58
+ ]
59
+ ]
60
+
61
+ class << self
62
+ # Traverses and renders a JSON Feed for each JSON Feed-enabled, feedable
63
+ # collection in the configured output directory for the Lifer project.
64
+ #
65
+ # @param root [String] The Lifer root.
66
+ # @return [void]
67
+ def execute(root:)
68
+ Dir.chdir Lifer.output_directory do
69
+ new(root:).execute
70
+ end
71
+ end
72
+ end
73
+
74
+ # Traverses and renders a JSON Feed for JSON Feed-enabled feedable
75
+ # collections.
76
+ #
77
+ # @return [void]
78
+ def execute
79
+ collections_with_feeds.each do |collection|
80
+ next unless (filename = output_filename(collection))
81
+
82
+ FileUtils.mkdir_p File.dirname(filename)
83
+
84
+ File.open filename, "w" do |file|
85
+ file.puts(
86
+ json_feed_for(collection) do |current_feed|
87
+ max_index = max_feed_items(collection) - 1
88
+
89
+ collection.entries
90
+ .select { |entry| entry.feedable? }[0..max_index]
91
+ .each { |entry| json_feed_entry(current_feed, entry, collection) }
92
+ end
93
+ )
94
+ end
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ attr_reader :collections_with_feeds, :root
101
+
102
+ def initialize(root:)
103
+ @collections_with_feeds =
104
+ Lifer.collections.select { |collection| collection.setting :json_feed }
105
+ @root = root
106
+ end
107
+
108
+ # Builds an entry in the JSON Feed given the current JSON Feed being
109
+ # built, a Lifer entry, and a Lifer collection.
110
+ #
111
+ # @param feed_object [Hash] JSON Feed currently being built.
112
+ # @param lifer_entry [Lifer::Entry] The Lifer entry to build an entry for.
113
+ # @param lifer_collection [Lifer::Collection] The Lifer collection for
114
+ # the entry. This provides additional context that may be required
115
+ # for some parts of JSON Feed entry schema.
116
+ # @return [void] The entry is added to the JSON Feed as a side effect
117
+ # by the end of this procedure.
118
+ def json_feed_entry(feed_object, lifer_entry, lifer_collection)
119
+ feed_item_object = {
120
+ id: lifer_entry.permalink,
121
+ url: lifer_entry.permalink,
122
+ external_url: lifer_entry.frontmatter[:external_url],
123
+ title: lifer_entry.title,
124
+ summary: lifer_entry.summary,
125
+ image: lifer_entry.assets.detect { |asset|
126
+ asset.match?(
127
+ url: lifer_entry.frontmatter[:image] ||
128
+ lifer_entry.frontmatter[:images]&.first
129
+ )
130
+ }&.url,
131
+ banner_image: lifer_entry.assets.detect { |asset|
132
+ asset.match? url: lifer_entry.frontmatter[:banner_image]
133
+ }&.url,
134
+ date_published: lifer_entry.published_at,
135
+ date_modified: lifer_entry.updated_at(fallback: lifer_entry.published_at),
136
+ tags: lifer_entry.tags,
137
+ language: lifer_collection.setting(:language)
138
+ }
139
+
140
+ feed_content_format =
141
+ lifer_collection.setting(:json_feed, :content_format)&.to_sym || :html
142
+ case feed_content_format
143
+ when :html
144
+ feed_item_object[:content_html] = lifer_entry.to_html
145
+ when :text
146
+ # Currently, `Entry#to_html` is just the name of the method we use
147
+ # to get the entry content, regardless of whether the entry output is
148
+ # HTML or not.
149
+ feed_item_object[:content_text] = lifer_entry.to_html
150
+ end
151
+
152
+ if (authors = lifer_entry.authors).any?
153
+ feed_item_object[:authors] = authors.map { |author|
154
+ {
155
+ name: author.name,
156
+ avatar: author.avatar,
157
+ url: author.url
158
+ }.reject { |_key, value| value.nil? }
159
+ }
160
+ end
161
+
162
+ feed_object[:items] << feed_item_object
163
+ end
164
+
165
+ # Provides the entire feed object for serialization.
166
+ #
167
+ # Note that we don't currently support JSON Feed 1.1 *exhaustively*. For
168
+ # example, we don't currently support pagniation or hubs. Here's a list
169
+ # of root-level 1.1 fields we do not currently support:
170
+ #
171
+ # - authors
172
+ # - favicon
173
+ # - hubs
174
+ # - icon
175
+ # - next_url
176
+ # - user_comment
177
+ #
178
+ # @param collection [Lifer::Collection] The current Lifer collection for
179
+ # metadata context.
180
+ # @return [String] The JSON representation of the JSON Feed.
181
+ def json_feed_for(collection, &block)
182
+ feed_object = {
183
+ version: JSON_FEED_VERSION,
184
+ title: collection.setting(:title),
185
+ description: collection.setting(:description) || collection.setting(:site_title),
186
+ expired: collection.setting(:json_feed, :expired, strict: true),
187
+ feed_url: collection.setting(:json_feed, :url, strict: true),
188
+ home_page_url: collection.setting(:json_feed, :home_page_url, strict: true),
189
+ language: collection.setting(:language),
190
+ items: []
191
+ }
192
+ feed_object = feed_object.reject { |_key, value| value.nil? }
193
+
194
+ yield feed_object
195
+
196
+ feed_object.to_json
197
+ end
198
+
199
+ def max_feed_items(collection) = collection.setting(:json_feed, :count) || 0
200
+
201
+ def output_filename(collection)
202
+ strict = !collection.root?
203
+
204
+ case collection.setting(:json_feed, strict:)
205
+ when FalseClass, NilClass then nil
206
+ when TrueClass then File.join(Dir.pwd, "#{collection.name}.json")
207
+ when Hash
208
+ File.join Dir.pwd, collection.setting(:json_feed, :url, strict:)
209
+ when String
210
+ File.join Dir.pwd, collection.setting(:json_feed, strict:)
211
+ end
212
+ end
213
+ end
214
+ end
@@ -15,7 +15,8 @@ require "rss"
15
15
  # 1. Boolean
16
16
  #
17
17
  # Simply set `rss: true` or `rss: false` to enable or disable a feed for a
18
- # collection. If `true`, an RSS feed will be built to `name-of-collection.xml`
18
+ # collection. If `true`, an RSS feed will be built to
19
+ # `/name-of-collection.xml` at the root of the Lifer output directory.
19
20
  #
20
21
  # 2. Simple
21
22
  #
@@ -78,8 +79,8 @@ class Lifer::Builder::RSS < Lifer::Builder
78
79
  ]
79
80
 
80
81
  class << self
81
- # Traverses and renders an RSS feed for each feedable collection in the
82
- # configured output directory for the Lifer project.
82
+ # Traverses and renders an RSS feed for each RSS-enabled feedable
83
+ # collection in the configured output directory for the Lifer project.
83
84
  #
84
85
  # @param root [String] The Lifer root.
85
86
  # @return [void]
@@ -98,7 +99,7 @@ class Lifer::Builder::RSS < Lifer::Builder
98
99
  DEFAULT_MAKER_FORMAT_NAME
99
100
  end
100
101
 
101
- # Traverses and renders an RSS feed for feedable collection.
102
+ # Traverses and renders an RSS feed for RSS-enabled feedable collections.
102
103
  #
103
104
  # @return [void]
104
105
  def execute
data/lib/lifer/builder.rb CHANGED
@@ -101,4 +101,5 @@ end
101
101
 
102
102
  require_relative "builder/rss"
103
103
  require_relative "builder/html"
104
+ require_relative "builder/json_feed"
104
105
  require_relative "builder/txt"
@@ -89,6 +89,8 @@ class Lifer::Collection
89
89
  #
90
90
  # @param name [*Symbol] A list of symbols that map to a nested Lifer
91
91
  # setting (for the current collection).
92
+ # @param strict [boolean] Choose whether to strictly return the collection
93
+ # setting or to fallback to the Lifer root and default settings.
92
94
  # @return [String, Nil] The setting as set in the Lifer project's
93
95
  # configuration file.
94
96
  def setting(*name, strict: false)
data/lib/lifer/config.rb CHANGED
@@ -1,5 +1,3 @@
1
- require_relative "utilities"
2
-
3
1
  # This class is responsible for reading the Lifer configuration YAML file. This
4
2
  # file should provided by the user, but the Lifer Ruby gem does provide a default
5
3
  # file, as well.
@@ -48,6 +48,7 @@ module Lifer::Dev
48
48
  case File.extname(path)
49
49
  when ".aac" then "audio/aac"
50
50
  when ".apng" then "image/apng"
51
+ when ".asc" then "application/pgp-signature"
51
52
  when ".avi" then "video/x-msvideo"
52
53
  when ".avif" then "image/avif"
53
54
  when ".bin" then "application/octet-stream"
@@ -2,8 +2,6 @@ require "date"
2
2
  require "kramdown"
3
3
  require "time"
4
4
 
5
- require_relative "../utilities"
6
-
7
5
  # We should initialize each Markdown file in a Lifer project as a
8
6
  # `Lifer::Entry::Markdown` object. This class contains convenience methods for
9
7
  # parsing a Markdown file with frontmatter as a weblog post or article. Of
data/lib/lifer/entry.rb CHANGED
@@ -29,6 +29,11 @@ module Lifer
29
29
  require_relative "entry/markdown"
30
30
  require_relative "entry/txt"
31
31
 
32
+ # If assets are represented in YAML frontmatter as a string, they're split on
33
+ # commas and/or spaces.
34
+ #
35
+ ASSET_DELIMITER_REGEX = /[,\s]+/
36
+
32
37
  # We provide a default date for entries that have no date and entry types that
33
38
  # otherwise could not have a date due to no real way of getting that metadata.
34
39
  #
@@ -58,12 +63,15 @@ module Lifer
58
63
  # @param file [String] The absolute filename of an entry file.
59
64
  # @param collection [Lifer::Collection] The collection for the entry.
60
65
  # @return [Lifer::Entry] An entry.
61
- def generate(file:, collection:)
66
+ def generate(file:, collection:, dependencies: [:assets, :authors, :tags])
62
67
  error!(file) unless File.exist?(file)
63
68
 
64
69
  if (new_entry = subclass_for(file)&.new(file:, collection:))
65
70
  Lifer.entry_manifest << new_entry
66
- new_entry.tags
71
+
72
+ dependencies.each do |dependency|
73
+ new_entry.public_send dependency
74
+ end
67
75
  end
68
76
 
69
77
  new_entry
@@ -124,6 +132,14 @@ module Lifer
124
132
  @collection = collection
125
133
  end
126
134
 
135
+ # Locates and returns all assets defined in the entry.
136
+ #
137
+ # @return [Array<Lifer::Asset>] The entry's assets.
138
+ def assets
139
+ @assets ||= candidate_asset_names
140
+ .map { Lifer::Asset.build_or_update(url: _1, entries: [self]) }
141
+ end
142
+
127
143
  # Given the entry's frontmatter, we should be able to get a list of authors.
128
144
  # We always prefer authors (as opposed to a singular author) because it makes
129
145
  # handling both cases easier in the long run.
@@ -133,7 +149,20 @@ module Lifer
133
149
  #
134
150
  # @return [Array<String>] An array of authors's names.
135
151
  def authors
136
- Array(frontmatter[:author] || frontmatter[:authors]).compact
152
+ list = Array(frontmatter[:author] || frontmatter[:authors]).compact
153
+ if list.any? && list.all? { _1.is_a? Array }
154
+ list = [list.to_h]
155
+ end
156
+
157
+ list.map {
158
+ attributes = Lifer::Utilities.symbolize_keys(
159
+ case _1
160
+ when Hash then _1
161
+ when String then {name: _1}
162
+ end
163
+ )
164
+ Lifer::Author.build_or_update **attributes.merge(entries: [self])
165
+ }
137
166
  end
138
167
 
139
168
  # This method returns the full text of the entry, only removing the
@@ -143,7 +172,7 @@ module Lifer
143
172
  def body
144
173
  return full_text.strip unless frontmatter?
145
174
 
146
- full_text.gsub(Lifer::FRONTMATTER_REGEX, "").strip
175
+ full_text.sub(Lifer::FRONTMATTER_REGEX, "").strip
147
176
  end
148
177
 
149
178
  def feedable?
@@ -284,6 +313,21 @@ module Lifer
284
313
 
285
314
  private
286
315
 
316
+ # Similar to tags, users may represent assets as a string or a list of
317
+ # strings instead of YAML-style arrays. So let's parse for all of them.
318
+ #
319
+ # @return [Array<String>] An array of candidate asset URLs.
320
+ def candidate_asset_names
321
+ candidate_frontmatter_fields = [:banner_image, :image, :images]
322
+ candidate_frontmatter_fields.flat_map {
323
+ case frontmatter[_1]
324
+ when Array then frontmatter[_1].map(&:to_s)
325
+ when String then frontmatter[_1].split(ASSET_DELIMITER_REGEX)
326
+ else []
327
+ end.uniq
328
+ }.compact
329
+ end
330
+
287
331
  # It is conventional for users to use spaces or commas to delimit tags in
288
332
  # other systems, so let's support that. But let's also support YAML-style
289
333
  # arrays.
@@ -8,6 +8,7 @@ author: Admin
8
8
  entries:
9
9
  default_title: Untitled Entry
10
10
 
11
+ json_feed: false
11
12
  rss: false
12
13
  uri_strategy: simple
13
14
 
@@ -33,12 +34,24 @@ uri_strategy: simple
33
34
  ###
34
35
  ### layout_file: path/to/my_other_layout.html.erb
35
36
  ###
36
- ### my_with_fine_grained_rss_settings:
37
- ### rss
37
+ ### my_collection_with_fine_grained_rss_settings:
38
+ ### json_feed:
39
+ ### authors:
40
+ ### - name: Hal Human
41
+ ### avatar: /images/avatars/hal.png
42
+ ### url: https://hals-website.local
43
+ ### content_format: html
44
+ ### count: 99
45
+ ### expired: false
46
+ ### home_page_url: https://example.com/my-collection
47
+ ### url: /location-of-the-feed.json
48
+ ###
49
+ ### rss:
38
50
  ### count: 99
39
51
  ### format: rss
40
52
  ### managing_editor: editor@example.com (Managing Editor)
41
53
  ### url: custom.xml
54
+ ###
42
55
 
43
56
  # Selections
44
57
  #
@@ -4,6 +4,8 @@ require "parallel"
4
4
  # Ensure that these are actually useful globally, though. :-)
5
5
  #
6
6
  module Lifer::Utilities
7
+ class AmbiguousURIError < StandardError; end
8
+
7
9
  class << self
8
10
  # Output a string using bold escape sequences to the output TTY text.
9
11
  #
@@ -129,6 +131,23 @@ module Lifer::Utilities
129
131
  symbolized_hash
130
132
  end
131
133
 
134
+ def uri_from(string, host:, object_type:)
135
+ uri = string && URI.parse(string.strip)
136
+
137
+ if uri && uri.relative? && uri.to_s.start_with?("/")
138
+ "%s%s" % [host, uri]
139
+ elsif uri && uri.relative? && !uri.to_s.start_with?("/")
140
+ raise AmbiguousURIError
141
+ elsif uri&.absolute?
142
+ uri.to_s
143
+ end
144
+ rescue AmbiguousURIError
145
+ Lifer::Message.error "utilities.ambiguous_uri_error",
146
+ object_type: object_type.inspect,
147
+ uri: uri&.to_s
148
+ nil
149
+ end
150
+
132
151
  private
133
152
 
134
153
  def camelize(string)
data/lib/lifer/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Lifer
2
- VERSION = "0.12.3"
2
+ VERSION = "0.13.0"
3
3
  end
data/lib/lifer.rb CHANGED
@@ -26,9 +26,25 @@ module Lifer
26
26
 
27
27
  # We expect frontmatter in any file to be provided in the following format.
28
28
  #
29
- FRONTMATTER_REGEX = /^---\n(.*)---\n/m
29
+ FRONTMATTER_REGEX = /^---\n(.*?)---\n/m
30
30
 
31
31
  class << self
32
+ # All of the assets represented in Lifer entries for the current project.
33
+ #
34
+ # @return [Array<Lifer::Asset>] The complete list of assets.
35
+ def asset_manifest = brain.asset_manifest
36
+
37
+ # All of the authors represented in Lifer entries for the current project.
38
+ #
39
+ # @return [Array<Lifer::Author>] The complete list of authors.
40
+ def authors = brain.authors
41
+
42
+ # A set of all authors added to the project. Prefer using the `#authors`
43
+ # method for author queries.
44
+ #
45
+ # @return [Set<Lifer::Author>] The complete set of author.
46
+ def author_manifest = brain.author_manifest
47
+
32
48
  # The first time `Lifer.brain` is referenced, we build a new `Lifer::Brain`
33
49
  # object that is used and reused until the current process has ended.
34
50
  #
@@ -173,11 +189,14 @@ require "i18n"
173
189
  I18n.load_path += Dir["%s/locales/*.yml" % Lifer.gem_root]
174
190
  I18n.available_locales = [:en]
175
191
 
176
- # `Lifer::Shared` contains modules that that may or may not be included on other
177
- # classes required below.
192
+ # `Lifer::Shared` and `Lifer::Utilities` contain modules and methods that
193
+ # are used by the main models required below.
178
194
  #
179
195
  require_relative "lifer/shared"
196
+ require_relative "lifer/utilities"
180
197
 
198
+ require_relative "lifer/asset"
199
+ require_relative "lifer/author"
181
200
  require_relative "lifer/brain"
182
201
  require_relative "lifer/builder"
183
202
  require_relative "lifer/collection"
data/locales/en.yml CHANGED
@@ -49,5 +49,6 @@ en:
49
49
  finder_methods:
50
50
  unknown_class: no class with name "%{name}"
51
51
  utilities:
52
+ ambiguous_uri_error: "An %{object_type} contains an ambiguous URI: \"%{uri}\". Use an absolute URI or relative from the project root instead."
52
53
  classify_error: >
53
54
  could not find constant for path "%{string_constant}" (%{camel_cased_string_constant})
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lifer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.3
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - benjamin wil
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-03 00:00:00.000000000 Z
11
+ date: 2026-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -169,7 +169,10 @@ files:
169
169
  - bin/console
170
170
  - bin/lifer
171
171
  - bin/setup
172
+ - guix.scm
172
173
  - lib/lifer.rb
174
+ - lib/lifer/asset.rb
175
+ - lib/lifer/author.rb
173
176
  - lib/lifer/brain.rb
174
177
  - lib/lifer/builder.rb
175
178
  - lib/lifer/builder/html.rb
@@ -177,6 +180,7 @@ files:
177
180
  - lib/lifer/builder/html/from_erb.rb
178
181
  - lib/lifer/builder/html/from_liquid.rb
179
182
  - lib/lifer/builder/html/from_liquid/drops.rb
183
+ - lib/lifer/builder/html/from_liquid/drops/authors_drop.rb
180
184
  - lib/lifer/builder/html/from_liquid/drops/collection_drop.rb
181
185
  - lib/lifer/builder/html/from_liquid/drops/collections_drop.rb
182
186
  - lib/lifer/builder/html/from_liquid/drops/entry_drop.rb
@@ -186,6 +190,7 @@ files:
186
190
  - lib/lifer/builder/html/from_liquid/drops/tags_drop.rb
187
191
  - lib/lifer/builder/html/from_liquid/filters.rb
188
192
  - lib/lifer/builder/html/from_liquid/liquid_env.rb
193
+ - lib/lifer/builder/json_feed.rb
189
194
  - lib/lifer/builder/rss.rb
190
195
  - lib/lifer/builder/txt.rb
191
196
  - lib/lifer/cli.rb
@@ -224,8 +229,8 @@ licenses:
224
229
  - MIT
225
230
  metadata:
226
231
  allowed_push_host: https://rubygems.org
227
- homepage_uri: https://github.com/benjaminwil/lifer/blob/v0.12.3/README.md
228
- source_code_uri: https://github.com/benjaminwil/lifer/tree/v0.12.3
232
+ homepage_uri: https://github.com/benjaminwil/lifer/blob/v0.13.0/README.md
233
+ source_code_uri: https://github.com/benjaminwil/lifer/tree/v0.13.0
229
234
  changelog_uri: https://github.com/benjaminwil/lifer/blob/main/CHANGELOG.md
230
235
  post_install_message:
231
236
  rdoc_options: []