lifer 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +26 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +110 -25
  7. data/LICENSE +18 -0
  8. data/README.md +79 -14
  9. data/Rakefile +2 -4
  10. data/bin/lifer +4 -2
  11. data/lib/lifer/brain.rb +171 -21
  12. data/lib/lifer/builder/html/from_erb.rb +92 -0
  13. data/lib/lifer/builder/html/from_liquid/drops/collection_drop.rb +40 -0
  14. data/lib/lifer/builder/html/from_liquid/drops/collections_drop.rb +40 -0
  15. data/lib/lifer/builder/html/from_liquid/drops/entry_drop.rb +63 -0
  16. data/lib/lifer/builder/html/from_liquid/drops/frontmatter_drop.rb +45 -0
  17. data/lib/lifer/builder/html/from_liquid/drops/settings_drop.rb +42 -0
  18. data/lib/lifer/builder/html/from_liquid/drops.rb +15 -0
  19. data/lib/lifer/builder/html/from_liquid/filters.rb +27 -0
  20. data/lib/lifer/builder/html/from_liquid/layout_tag.rb +67 -0
  21. data/lib/lifer/builder/html/from_liquid.rb +116 -0
  22. data/lib/lifer/builder/html.rb +107 -51
  23. data/lib/lifer/builder/rss.rb +113 -0
  24. data/lib/lifer/builder/txt.rb +60 -0
  25. data/lib/lifer/builder.rb +100 -1
  26. data/lib/lifer/cli.rb +105 -0
  27. data/lib/lifer/collection.rb +87 -8
  28. data/lib/lifer/config.rb +159 -31
  29. data/lib/lifer/dev/response.rb +61 -0
  30. data/lib/lifer/dev/router.rb +44 -0
  31. data/lib/lifer/dev/server.rb +97 -0
  32. data/lib/lifer/entry/html.rb +39 -0
  33. data/lib/lifer/entry/markdown.rb +162 -0
  34. data/lib/lifer/entry/txt.rb +41 -0
  35. data/lib/lifer/entry.rb +142 -41
  36. data/lib/lifer/message.rb +58 -0
  37. data/lib/lifer/selection/all_markdown.rb +16 -0
  38. data/lib/lifer/selection/included_in_feeds.rb +15 -0
  39. data/lib/lifer/selection.rb +79 -0
  40. data/lib/lifer/shared/finder_methods.rb +35 -0
  41. data/lib/lifer/shared.rb +6 -0
  42. data/lib/lifer/templates/cli.txt.erb +10 -0
  43. data/lib/lifer/templates/config.yaml +77 -0
  44. data/lib/lifer/templates/its-a-living.png +0 -0
  45. data/lib/lifer/templates/layout.html.erb +1 -1
  46. data/lib/lifer/uri_strategy/pretty.rb +14 -6
  47. data/lib/lifer/uri_strategy/pretty_root.rb +24 -0
  48. data/lib/lifer/uri_strategy/pretty_yyyy_mm_dd.rb +32 -0
  49. data/lib/lifer/uri_strategy/root.rb +17 -0
  50. data/lib/lifer/uri_strategy/simple.rb +10 -6
  51. data/lib/lifer/uri_strategy.rb +46 -6
  52. data/lib/lifer/utilities.rb +117 -0
  53. data/lib/lifer/version.rb +3 -0
  54. data/lib/lifer.rb +130 -23
  55. data/lifer.gemspec +12 -6
  56. data/locales/en.yml +54 -0
  57. metadata +142 -9
  58. data/lib/lifer/layout.rb +0 -25
  59. data/lib/lifer/templates/config +0 -4
  60. data/lib/lifer/uri_strategy/base.rb +0 -15
@@ -0,0 +1,92 @@
1
+ require "erb"
2
+
3
+ class Lifer::Builder::HTML
4
+ # If the HTML builder is given an ERB template, it uses this class to parse
5
+ # the ERB into HTML. Lifer project metadata is provided as context. For
6
+ # example:
7
+ #
8
+ # <html>
9
+ # <head>
10
+ # <title><%= my_collection.name %></title>
11
+ # </head>
12
+ #
13
+ # <body>
14
+ # <h1><%= my_collection.name %></h1>
15
+ #
16
+ # <% my_collection.entries.each do |entry| %>
17
+ # <section>
18
+ # <h2><%= entry.title %></h2>
19
+ # <p><%= entry.summary %></p>
20
+ # <a href="<%= entry.permalink %>">Read more</a>
21
+ # </section>
22
+ # <% end %>
23
+ # </body>
24
+ # </html>
25
+ #
26
+ class FromERB
27
+ class << self
28
+ # Build and render an entry.
29
+ #
30
+ # @param entry [Lifer::Entry] The entry to be rendered.
31
+ # @return [String] The rendered entry.
32
+ def build(entry:)
33
+ new(entry: entry).render
34
+ end
35
+ end
36
+
37
+ # Reads the entry as ERB, given our renderer context (see the documentation
38
+ # for `#build_binding_context`) and renders the production-ready entry.
39
+ #
40
+ # @return [String] The rendered entry.
41
+ def render
42
+ ERB.new(File.read layout_file).result context
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :context, :entry, :layout_file
48
+
49
+ # @private
50
+ # @param entry [Lifer::Entry] The entry to be rendered.
51
+ # @return [void]
52
+ def initialize(entry:)
53
+ @entry = entry
54
+ @layout_file = entry.collection.layout_file
55
+ @context = build_binding_context
56
+ end
57
+
58
+ # @private
59
+ # Each collection name is provided as a local variable. This allows you to
60
+ # make ERB files that contain loops like:
61
+ #
62
+ # <% my_collection_name.entries.each do |entry| %>
63
+ # <%= entry.title %>
64
+ # <% end %>
65
+ #
66
+ # @return [Binding] A binding object with preset context from the current
67
+ # Lifer project and in-scope entry.
68
+ def build_binding_context
69
+ binding.tap { |binding|
70
+ Lifer.collections.each do |collection|
71
+ binding.local_variable_set collection.name, collection
72
+
73
+ collection_context_class.define_method(collection.name) do
74
+ collection
75
+ end
76
+ end
77
+
78
+ collections = collection_context_class.new Lifer.collections.to_a
79
+
80
+ binding.local_variable_set :collections, collections
81
+ binding.local_variable_set :settings, Lifer.settings
82
+ binding.local_variable_set :content,
83
+ ERB.new(entry.to_html).result(binding)
84
+ }
85
+ end
86
+
87
+ # @private
88
+ def collection_context_class
89
+ @collection_context_class ||= Class.new(Array) do end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,40 @@
1
+ module Lifer::Builder::HTML::FromLiquid::Drops
2
+ # This drop allows users to access Lifer collection information from within
3
+ # Liquid templates. Example:
4
+ #
5
+ # {{ collection.name }}
6
+ #
7
+ # {% for entries in collection.entries %}
8
+ # {{ entry.title }}
9
+ # {% endfor %}
10
+ #
11
+ class CollectionDrop < Liquid::Drop
12
+ attr_accessor :lifer_collection
13
+
14
+ def initialize(lifer_collection) = (@lifer_collection = lifer_collection)
15
+
16
+ # The collection name.
17
+ #
18
+ # @return [Symbol]
19
+ def name = (@name ||= lifer_collection.name)
20
+
21
+ # Gets all entries in a collection and converts them to entry drops that can
22
+ # be accessed in Liquid templates. Example:
23
+ #
24
+ # {% for entry in collections.root.entries %}
25
+ # {{ entry.title }}
26
+ # {% endfor %}
27
+ #
28
+ # @return [Array<EntryDrop>]
29
+ def entries
30
+ @entries ||= lifer_collection.entries.map {
31
+ EntryDrop.new _1, collection: self
32
+ }
33
+ end
34
+
35
+ # The collection's layout file path.
36
+ #
37
+ # @return [String] The path to the layout file.
38
+ def layout_file = (@lifer_collection.layout_file)
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ module Lifer::Builder::HTML::FromLiquid::Drops
2
+ # This drop allows users to iterate over their Lifer collections in Liquid
3
+ # templates. Example:
4
+ #
5
+ # {% for collection in collections %}
6
+ # {{ collection.name }}
7
+ # {% endfor %}
8
+ #
9
+ class CollectionsDrop < Liquid::Drop
10
+ attr_accessor :collections
11
+
12
+ def initialize
13
+ @collections = Lifer.collections.map { CollectionDrop.new _1 }
14
+ end
15
+
16
+ # Allow collections to be iterable in Liquid templates.
17
+ #
18
+ # @yield [CollectionDrop] All available collection drops.
19
+ def each(&block)
20
+
21
+ collections.each(&block)
22
+ end
23
+
24
+ # Allow collections to be rendered as an array in Liquid templates.
25
+ #
26
+ # @return [Array]
27
+ def to_a = @collections
28
+
29
+ # Dynamically define Liquid accessors based on the Lifer project's
30
+ # collection names. For example, to get the root collection's name:
31
+ #
32
+ # {{ collections.root.name }}
33
+ #
34
+ # @param arg [String] The name of a collection.
35
+ # @return [CollectionDrop, NilClass]
36
+ def liquid_method_missing(arg)
37
+ collections.detect { arg.to_sym == _1.name }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,63 @@
1
+ module Lifer::Builder::HTML::FromLiquid::Drops
2
+ # This drop represents a Lifer entry and allows users to access entry
3
+ # metadata and content in Liquid templates.
4
+ #
5
+ # Example usage:
6
+ #
7
+ # <h1>{{ entry.title }}</h1>
8
+ # <small>Published on <datetime>{{ entry.date }}</datetime></small>
9
+ #
10
+ class EntryDrop < Liquid::Drop
11
+ attr_accessor :lifer_entry, :collection
12
+
13
+ def initialize(lifer_entry, collection:)
14
+ @lifer_entry = lifer_entry
15
+ @collection = collection
16
+ end
17
+
18
+ # The entry author (or authors).
19
+ #
20
+ # @return [String]
21
+ def author = authors
22
+
23
+ # The entry authors (or author).
24
+ #
25
+ # @return [String]
26
+ def authors = (@authors ||= lifer_entry.authors.join(", "))
27
+
28
+ # The entry content.
29
+ #
30
+ # @return [String]
31
+ def content = (@content ||= lifer_entry.to_html)
32
+
33
+ # The entry date (as a string).
34
+ #
35
+ # @return [String]
36
+ def date = (@date ||= lifer_entry.date)
37
+
38
+ # The entry frontmatter data.
39
+ #
40
+ # @return [FrontmatterDrop]
41
+ def frontmatter = (@frontmatter ||= FrontmatterDrop.new(lifer_entry))
42
+
43
+ # The path to the entry.
44
+ #
45
+ # @return [String] The path to the entry.
46
+ def path = (@path ||= lifer_entry.path)
47
+
48
+ # The entry permalink.
49
+ #
50
+ # @return [String] The entry permalink.
51
+ def permalink = (@permalink ||= lifer_entry.permalink)
52
+
53
+ # The summary of the entry.
54
+ #
55
+ # @return [String] The summary of the entry.
56
+ def summary = (@summary ||= lifer_entry.summary)
57
+
58
+ # The entry title.
59
+ #
60
+ # @return [String] The entry title.
61
+ def title = (@title ||= lifer_entry.title)
62
+ end
63
+ end
@@ -0,0 +1,45 @@
1
+ module Lifer::Builder::HTML::FromLiquid::Drops
2
+ # Markdown entries may contain YAML frontmatter. And if they do, we need a way
3
+ # for the Liquid templates to access that data.
4
+ #
5
+ # Example usage:
6
+ #
7
+ # {{ entry.frontmatter.any_available_frontmatter_key }}
8
+ #
9
+ class FrontmatterDrop < Liquid::Drop
10
+ def initialize(entry)
11
+ @frontmatter = Lifer::Utilities.stringify_keys(entry.frontmatter)
12
+ end
13
+
14
+ # Ensure that the frontmatter can be output wholly into a rendered template
15
+ # if need be.
16
+ #
17
+ # @return [String]
18
+ def to_s = frontmatter.to_json
19
+
20
+ # Dynamically define Liquid accessors based on the Lifer settings object.
21
+ # For example, to get a collections URI strategy:
22
+ #
23
+ # {{ settings.my_collection.uri_strategy }}
24
+ #
25
+ # @param arg [String] The name of a collection.
26
+ # @return [CollectionDrop, NilClass]
27
+ def liquid_method_missing(arg)
28
+ value = frontmatter[arg]
29
+
30
+ if value.is_a?(Hash)
31
+ as_drop(value)
32
+ elsif value.is_a?(Array) && value.all? { _1.is_a?(Hash) }
33
+ value.map { as_drop(_1) }
34
+ else
35
+ value
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :frontmatter
42
+
43
+ def as_drop(hash) = self.class.new(hash)
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ module Lifer::Builder::HTML::FromLiquid::Drops
2
+ # This drop allows users to access the current Lifer project settings from
3
+ # Liquid templates. Example:
4
+ #
5
+ # {{ settings.my_collection.uri_strategy }}
6
+ #
7
+ class SettingsDrop < Liquid::Drop
8
+ def initialize(settings = Lifer.settings)
9
+ @settings = Lifer::Utilities.stringify_keys(settings)
10
+ end
11
+
12
+ # Ensure the settings tree can be output to a rendered template if need be.
13
+ #
14
+ # @return [String]
15
+ def to_s = settings.to_json
16
+
17
+ # Dynamically define Liquid accessors based on the Lifer settings object.
18
+ # For example, to get a collections URI strategy:
19
+ #
20
+ # {{ settings.my_collection.uri_strategy }}
21
+ #
22
+ # @param arg [String] The name of a collection.
23
+ # @return [CollectionDrop, NilClass]
24
+ def liquid_method_missing(arg)
25
+ value = settings[arg]
26
+
27
+ if value.is_a?(Hash)
28
+ as_drop(value)
29
+ elsif value.is_a?(Array) && value.all? { _1.is_a?(Hash) }
30
+ value.map { as_drop(_1) }
31
+ else
32
+ value
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ attr_accessor :settings
39
+
40
+ def as_drop(hash) = self.class.new(hash)
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ class Lifer::Builder::HTML::FromLiquid
2
+ # This module contains all of custom Liquid data drops used in order to render
3
+ # Lifer entries.
4
+ #
5
+ # For more information about drops, see the `liquid` gem source code. (The
6
+ # docs are awful.)
7
+ #
8
+ module Drops; end
9
+ end
10
+
11
+ require_relative "drops/collection_drop"
12
+ require_relative "drops/collections_drop"
13
+ require_relative "drops/entry_drop"
14
+ require_relative "drops/frontmatter_drop"
15
+ require_relative "drops/settings_drop"
@@ -0,0 +1,27 @@
1
+ # This module provides Liquid filters to be used within Liquid templates.
2
+ # In many cases these utilities exist to be pseudo-compatible with Jekyll.
3
+ #
4
+ # For example, a filter (in a Liquid template):
5
+ #
6
+ # {{ entry.date | date_to_xmlschema }}
7
+ #
8
+ module Lifer::Builder::HTML::FromLiquid::Filters
9
+ # @!visibility private
10
+ Util = Lifer::Utilities
11
+
12
+ # Converts date to ISO-8601 format.
13
+ #
14
+ # @param input [String] A date string, I hope.
15
+ # @return [String] The transformed date string.
16
+ def date_to_xmlschema(input) = Util.date_as_iso8601(input)
17
+
18
+ # Transforms a string to kabab-case.
19
+ #
20
+ # For example:
21
+ #
22
+ # Before: hello_there
23
+ # After: hello-there
24
+ # @param input [String] A string.
25
+ # @return [String] The transformed string.
26
+ def handleize(input) = Util.handleize(input)
27
+ end
@@ -0,0 +1,67 @@
1
+ class Lifer::Builder::HTML::FromLiquid
2
+ # Note that if you want to learn more about the shape of this class, check out
3
+ # `Liquid::Block` in the `liquid` gem.
4
+ #
5
+ # The layout tag is a bit magic. The idea here is to emulate how Jekyll
6
+ # handles `layout:` YAML frontmatter within entries to change the normal
7
+ # parent layout to an override parent layout--but without the need for
8
+ # frontmatter.
9
+ #
10
+ # The reason we took this strategy was to avoid pre-processing every entry for
11
+ # frontmatter when we didn't need to. Maybe in the long run this was a bad
12
+ # call? I don't know.
13
+ #
14
+ # Example usage (from a Liquid template):
15
+ #
16
+ # {% layout "path/to/my_liquid_layout_template" %}
17
+ #
18
+ # (The required `endlayout` tag will be appended to the end of the file
19
+ # on render if you do not insert it yourself.
20
+ #
21
+ class LayoutTag < Liquid::Block
22
+ # The name of the tag in Liquid templates, `layout`.
23
+ #
24
+ NAME = :layout
25
+
26
+ # The end name of the tag in Liquid templates, `endlayout`.
27
+ #
28
+ ENDNAME = ("end%s" % NAME).to_sym
29
+
30
+ def initialize(layout, path, options)
31
+ @path = path.delete("\"").strip
32
+ super
33
+ end
34
+
35
+ # A layout tag wraps an entire document and outputs it inside of whatever
36
+ # the `@layout` is. This lets a child document specify a parernt layout!
37
+ # Very confusing stuff.
38
+ #
39
+ # @param context [Liquid::Context] All of the context of the Liquid
40
+ # document that would be rendered.
41
+ # @return [String] A rendered document.
42
+ def render(context)
43
+ document_context = context.environments.first
44
+ parse_options = document_context["parse_options"]
45
+ liquid_file_system = parse_options[:environment].file_system
46
+ render_options = document_context["render_options"]
47
+
48
+ current_layout_file = File
49
+ .read(document_context["entry"]["collection"]["layout_file"])
50
+ .gsub(/\{%\s*#{tag_name}.+%\}/, "")
51
+
52
+ content_with_layout = Liquid::Template
53
+ .parse(current_layout_file, error_mode: :strict)
54
+ .render(document_context, render_options)
55
+
56
+ Liquid::Template
57
+ .parse(
58
+ liquid_file_system.read_template_file(@path),
59
+ **parse_options
60
+ )
61
+ .render(
62
+ document_context.merge({"content" => content_with_layout}),
63
+ **render_options
64
+ )
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,116 @@
1
+ require "liquid"
2
+
3
+ require_relative "from_liquid/drops"
4
+ require_relative "from_liquid/filters"
5
+ require_relative "from_liquid/layout_tag"
6
+
7
+ class Lifer::Builder::HTML
8
+ # If the HTML builder is given a Liquid template, it uses this class to parse
9
+ # the Liquid into HTML. Lifer project metadata is provided as context. For
10
+ # example:
11
+ #
12
+ # <html>
13
+ # <head>
14
+ # <title>{{ collections.my_collection.name }}</title>
15
+ # </head>
16
+ #
17
+ # <body>
18
+ # <h1>{{ collections.my_collection.name }}</h1>
19
+ #
20
+ # {% for entry in collections.my_collection.entries %}
21
+ # <section>
22
+ # <h2>{{ entry.title }}</h2>
23
+ # <p>{{ entry.summary }}</p>
24
+ # <a href="{{ entry.permalink }}">Read more</a>
25
+ # </section>
26
+ # {% endfor %}
27
+ # </body>
28
+ # </html>
29
+ #
30
+ class FromLiquid
31
+ class << self
32
+ # Render and build a Lifer entry.
33
+ #
34
+ # @param entry [Lifer::Entry] The entry to render.
35
+ # @return [String] The rendered entry, ready for output.
36
+ def build(entry:) = new(entry:).render
37
+ end
38
+
39
+ attr_accessor :entry, :layout_file
40
+
41
+ # Reads the entry as Liquid, given our document context, and renders
42
+ # an entry.
43
+ #
44
+ # @return [String] The rendered entry.
45
+ def render
46
+ document_context = context.merge!(
47
+ "content" => Liquid::Template
48
+ .parse(entry.to_html, **parse_options)
49
+ .render(context, **render_options)
50
+ )
51
+ Liquid::Template
52
+ .parse(layout, **parse_options)
53
+ .render(document_context, **render_options)
54
+ end
55
+
56
+ private
57
+
58
+ def initialize(entry:)
59
+ @entry = entry
60
+ @layout_file = entry.collection.layout_file
61
+ end
62
+
63
+ def context
64
+ collections = Drops::CollectionsDrop.new
65
+ collection = collections
66
+ .to_a
67
+ .detect { _1.name.to_sym == entry.collection.name }
68
+
69
+ {
70
+ "collections" => collections,
71
+ "entry" => Drops::EntryDrop.new(entry, collection:),
72
+ "parse_options" => parse_options,
73
+ "render_options" => render_options,
74
+ "settings" => Drops::SettingsDrop.new
75
+ }
76
+ end
77
+
78
+ # @private
79
+ # It's possible for the provided layout to request a parent layout, which
80
+ # makes this method a bit complicated.
81
+ #
82
+ # @return [String] A Liquid layout document, ready for parsing.
83
+ def layout
84
+ contents = File.read layout_file
85
+
86
+ return contents unless contents.match?(/\{%\s*#{LayoutTag::NAME}.*%\}/)
87
+
88
+ contents + "\n{% #{LayoutTag::ENDNAME} %}"
89
+ end
90
+
91
+ def liquid_environment
92
+ @liquid_environment ||= Liquid::Environment.build do |environment|
93
+ environment.file_system =
94
+ Liquid::LocalFileSystem.new(Lifer.root, "%s.html.liquid")
95
+
96
+ environment.register_filter Lifer::Builder::HTML::FromLiquid::Filters
97
+ environment.register_tag "layout",
98
+ Lifer::Builder::HTML::FromLiquid::LayoutTag
99
+ end
100
+ end
101
+
102
+ def parse_options
103
+ {
104
+ environment: liquid_environment,
105
+ error_mode: :strict
106
+ }
107
+ end
108
+
109
+ def render_options
110
+ {
111
+ strict_variables: true,
112
+ strict_filters: true
113
+ }
114
+ end
115
+ end
116
+ end