lifer 0.7.0 → 0.8.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 +28 -0
- data/Gemfile.lock +1 -1
- data/lib/lifer/brain.rb +14 -1
- data/lib/lifer/builder/html/from_erb.rb +24 -5
- data/lib/lifer/builder/html/from_liquid/drops/collection_drop.rb +1 -1
- data/lib/lifer/builder/html/from_liquid/drops/collections_drop.rb +0 -1
- data/lib/lifer/builder/html/from_liquid/drops/entry_drop.rb +2 -1
- data/lib/lifer/builder/html/from_liquid/drops/tag_drop.rb +42 -0
- data/lib/lifer/builder/html/from_liquid/drops/tags_drop.rb +43 -0
- data/lib/lifer/builder/html/from_liquid/drops.rb +2 -0
- data/lib/lifer/builder/html/from_liquid.rb +4 -2
- data/lib/lifer/entry/html.rb +10 -13
- data/lib/lifer/entry/markdown.rb +12 -79
- data/lib/lifer/entry/txt.rb +10 -13
- data/lib/lifer/entry.rb +252 -129
- data/lib/lifer/tag.rb +53 -0
- data/lib/lifer/version.rb +1 -1
- data/lib/lifer.rb +12 -0
- data/lifer.gemspec +2 -2
- data/locales/en.yml +2 -3
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea41a2cd1a058f5a103121b6b7641ce5857ec74300ecad4bb36ec33578170650
|
4
|
+
data.tar.gz: f752dc7498a0457945d895fc591eefc7a27a296e01a5b547264a13fd8113c3b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4665579521538df261c0839f6c1a7ee6b24362ca75c03cc6d8fbf42dfffa028ad0bc39c1fe01ef2407459cb1be6bdf9d08e60cd5abacd1e233b0819bbcab6186
|
7
|
+
data.tar.gz: e1bfbd9fa63bd2de10022f2b5810cfcf518acc5112f51bb843c19c936bc269436807a606c335ab90693bafac53569315b0bfcbe245b9c8570a56ff6ab60af6f0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,33 @@
|
|
1
1
|
## Next
|
2
2
|
|
3
|
+
## v0.8.0
|
4
|
+
|
5
|
+
### Tags
|
6
|
+
|
7
|
+
Entries now support tag frontmatter. This introduces a new way of making
|
8
|
+
associations between entries. Tags can encoded in entries as YAML arrays or as a
|
9
|
+
string (with comma and/or space delimiters):
|
10
|
+
|
11
|
+
---
|
12
|
+
title: My Entry
|
13
|
+
tags: beautifulTag, lovelyTag
|
14
|
+
---
|
15
|
+
|
16
|
+
Blah blah blah.
|
17
|
+
|
18
|
+
Then, in your ERB or Liquid templates you can get entries via tag:
|
19
|
+
|
20
|
+
<% tags.beautifulTag.entries.each do |entry| %>
|
21
|
+
<li><%= entry.title %></li>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
### Frontmatter support across all entry types
|
25
|
+
|
26
|
+
Before this release, frontmatter was only supported by Markdown files. This
|
27
|
+
started to be annoying, because of features I wanted like tags. I figured it
|
28
|
+
couldn't hurt to just check any entry file for frontmatter, so that's how it
|
29
|
+
works now.
|
30
|
+
|
3
31
|
## v0.7.0
|
4
32
|
|
5
33
|
This release adds Atom feed support to the RSS builder. In your configuration
|
data/Gemfile.lock
CHANGED
data/lib/lifer/brain.rb
CHANGED
@@ -65,7 +65,7 @@ class Lifer::Brain
|
|
65
65
|
# @return [Array<Lifer::Collection>] All the collections for the current Lifer
|
66
66
|
# project.
|
67
67
|
def collections
|
68
|
-
@collections ||= generate_collections + generate_selections
|
68
|
+
@collections ||= (generate_collections + generate_selections).to_a
|
69
69
|
end
|
70
70
|
|
71
71
|
# Returns the Lifer project's configuration object.
|
@@ -139,6 +139,19 @@ class Lifer::Brain
|
|
139
139
|
config.setting *name, collection_name: collection&.name, strict: strict
|
140
140
|
end
|
141
141
|
|
142
|
+
# Given the tag manifest, this returns an array of all tags for the current
|
143
|
+
# project. This method is preferrable for accessing and querying for tags.
|
144
|
+
#
|
145
|
+
# @return [Array<Lifer::Tag>]
|
146
|
+
def tags = tag_manifest.to_a
|
147
|
+
|
148
|
+
# The tag manifest tracks the unique tags added to the project as they're added.
|
149
|
+
# The writer method for this instance variable is used internally by Lifer when
|
150
|
+
# adding new tags.
|
151
|
+
#
|
152
|
+
# @return [Set<Lifer::Tag>]
|
153
|
+
def tag_manifest = (@tag_manifest ||= Set.new)
|
154
|
+
|
142
155
|
private
|
143
156
|
|
144
157
|
attr_reader :config_file_location
|
@@ -56,10 +56,16 @@ class Lifer::Builder::HTML
|
|
56
56
|
end
|
57
57
|
|
58
58
|
# @private
|
59
|
-
# Each collection name is provided as a local variable. This allows
|
60
|
-
# make ERB files that contain loops like:
|
59
|
+
# Each collection and tag name is provided as a local variable. This allows
|
60
|
+
# you to make ERB files that contain loops like:
|
61
61
|
#
|
62
|
-
# <% my_collection_name.entries.each do |entry| %>
|
62
|
+
# <% collections.my_collection_name.entries.each do |entry| %>
|
63
|
+
# <%= entry.title %>
|
64
|
+
# <% end %>
|
65
|
+
#
|
66
|
+
# or:
|
67
|
+
#
|
68
|
+
# <% tags.my_tag_name.entries.each do |entry| %>
|
63
69
|
# <%= entry.title %>
|
64
70
|
# <% end %>
|
65
71
|
#
|
@@ -75,18 +81,31 @@ class Lifer::Builder::HTML
|
|
75
81
|
end
|
76
82
|
end
|
77
83
|
|
78
|
-
|
84
|
+
Lifer.tags.each do |tag|
|
85
|
+
binding.local_variable_set tag.name, tag
|
86
|
+
|
87
|
+
tag_context_class.define_method(tag.name) do
|
88
|
+
tag
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
collections = collection_context_class.new Lifer.collections
|
93
|
+
tags = tag_context_class.new Lifer.tags
|
79
94
|
|
80
95
|
binding.local_variable_set :collections, collections
|
81
96
|
binding.local_variable_set :settings, Lifer.settings
|
97
|
+
binding.local_variable_set :tags, tags
|
82
98
|
binding.local_variable_set :content,
|
83
99
|
ERB.new(entry.to_html).result(binding)
|
84
100
|
}
|
85
101
|
end
|
86
102
|
|
87
|
-
# @private
|
88
103
|
def collection_context_class
|
89
104
|
@collection_context_class ||= Class.new(Array) do end
|
90
105
|
end
|
106
|
+
|
107
|
+
def tag_context_class
|
108
|
+
@tag_context_class ||= Class.new(Array) do end
|
109
|
+
end
|
91
110
|
end
|
92
111
|
end
|
@@ -9,9 +9,10 @@ module Lifer::Builder::HTML::FromLiquid::Drops
|
|
9
9
|
class EntryDrop < Liquid::Drop
|
10
10
|
attr_accessor :lifer_entry, :collection
|
11
11
|
|
12
|
-
def initialize(lifer_entry, collection:)
|
12
|
+
def initialize(lifer_entry, collection:, tags:)
|
13
13
|
@lifer_entry = lifer_entry
|
14
14
|
@collection = collection
|
15
|
+
@tags = tags
|
15
16
|
end
|
16
17
|
|
17
18
|
# The entry author (or authors).
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Lifer::Builder::HTML::FromLiquid::Drops
|
2
|
+
# This drop allows users to access Lifer tag information from within
|
3
|
+
# Liquid templates.
|
4
|
+
#
|
5
|
+
# @example Usage
|
6
|
+
# {{ tag.name }}
|
7
|
+
# {% for entries in tag.entries %}
|
8
|
+
# {{ entry.title }}
|
9
|
+
# {% endfor %}
|
10
|
+
#
|
11
|
+
class TagDrop < Liquid::Drop
|
12
|
+
attr_accessor :lifer_tag
|
13
|
+
|
14
|
+
def initialize(lifer_tag) = (@lifer_tag = lifer_tag)
|
15
|
+
|
16
|
+
# The tag name.
|
17
|
+
#
|
18
|
+
# @return [Symbol]
|
19
|
+
def name = (@name ||= lifer_tag.name)
|
20
|
+
|
21
|
+
# Gets all entries in a tag and converts them to entry drops that can
|
22
|
+
# be accessed in Liquid templates. Example:
|
23
|
+
#
|
24
|
+
# {% for entry in tags..entries %}
|
25
|
+
# {{ entry.title }}
|
26
|
+
# {% endfor %}
|
27
|
+
#
|
28
|
+
# @return [Array<EntryDrop>]
|
29
|
+
def entries
|
30
|
+
@entries ||= lifer_tag.entries.map { |lifer_entry|
|
31
|
+
EntryDrop.new lifer_entry,
|
32
|
+
collection: CollectionDrop.new(lifer_entry.collection),
|
33
|
+
tags: lifer_entry.tags.map { TagDrop.new _1 }
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# The tag's layout file path.
|
38
|
+
#
|
39
|
+
# @return [String] The path to the layout file.
|
40
|
+
def layout_file = (@lifer_tag.layout_file)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Lifer::Builder::HTML::FromLiquid::Drops
|
2
|
+
# This drop allows users to iterate over their Lifer tags in Liquid
|
3
|
+
# templates.
|
4
|
+
#
|
5
|
+
# @example Usage
|
6
|
+
# {% for tag in tags %}
|
7
|
+
# {{ tag.name }}
|
8
|
+
# {% endfor %}
|
9
|
+
#
|
10
|
+
# {% for entry in tags.name-of-tag.entries %}
|
11
|
+
# {{ entry.title }}
|
12
|
+
# {% endfor %}
|
13
|
+
#
|
14
|
+
class TagsDrop < Liquid::Drop
|
15
|
+
attr_accessor :tags
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@tags = Lifer.tags.map { TagDrop.new _1 }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Allow tags to be iterable in Liquid templates.
|
22
|
+
#
|
23
|
+
# @yield [CollectionDrop] All available collection drops.
|
24
|
+
def each(&block) = tags.each(&block)
|
25
|
+
|
26
|
+
# Allow tags to be rendered as an array in Liquid templates.
|
27
|
+
#
|
28
|
+
# @return [Array]
|
29
|
+
def to_a = @tags
|
30
|
+
|
31
|
+
# Dynamically define Liquid accessors based on the Lifer project's
|
32
|
+
# collection names.
|
33
|
+
#
|
34
|
+
# @example Get the "tagName" tag's entries.
|
35
|
+
# {{ tags.tagName.entries }}
|
36
|
+
#
|
37
|
+
# @param arg [String] The name of a collection.
|
38
|
+
# @return [CollectionDrop, NilClass]
|
39
|
+
def liquid_method_missing(arg)
|
40
|
+
tags.detect { arg.to_s == _1.name.to_s }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -5,7 +5,6 @@ require_relative "from_liquid/filters"
|
|
5
5
|
require_relative "from_liquid/layout_tag"
|
6
6
|
require_relative "from_liquid/liquid_env"
|
7
7
|
|
8
|
-
|
9
8
|
class Lifer::Builder::HTML
|
10
9
|
# If the HTML builder is given a Liquid template, it uses this class to parse
|
11
10
|
# the Liquid into HTML. Lifer project metadata is provided as context. For
|
@@ -64,13 +63,16 @@ class Lifer::Builder::HTML
|
|
64
63
|
|
65
64
|
def context
|
66
65
|
collections = Drops::CollectionsDrop.new
|
66
|
+
tags = Drops::TagsDrop.new
|
67
67
|
collection = collections
|
68
68
|
.to_a
|
69
69
|
.detect { _1.name.to_sym == entry.collection.name }
|
70
|
+
entry_tags = tags.to_a.select { entry.tags.include? _1 }
|
70
71
|
|
71
72
|
{
|
72
73
|
"collections" => collections,
|
73
|
-
"
|
74
|
+
"tags" => tags,
|
75
|
+
"entry" => Drops::EntryDrop.new(entry, collection:, tags: entry_tags),
|
74
76
|
"parse_options" => parse_options,
|
75
77
|
"render_options" => render_options,
|
76
78
|
"settings" => Drops::SettingsDrop.new
|
data/lib/lifer/entry/html.rb
CHANGED
@@ -7,21 +7,18 @@ class Lifer::Entry::HTML < Lifer::Entry
|
|
7
7
|
self.input_extensions = ["html", "html.erb", "html.liquid"]
|
8
8
|
self.output_extension = :html
|
9
9
|
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# just give them a default date to start.
|
10
|
+
# If there is no available metadata in the HTML file, we can extract a
|
11
|
+
# makeshift title from the permalink.
|
13
12
|
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
# a title from the permalink. Depending on the filename and URI strategy being
|
19
|
-
# used for the collection, it's possible that the extracted title would be
|
20
|
-
# "index", which is not very descriptive. If that's the case, we attempt to go
|
21
|
-
# up a directory to find a "non-index" title.
|
13
|
+
# Depending on the filename and URI strategy being used for the collection,
|
14
|
+
# it's possible that the extracted title would be "index", which is not very
|
15
|
+
# descriptive. If that's the case, we attempt to go up a directory to find a
|
16
|
+
# non-"index" title.
|
22
17
|
#
|
23
|
-
# @return [String] The extracted title of the entry.
|
18
|
+
# @return [String] The given or extracted title of the entry.
|
24
19
|
def title
|
20
|
+
return frontmatter[:title] if frontmatter[:title]
|
21
|
+
|
25
22
|
candidate = File.basename(permalink, ".html")
|
26
23
|
|
27
24
|
if candidate.include?("index") && !file.to_s.include?("index")
|
@@ -35,5 +32,5 @@ class Lifer::Entry::HTML < Lifer::Entry
|
|
35
32
|
# doesn't do much here.
|
36
33
|
#
|
37
34
|
# @return [String]
|
38
|
-
def to_html =
|
35
|
+
def to_html = body
|
39
36
|
end
|
data/lib/lifer/entry/markdown.rb
CHANGED
@@ -14,78 +14,10 @@ require_relative "../utilities"
|
|
14
14
|
# it may make sense to pull some of these methods into a separate module.
|
15
15
|
#
|
16
16
|
class Lifer::Entry::Markdown < Lifer::Entry
|
17
|
-
# If a filename contains a date, we should expect it to be in the following
|
18
|
-
# format.
|
19
|
-
#
|
20
|
-
FILENAME_DATE_FORMAT = /^(\d{4}-\d{1,2}-\d{1,2})-/
|
21
|
-
|
22
|
-
# We expect frontmatter to be provided in the following format.
|
23
|
-
#
|
24
|
-
FRONTMATTER_REGEX = /^---\n(.*)---\n/m
|
25
|
-
|
26
|
-
# We truncate anything that needs to be truncated (summaries, meta
|
27
|
-
# descriptions) at the following character count.
|
28
|
-
#
|
29
|
-
TRUNCATION_THRESHOLD = 120
|
30
|
-
|
31
17
|
self.include_in_feeds = true
|
32
18
|
self.input_extensions = ["md"]
|
33
19
|
self.output_extension = :html
|
34
20
|
|
35
|
-
# Given the entry's frontmatter, we should be able to get a list of authors.
|
36
|
-
# We always prefer authors (as opposed to a singular author) because it makes
|
37
|
-
# handling both cases easier in the long run.
|
38
|
-
#
|
39
|
-
# The return value here is likely an author's name. Whether that's a full
|
40
|
-
# name, a first name, or a handle is up to the end user.
|
41
|
-
#
|
42
|
-
# @return [Array<String>] An array of authors's names.
|
43
|
-
def authors
|
44
|
-
Array(frontmatter[:author] || frontmatter[:authors]).compact
|
45
|
-
end
|
46
|
-
|
47
|
-
# This method returns the full text of the entry, only removing the
|
48
|
-
# frontmatter. It should not parse anything other than frontmatter.
|
49
|
-
#
|
50
|
-
# @return [String] The body of the entry.
|
51
|
-
def body
|
52
|
-
return full_text.strip unless frontmatter?
|
53
|
-
|
54
|
-
full_text.gsub(FRONTMATTER_REGEX, "").strip
|
55
|
-
end
|
56
|
-
|
57
|
-
# Since Markdown files would only store dates as simple strings, it's nice to
|
58
|
-
# attempt to convert those into Ruby date or datetime objects.
|
59
|
-
#
|
60
|
-
# @return [Time] A Ruby representation of the date and time provided by the
|
61
|
-
# entry frontmatter or filename.
|
62
|
-
def date
|
63
|
-
date_data = frontmatter[:date] || filename_date
|
64
|
-
|
65
|
-
case date_data
|
66
|
-
when Time then date_data
|
67
|
-
when String then DateTime.parse(date_data).to_time
|
68
|
-
else
|
69
|
-
Lifer::Message.log("entry.markdown.no_date_metadata", filename: file)
|
70
|
-
Lifer::Entry::DEFAULT_DATE
|
71
|
-
end
|
72
|
-
rescue ArgumentError => error
|
73
|
-
Lifer::Message.error("entry.markdown.date_error", filename: file, error:)
|
74
|
-
Lifer::Entry::DEFAULT_DATE
|
75
|
-
end
|
76
|
-
|
77
|
-
# Frontmatter is a widely supported YAML metadata block found at the top of
|
78
|
-
# Markdown files. We should attempt to parse Markdown entries for it.
|
79
|
-
#
|
80
|
-
# @return [Hash] A hash representation of the entry frontmatter.
|
81
|
-
def frontmatter
|
82
|
-
return {} unless frontmatter?
|
83
|
-
|
84
|
-
Lifer::Utilities.symbolize_keys(
|
85
|
-
YAML.load(full_text[FRONTMATTER_REGEX, 1], permitted_classes: [Time])
|
86
|
-
)
|
87
|
-
end
|
88
|
-
|
89
21
|
# If given a summary in the frontmatter of the entry, we can use this to
|
90
22
|
# provide a summary. Otherwise, we can truncate the first paragraph and use
|
91
23
|
# that as a summary, although that is a bit annoying. This is useful for
|
@@ -96,7 +28,7 @@ class Lifer::Entry::Markdown < Lifer::Entry
|
|
96
28
|
#
|
97
29
|
# @return [String] A summary of the entry.
|
98
30
|
def summary
|
99
|
-
return
|
31
|
+
return super if super
|
100
32
|
|
101
33
|
return if first_paragraph.nil?
|
102
34
|
return first_paragraph if first_paragraph.length <= TRUNCATION_THRESHOLD
|
@@ -130,11 +62,17 @@ class Lifer::Entry::Markdown < Lifer::Entry
|
|
130
62
|
|
131
63
|
private
|
132
64
|
|
133
|
-
#
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
65
|
+
# It is conventional for users to use spaces or commas to delimit tags in
|
66
|
+
# other systems, so let's support that. But let's also support YAML-style
|
67
|
+
# arrays.
|
68
|
+
#
|
69
|
+
# @return [Array<String>] An array of candidate tag names.
|
70
|
+
def candidate_tag_names
|
71
|
+
case frontmatter[:tags]
|
72
|
+
when Array then frontmatter[:tags].map(&:to_s)
|
73
|
+
when String then frontmatter[:tags].split(TAG_DELIMITER_REGEX)
|
74
|
+
else []
|
75
|
+
end.uniq
|
138
76
|
end
|
139
77
|
|
140
78
|
# Using Kramdown we can detect the first paragraph of the entry.
|
@@ -149,11 +87,6 @@ class Lifer::Entry::Markdown < Lifer::Entry
|
|
149
87
|
)
|
150
88
|
end
|
151
89
|
|
152
|
-
# @private
|
153
|
-
def frontmatter?
|
154
|
-
full_text && full_text.match?(FRONTMATTER_REGEX)
|
155
|
-
end
|
156
|
-
|
157
90
|
# @private
|
158
91
|
def kramdown_paragraph_text(kramdown_element)
|
159
92
|
return if kramdown_element.nil?
|
data/lib/lifer/entry/txt.rb
CHANGED
@@ -7,21 +7,18 @@ class Lifer::Entry::TXT < Lifer::Entry
|
|
7
7
|
self.input_extensions = ["txt"]
|
8
8
|
self.output_extension = :txt
|
9
9
|
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# just give them a default date to start.
|
10
|
+
# If there is no available metadata in the text file, we can extract a
|
11
|
+
# makeshift title from the permalink.
|
13
12
|
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
# a title from the permalink. Depending on the filename and URI strategy being
|
19
|
-
# used for the collection, it's possible that the extracted title would be
|
20
|
-
# "index", which is not very descriptive. If that's the case, we attempt to go
|
21
|
-
# up a directory to find a "non-index" title.
|
13
|
+
# Depending on the filename and URI strategy being used for the collection,
|
14
|
+
# it's possible that the extracted title would be "index", which is not very
|
15
|
+
# descriptive. If that's the case, we attempt to go up a directory to find a
|
16
|
+
# non-"index" title.
|
22
17
|
#
|
23
|
-
# @return [String] The extracted title of the entry.
|
18
|
+
# @return [String] The given or extracted title of the entry.
|
24
19
|
def title
|
20
|
+
return frontmatter[:title] if frontmatter[:title]
|
21
|
+
|
25
22
|
candidate = File.basename(permalink, ".txt")
|
26
23
|
|
27
24
|
if candidate.include?("index") && !file.to_s.include?("index")
|
@@ -37,5 +34,5 @@ class Lifer::Entry::TXT < Lifer::Entry
|
|
37
34
|
# @fixme Maybe the `#to_html` methods should be renamed, then?
|
38
35
|
#
|
39
36
|
# @return [String] The output HTML (not actually HTML).
|
40
|
-
def to_html =
|
37
|
+
def to_html = body
|
41
38
|
end
|
data/lib/lifer/entry.rb
CHANGED
@@ -1,166 +1,289 @@
|
|
1
1
|
require "digest/sha1"
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# subclasses.
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
17
|
-
class
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
3
|
+
module Lifer
|
4
|
+
# An entry is a Lifer file that will be built into the output directory.
|
5
|
+
# There are more than one subclass of entry: Markdown entries are the most
|
6
|
+
# traditional, but HTML and text files are also very valid entries.
|
7
|
+
#
|
8
|
+
# This class provides a baseline of the functionality that all entry
|
9
|
+
# subclasses should implement. It also provides the entry generator for
|
10
|
+
# *all* entry subclasses.
|
11
|
+
#
|
12
|
+
# @fixme Markdown entries are able to provide metadata via frontmatter, but
|
13
|
+
# other entry types do not currently support frontmatter. Should they? Or is
|
14
|
+
# there some nicer way to provide entry metadata for non-Markdown files in
|
15
|
+
# 2024?
|
16
|
+
#
|
17
|
+
class Entry
|
18
|
+
class << self
|
19
|
+
attr_accessor :include_in_feeds
|
20
|
+
attr_accessor :input_extensions
|
21
|
+
attr_accessor :output_extension
|
22
|
+
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
self.include_in_feeds = false
|
25
|
+
self.input_extensions = []
|
26
|
+
self.output_extension = nil
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
require_relative "entry/html"
|
29
|
+
require_relative "entry/markdown"
|
30
|
+
require_relative "entry/txt"
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
# We provide a default date for entries that have no date and entry types that
|
33
|
+
# otherwise could not have a date due to no real way of getting that metadata.
|
34
|
+
#
|
35
|
+
DEFAULT_DATE = Time.new(1900, 01, 01, 0, 0, 0, "+00:00")
|
36
|
+
|
37
|
+
# If a filename contains a date, we should expect it to be in the following
|
38
|
+
# format.
|
39
|
+
#
|
40
|
+
FILENAME_DATE_FORMAT = /^(\d{4}-\d{1,2}-\d{1,2})-/
|
41
|
+
|
42
|
+
# We expect frontmatter to be provided in the following format.
|
43
|
+
#
|
44
|
+
FRONTMATTER_REGEX = /^---\n(.*)---\n/m
|
35
45
|
|
36
|
-
|
46
|
+
# If tags are represented in YAML frontmatter as a string, they're split on
|
47
|
+
# commas and/or spaces.
|
48
|
+
#
|
49
|
+
TAG_DELIMITER_REGEX = /[,\s]+/
|
37
50
|
|
38
|
-
|
39
|
-
#
|
40
|
-
# `Lifer::Entry` records: only subclasses.
|
51
|
+
# We truncate anything that needs to be truncated (summaries, meta
|
52
|
+
# descriptions) at the following character count.
|
41
53
|
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
54
|
+
TRUNCATION_THRESHOLD = 120
|
55
|
+
|
56
|
+
attr_reader :file, :collection
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# The entrypoint for generating entry objects. We should never end up with
|
60
|
+
# `Lifer::Entry` records: only subclasses.
|
61
|
+
#
|
62
|
+
# @param file [String] The absolute filename of an entry file.
|
63
|
+
# @param collection [Lifer::Collection] The collection for the entry.
|
64
|
+
# @return [Lifer::Entry] An entry.
|
65
|
+
def generate(file:, collection:)
|
66
|
+
error!(file) unless File.exist?(file)
|
67
|
+
|
68
|
+
if (new_entry = subclass_for(file)&.new(file:, collection:))
|
69
|
+
Lifer.entry_manifest << new_entry
|
70
|
+
new_entry.tags
|
71
|
+
end
|
72
|
+
|
73
|
+
new_entry
|
74
|
+
end
|
75
|
+
|
76
|
+
# Whenever an entry is generated it should be added to the entry manifest.
|
77
|
+
# This lets us get a list of all generated entries.
|
78
|
+
#
|
79
|
+
# @return [Array<Lifer::Entry>] A list of all entries that currently exist.
|
80
|
+
def manifest
|
81
|
+
return Lifer.entry_manifest if self == Lifer::Entry
|
82
|
+
|
83
|
+
Lifer.entry_manifest.select { |entry| entry.class == self }
|
84
|
+
end
|
47
85
|
|
48
|
-
|
49
|
-
|
86
|
+
# Checks whether the given filename is supported entry type (using only its
|
87
|
+
# file extension).
|
88
|
+
#
|
89
|
+
# @param filename [String] The absolute filename to an entry.
|
90
|
+
# @param file_extensions [Array<String>] An array of file extensions to
|
91
|
+
# check against.
|
92
|
+
# @return [Boolean]
|
93
|
+
def supported?(filename, file_extensions= supported_file_extensions)
|
94
|
+
file_extensions.any? { |ext| filename.end_with? ext }
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def supported_file_extensions
|
100
|
+
@supported_file_extensions ||= subclasses.flat_map(&:input_extensions)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @private
|
104
|
+
# Retrieve the entry subclass based on the current filename.
|
105
|
+
#
|
106
|
+
# @param filename [String] The current entry's filename.
|
107
|
+
# @return [Class] The entry subclass for the current entry.
|
108
|
+
def subclass_for(filename)
|
109
|
+
Lifer::Entry.subclasses.detect { |klass|
|
110
|
+
klass.input_extensions.any? { |ext| filename.end_with? ext }
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
def error!(file)
|
116
|
+
raise StandardError, I18n.t("entry.not_found", file:)
|
50
117
|
end
|
51
|
-
new_entry
|
52
118
|
end
|
53
119
|
|
54
|
-
#
|
55
|
-
#
|
120
|
+
# When a new entry is initialized we expect the file to already exist, and
|
121
|
+
# we expect to know which `Lifer::Collection` it belongs to.
|
56
122
|
#
|
57
|
-
# @
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
123
|
+
# @param file [String] An absolute path to a file.
|
124
|
+
# @param collection [Lifer::Collection] A collection.
|
125
|
+
# @return [Lifer::Entry]
|
126
|
+
def initialize(file:, collection:)
|
127
|
+
@file = Pathname file
|
128
|
+
@collection = collection
|
62
129
|
end
|
63
130
|
|
64
|
-
#
|
65
|
-
#
|
131
|
+
# Given the entry's frontmatter, we should be able to get a list of authors.
|
132
|
+
# We always prefer authors (as opposed to a singular author) because it makes
|
133
|
+
# handling both cases easier in the long run.
|
66
134
|
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
# @return [
|
71
|
-
def
|
72
|
-
|
135
|
+
# The return value here is likely an author's name. Whether that's a full
|
136
|
+
# name, a first name, or a handle is up to the end user.
|
137
|
+
#
|
138
|
+
# @return [Array<String>] An array of authors's names.
|
139
|
+
def authors
|
140
|
+
Array(frontmatter[:author] || frontmatter[:authors]).compact
|
73
141
|
end
|
74
142
|
|
75
|
-
|
143
|
+
# This method returns the full text of the entry, only removing the
|
144
|
+
# frontmatter. It should not parse anything other than frontmatter.
|
145
|
+
#
|
146
|
+
# @return [String] The body of the entry.
|
147
|
+
def body
|
148
|
+
return full_text.strip unless frontmatter?
|
76
149
|
|
77
|
-
|
78
|
-
@supported_file_extensions ||= subclasses.flat_map(&:input_extensions)
|
150
|
+
full_text.gsub(FRONTMATTER_REGEX, "").strip
|
79
151
|
end
|
80
152
|
|
81
|
-
#
|
82
|
-
#
|
153
|
+
# Since text files would only store dates as simple strings, it's nice to
|
154
|
+
# attempt to convert those into Ruby date or datetime objects.
|
83
155
|
#
|
84
|
-
# @
|
85
|
-
#
|
86
|
-
def
|
87
|
-
|
88
|
-
|
89
|
-
|
156
|
+
# @return [Time] A Ruby representation of the date and time provided by the
|
157
|
+
# entry frontmatter or filename.
|
158
|
+
def date
|
159
|
+
date_data = frontmatter[:date] || filename_date
|
160
|
+
|
161
|
+
case date_data
|
162
|
+
when Time then date_data
|
163
|
+
when String then DateTime.parse(date_data).to_time
|
164
|
+
else
|
165
|
+
Lifer::Message.log("entry.no_date_metadata", filename: file)
|
166
|
+
Lifer::Entry::DEFAULT_DATE
|
167
|
+
end
|
168
|
+
rescue ArgumentError => error
|
169
|
+
Lifer::Message.error("entry.date_error", filename: file, error:)
|
170
|
+
Lifer::Entry::DEFAULT_DATE
|
90
171
|
end
|
91
172
|
|
92
|
-
|
93
|
-
|
94
|
-
|
173
|
+
def feedable?
|
174
|
+
if (setting = self.class.include_in_feeds).nil?
|
175
|
+
raise NotImplementedError,
|
176
|
+
I18n.t("entry.feedable_error", entry_class: self.class)
|
177
|
+
end
|
178
|
+
|
179
|
+
setting
|
95
180
|
end
|
96
|
-
end
|
97
181
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
@file = Pathname file
|
106
|
-
@collection = collection
|
107
|
-
end
|
182
|
+
# Frontmatter is a widely supported YAML metadata block found at the top of
|
183
|
+
# text--often Markdown--files. We attempt to parse all entries for
|
184
|
+
# frontmatter.
|
185
|
+
#
|
186
|
+
# @return [Hash] A hash representation of the entry frontmatter.
|
187
|
+
def frontmatter
|
188
|
+
return {} unless frontmatter?
|
108
189
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
I18n.t("entry.feedable_error", entry_class: self.class)
|
190
|
+
Lifer::Utilities.symbolize_keys(
|
191
|
+
YAML.load(full_text[FRONTMATTER_REGEX, 1], permitted_classes: [Time])
|
192
|
+
)
|
113
193
|
end
|
114
194
|
|
115
|
-
|
116
|
-
|
195
|
+
# The full text of the entry.
|
196
|
+
#
|
197
|
+
# @return [String]
|
198
|
+
def full_text
|
199
|
+
@full_text ||= File.readlines(file).join if file
|
200
|
+
end
|
117
201
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
202
|
+
# Using the current Lifer configuration, we can calculate the expected
|
203
|
+
# permalink for the entry. For example:
|
204
|
+
#
|
205
|
+
# https://example.com/index.html
|
206
|
+
# https://example.com/blog/my-trip-to-toronto.html
|
207
|
+
#
|
208
|
+
# This would be useful for indexes and feeds and so on.
|
209
|
+
#
|
210
|
+
# @return [String] A permalink to the current entry.
|
211
|
+
def permalink(host: Lifer.setting(:global, :host))
|
212
|
+
cached_permalink_variable =
|
213
|
+
"@entry_permalink_" + Digest::SHA1.hexdigest(host)
|
124
214
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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)
|
215
|
+
instance_variable_get(cached_permalink_variable) ||
|
216
|
+
instance_variable_set(
|
217
|
+
cached_permalink_variable,
|
218
|
+
File.join(
|
219
|
+
host,
|
220
|
+
Lifer::URIStrategy.find(collection.setting :uri_strategy)
|
221
|
+
.new(root: Lifer.root)
|
222
|
+
.output_file(self)
|
223
|
+
)
|
146
224
|
)
|
147
|
-
|
148
|
-
end
|
225
|
+
end
|
149
226
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
227
|
+
# The expected, absolute URI path to the entry. For example:
|
228
|
+
#
|
229
|
+
# /index.html
|
230
|
+
# /blog/my-trip-to-toronto.html
|
231
|
+
#
|
232
|
+
# @return [String] The absolute URI path to the entry.
|
233
|
+
def path = permalink(host: "/")
|
157
234
|
|
158
|
-
|
159
|
-
|
160
|
-
|
235
|
+
# If given a summary in the frontmatter of the entry, we can use this to
|
236
|
+
# provide a summary.
|
237
|
+
#
|
238
|
+
# Since subclasses may have more sophisticated access to the document, they
|
239
|
+
# may override this method with their own distinct implementations.
|
240
|
+
##
|
241
|
+
# @return [String] A summary of the entry.
|
242
|
+
def summary
|
243
|
+
return frontmatter[:summary] if frontmatter[:summary]
|
244
|
+
end
|
245
|
+
|
246
|
+
# Locates and returns all tags defined in the entry.
|
247
|
+
#
|
248
|
+
# @return [Array<Lifer::Tag>] The entry's tags.
|
249
|
+
def tags
|
250
|
+
@tags ||= candidate_tag_names
|
251
|
+
.map { Lifer::Tag.build_or_update(name: _1, entries: [self]) }
|
252
|
+
end
|
253
|
+
|
254
|
+
# Returns the title of the entry. Every entry subclass must implement this
|
255
|
+
# method so that builders have access to *some* kind of title for each entry.
|
256
|
+
#
|
257
|
+
# @return [String]
|
258
|
+
def title
|
259
|
+
raise NotImplementedError, I18n.t("shared.not_implemented_method")
|
260
|
+
end
|
261
|
+
|
262
|
+
def to_html
|
263
|
+
raise NotImplementedError, I18n.t("shared.not_implemented_method")
|
264
|
+
end
|
161
265
|
|
162
|
-
|
163
|
-
|
266
|
+
private
|
267
|
+
|
268
|
+
# It is conventional for users to use spaces or commas to delimit tags in
|
269
|
+
# other systems, so let's support that. But let's also support YAML-style
|
270
|
+
# arrays.
|
271
|
+
#
|
272
|
+
# @return [Array<String>] An array of candidate tag names.
|
273
|
+
def candidate_tag_names
|
274
|
+
case frontmatter[:tags]
|
275
|
+
when Array then frontmatter[:tags].map(&:to_s)
|
276
|
+
when String then frontmatter[:tags].split(TAG_DELIMITER_REGEX)
|
277
|
+
else []
|
278
|
+
end.uniq
|
279
|
+
end
|
280
|
+
|
281
|
+
def filename_date
|
282
|
+
return unless file && File.basename(file).match?(FILENAME_DATE_FORMAT)
|
283
|
+
|
284
|
+
File.basename(file).match(FILENAME_DATE_FORMAT)[1]
|
285
|
+
end
|
286
|
+
|
287
|
+
def frontmatter? = (full_text && full_text.match?(FRONTMATTER_REGEX))
|
164
288
|
end
|
165
289
|
end
|
166
|
-
|
data/lib/lifer/tag.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Lifer
|
2
|
+
# A tag is a way to categorize entries. You've likely encountered tags in
|
3
|
+
# other software before. In Lifer, tags are sort of the inverse of
|
4
|
+
# collections. It's a nice way to associate entries across many collections.
|
5
|
+
#
|
6
|
+
# Because tags are used to link entries, we definitely do not want duplicate
|
7
|
+
# tags. So the only way to build or retrieve tags is via the
|
8
|
+
# `.build_or_update` class method, which helps us responsibly manage the
|
9
|
+
# global tag manifest.
|
10
|
+
#
|
11
|
+
class Tag
|
12
|
+
class << self
|
13
|
+
# Builds or updates a Lifer tag. On update, its list of entries gets
|
14
|
+
# freshened.
|
15
|
+
#
|
16
|
+
# @param name [String] The name of the tag.
|
17
|
+
# @param entries [Array<Lifer::Entry>] A list of entries that should be
|
18
|
+
# associated with the tag. This parameter is not a true writer, in that
|
19
|
+
# if the tag already exists, old entry associations won't be removed--
|
20
|
+
# only appended to.
|
21
|
+
# @return [Lifer:Tag] The new or updated tag.
|
22
|
+
def build_or_update(name:, entries: [])
|
23
|
+
update(name:, entries:) || build(name:, entries:)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def build(name:, entries:)
|
29
|
+
if (new_tag = new(name:, entries:))
|
30
|
+
Lifer.tag_manifest << new_tag
|
31
|
+
end
|
32
|
+
new_tag || false
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(name:, entries:)
|
36
|
+
if (tag = Lifer.tags.detect { _1.name == name })
|
37
|
+
tag.instance_variable_set :@entries,
|
38
|
+
(tag.instance_variable_get(:@entries) | entries)
|
39
|
+
end
|
40
|
+
tag || false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_accessor :name
|
45
|
+
|
46
|
+
attr_reader :entries
|
47
|
+
|
48
|
+
def initialize(name:, entries:)
|
49
|
+
@name = name
|
50
|
+
@entries = entries
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/lifer/version.rb
CHANGED
data/lib/lifer.rb
CHANGED
@@ -141,6 +141,17 @@ module Lifer
|
|
141
141
|
#
|
142
142
|
# @return [Hash] The `Lifer::Config#settings`.
|
143
143
|
def settings = brain.config.settings
|
144
|
+
|
145
|
+
# All of the tags represented in Lifer entries for the current project.
|
146
|
+
#
|
147
|
+
# @return [Array<Lifer::Tag>] The complete list of tags.
|
148
|
+
def tags = brain.tags
|
149
|
+
|
150
|
+
# A set of all tags added to the project. Prefer using the `#tags` method
|
151
|
+
# for tag queries.
|
152
|
+
#
|
153
|
+
# @return [Set<Lifer::Tag>] The complete set of tags.
|
154
|
+
def tag_manifest = brain.tag_manifest
|
144
155
|
end
|
145
156
|
end
|
146
157
|
|
@@ -158,4 +169,5 @@ require_relative "lifer/builder"
|
|
158
169
|
require_relative "lifer/collection"
|
159
170
|
require_relative "lifer/entry"
|
160
171
|
require_relative "lifer/message"
|
172
|
+
require_relative "lifer/tag"
|
161
173
|
require_relative "lifer/uri_strategy"
|
data/lifer.gemspec
CHANGED
@@ -17,9 +17,9 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
18
|
|
19
19
|
spec.metadata["homepage_uri"] =
|
20
|
-
"%s/blob/%s/README.md" % [spec.homepage, Lifer::VERSION]
|
20
|
+
"%s/blob/%s/README.md" % [spec.homepage, "v#{Lifer::VERSION}"]
|
21
21
|
spec.metadata["source_code_uri"] =
|
22
|
-
"%s/tree/%s" % [spec.homepage, Lifer::VERSION]
|
22
|
+
"%s/tree/%s" % [spec.homepage, "v#{Lifer::VERSION}"]
|
23
23
|
spec.metadata["changelog_uri"] = "%s/blob/main/CHANGELOG.md" % spec.homepage
|
24
24
|
|
25
25
|
# Specify which files should be added to the gem when it is released. The
|
data/locales/en.yml
CHANGED
@@ -30,11 +30,10 @@ en:
|
|
30
30
|
content_type_not_implemented: no content type defined for files like %{path} yet
|
31
31
|
four_oh_four: 404 Not Found
|
32
32
|
entry:
|
33
|
+
date_error: "[%{filename}]: %{error}"
|
33
34
|
feedable_error: >
|
34
35
|
please set `%{entry_class}.include_in_feeds` to true or false
|
35
|
-
|
36
|
-
date_error: "[%{filename}]: %{error}"
|
37
|
-
no_date_metadata: "[%{filename}]: no date metadata"
|
36
|
+
no_date_metadata: "[%{filename}]: no date metadata"
|
38
37
|
not_found: >
|
39
38
|
file "%{file}" does not exist
|
40
39
|
config:
|
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.
|
4
|
+
version: 0.8.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: 2025-03-
|
11
|
+
date: 2025-03-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: i18n
|
@@ -167,6 +167,8 @@ files:
|
|
167
167
|
- lib/lifer/builder/html/from_liquid/drops/entry_drop.rb
|
168
168
|
- lib/lifer/builder/html/from_liquid/drops/frontmatter_drop.rb
|
169
169
|
- lib/lifer/builder/html/from_liquid/drops/settings_drop.rb
|
170
|
+
- lib/lifer/builder/html/from_liquid/drops/tag_drop.rb
|
171
|
+
- lib/lifer/builder/html/from_liquid/drops/tags_drop.rb
|
170
172
|
- lib/lifer/builder/html/from_liquid/filters.rb
|
171
173
|
- lib/lifer/builder/html/from_liquid/layout_tag.rb
|
172
174
|
- lib/lifer/builder/html/from_liquid/liquid_env.rb
|
@@ -188,6 +190,7 @@ files:
|
|
188
190
|
- lib/lifer/selection/included_in_feeds.rb
|
189
191
|
- lib/lifer/shared.rb
|
190
192
|
- lib/lifer/shared/finder_methods.rb
|
193
|
+
- lib/lifer/tag.rb
|
191
194
|
- lib/lifer/templates/cli.txt.erb
|
192
195
|
- lib/lifer/templates/config.yaml
|
193
196
|
- lib/lifer/templates/its-a-living.png
|
@@ -207,8 +210,8 @@ licenses:
|
|
207
210
|
- MIT
|
208
211
|
metadata:
|
209
212
|
allowed_push_host: https://rubygems.org
|
210
|
-
homepage_uri: https://github.com/benjaminwil/lifer/blob/
|
211
|
-
source_code_uri: https://github.com/benjaminwil/lifer/tree/
|
213
|
+
homepage_uri: https://github.com/benjaminwil/lifer/blob/v0.8.0/README.md
|
214
|
+
source_code_uri: https://github.com/benjaminwil/lifer/tree/v0.8.0
|
212
215
|
changelog_uri: https://github.com/benjaminwil/lifer/blob/main/CHANGELOG.md
|
213
216
|
post_install_message:
|
214
217
|
rdoc_options: []
|