lifer 0.12.4 → 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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/guix.scm +19 -0
- data/lib/lifer/asset.rb +82 -0
- data/lib/lifer/author.rb +106 -0
- data/lib/lifer/brain.rb +21 -0
- data/lib/lifer/builder/html/from_liquid/drops/authors_drop.rb +33 -0
- data/lib/lifer/builder/html/from_liquid/drops/entry_drop.rb +3 -1
- data/lib/lifer/builder/html/from_liquid/drops.rb +1 -0
- data/lib/lifer/builder/json_feed.rb +214 -0
- data/lib/lifer/builder/rss.rb +5 -4
- data/lib/lifer/builder.rb +1 -0
- data/lib/lifer/collection.rb +2 -0
- data/lib/lifer/config.rb +0 -2
- data/lib/lifer/entry/markdown.rb +0 -2
- data/lib/lifer/entry.rb +47 -3
- data/lib/lifer/templates/config.yaml +15 -2
- data/lib/lifer/utilities.rb +19 -0
- data/lib/lifer/version.rb +1 -1
- data/lib/lifer.rb +21 -2
- data/locales/en.yml +1 -0
- metadata +13 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e8681438caf9356526a0af22c91ec3470c3ccba625c7d6602adab532d89a4b2
|
|
4
|
+
data.tar.gz: 9c2eefb99f760d50aac163f2dff6e0d06d3710d2a0682426ff45d75733be3640
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4411e369a5fab2ec20c0a9c6835279ecc3b0a9c8d522c8ad9b2f5e06223b60ccd3604f576bc9a06f051a6a6e1d33d2990e6e601b1046488ef6812c9ca27182b
|
|
7
|
+
data.tar.gz: 45697944e8bb0c378083db6b4ec2c5fe0df67c70a51a5dad87cb6d57fade3f58a98d5dbc07086bd231126f49802c47930490febc85aa1b313bbe372b1a70e13c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## v0.12.4
|
|
4
14
|
|
|
5
15
|
This release resolves two issues:
|
data/Gemfile.lock
CHANGED
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))
|
data/lib/lifer/asset.rb
ADDED
|
@@ -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
|
data/lib/lifer/author.rb
ADDED
|
@@ -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
|
#
|
|
@@ -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
|
|
28
|
+
def authors
|
|
29
|
+
@authors ||= AuthorsDrop.new(lifer_entry.authors)
|
|
30
|
+
end
|
|
29
31
|
|
|
30
32
|
# The entry content.
|
|
31
33
|
#
|
|
@@ -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
|
data/lib/lifer/builder/rss.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
data/lib/lifer/collection.rb
CHANGED
|
@@ -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
data/lib/lifer/entry/markdown.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
-
###
|
|
37
|
-
###
|
|
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
|
#
|
data/lib/lifer/utilities.rb
CHANGED
|
@@ -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
data/lib/lifer.rb
CHANGED
|
@@ -29,6 +29,22 @@ module Lifer
|
|
|
29
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`
|
|
177
|
-
#
|
|
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,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lifer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- benjamin wil
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-05-30 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: base64
|
|
@@ -168,7 +169,10 @@ files:
|
|
|
168
169
|
- bin/console
|
|
169
170
|
- bin/lifer
|
|
170
171
|
- bin/setup
|
|
172
|
+
- guix.scm
|
|
171
173
|
- lib/lifer.rb
|
|
174
|
+
- lib/lifer/asset.rb
|
|
175
|
+
- lib/lifer/author.rb
|
|
172
176
|
- lib/lifer/brain.rb
|
|
173
177
|
- lib/lifer/builder.rb
|
|
174
178
|
- lib/lifer/builder/html.rb
|
|
@@ -176,6 +180,7 @@ files:
|
|
|
176
180
|
- lib/lifer/builder/html/from_erb.rb
|
|
177
181
|
- lib/lifer/builder/html/from_liquid.rb
|
|
178
182
|
- lib/lifer/builder/html/from_liquid/drops.rb
|
|
183
|
+
- lib/lifer/builder/html/from_liquid/drops/authors_drop.rb
|
|
179
184
|
- lib/lifer/builder/html/from_liquid/drops/collection_drop.rb
|
|
180
185
|
- lib/lifer/builder/html/from_liquid/drops/collections_drop.rb
|
|
181
186
|
- lib/lifer/builder/html/from_liquid/drops/entry_drop.rb
|
|
@@ -185,6 +190,7 @@ files:
|
|
|
185
190
|
- lib/lifer/builder/html/from_liquid/drops/tags_drop.rb
|
|
186
191
|
- lib/lifer/builder/html/from_liquid/filters.rb
|
|
187
192
|
- lib/lifer/builder/html/from_liquid/liquid_env.rb
|
|
193
|
+
- lib/lifer/builder/json_feed.rb
|
|
188
194
|
- lib/lifer/builder/rss.rb
|
|
189
195
|
- lib/lifer/builder/txt.rb
|
|
190
196
|
- lib/lifer/cli.rb
|
|
@@ -223,9 +229,10 @@ licenses:
|
|
|
223
229
|
- MIT
|
|
224
230
|
metadata:
|
|
225
231
|
allowed_push_host: https://rubygems.org
|
|
226
|
-
homepage_uri: https://github.com/benjaminwil/lifer/blob/v0.
|
|
227
|
-
source_code_uri: https://github.com/benjaminwil/lifer/tree/v0.
|
|
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
|
|
228
234
|
changelog_uri: https://github.com/benjaminwil/lifer/blob/main/CHANGELOG.md
|
|
235
|
+
post_install_message:
|
|
229
236
|
rdoc_options: []
|
|
230
237
|
require_paths:
|
|
231
238
|
- lib
|
|
@@ -240,7 +247,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
240
247
|
- !ruby/object:Gem::Version
|
|
241
248
|
version: '0'
|
|
242
249
|
requirements: []
|
|
243
|
-
rubygems_version: 3.
|
|
250
|
+
rubygems_version: 3.5.22
|
|
251
|
+
signing_key:
|
|
244
252
|
specification_version: 4
|
|
245
253
|
summary: Minimal static weblog generator.
|
|
246
254
|
test_files: []
|