lifer 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +26 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +110 -25
  7. data/LICENSE +18 -0
  8. data/README.md +79 -14
  9. data/Rakefile +2 -4
  10. data/bin/lifer +4 -2
  11. data/lib/lifer/brain.rb +171 -21
  12. data/lib/lifer/builder/html/from_erb.rb +92 -0
  13. data/lib/lifer/builder/html/from_liquid/drops/collection_drop.rb +40 -0
  14. data/lib/lifer/builder/html/from_liquid/drops/collections_drop.rb +40 -0
  15. data/lib/lifer/builder/html/from_liquid/drops/entry_drop.rb +63 -0
  16. data/lib/lifer/builder/html/from_liquid/drops/frontmatter_drop.rb +45 -0
  17. data/lib/lifer/builder/html/from_liquid/drops/settings_drop.rb +42 -0
  18. data/lib/lifer/builder/html/from_liquid/drops.rb +15 -0
  19. data/lib/lifer/builder/html/from_liquid/filters.rb +27 -0
  20. data/lib/lifer/builder/html/from_liquid/layout_tag.rb +67 -0
  21. data/lib/lifer/builder/html/from_liquid.rb +116 -0
  22. data/lib/lifer/builder/html.rb +107 -51
  23. data/lib/lifer/builder/rss.rb +113 -0
  24. data/lib/lifer/builder/txt.rb +60 -0
  25. data/lib/lifer/builder.rb +100 -1
  26. data/lib/lifer/cli.rb +105 -0
  27. data/lib/lifer/collection.rb +87 -8
  28. data/lib/lifer/config.rb +159 -31
  29. data/lib/lifer/dev/response.rb +61 -0
  30. data/lib/lifer/dev/router.rb +44 -0
  31. data/lib/lifer/dev/server.rb +97 -0
  32. data/lib/lifer/entry/html.rb +39 -0
  33. data/lib/lifer/entry/markdown.rb +162 -0
  34. data/lib/lifer/entry/txt.rb +41 -0
  35. data/lib/lifer/entry.rb +142 -41
  36. data/lib/lifer/message.rb +58 -0
  37. data/lib/lifer/selection/all_markdown.rb +16 -0
  38. data/lib/lifer/selection/included_in_feeds.rb +15 -0
  39. data/lib/lifer/selection.rb +79 -0
  40. data/lib/lifer/shared/finder_methods.rb +35 -0
  41. data/lib/lifer/shared.rb +6 -0
  42. data/lib/lifer/templates/cli.txt.erb +10 -0
  43. data/lib/lifer/templates/config.yaml +77 -0
  44. data/lib/lifer/templates/its-a-living.png +0 -0
  45. data/lib/lifer/templates/layout.html.erb +1 -1
  46. data/lib/lifer/uri_strategy/pretty.rb +14 -6
  47. data/lib/lifer/uri_strategy/pretty_root.rb +24 -0
  48. data/lib/lifer/uri_strategy/pretty_yyyy_mm_dd.rb +32 -0
  49. data/lib/lifer/uri_strategy/root.rb +17 -0
  50. data/lib/lifer/uri_strategy/simple.rb +10 -6
  51. data/lib/lifer/uri_strategy.rb +46 -6
  52. data/lib/lifer/utilities.rb +117 -0
  53. data/lib/lifer/version.rb +3 -0
  54. data/lib/lifer.rb +130 -23
  55. data/lifer.gemspec +12 -6
  56. data/locales/en.yml +54 -0
  57. metadata +142 -9
  58. data/lib/lifer/layout.rb +0 -25
  59. data/lib/lifer/templates/config +0 -4
  60. data/lib/lifer/uri_strategy/base.rb +0 -15
@@ -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