luoma 0.1.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 +7 -0
- checksums.yaml.gz.sig +2 -0
- data/.rdoc_options +16 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/Rakefile +69 -0
- data/Steepfile +41 -0
- data/certs/jgrp.pem +27 -0
- data/docs/configuration.md +180 -0
- data/docs/custom_filters.md +3 -0
- data/docs/custom_tags.md +3 -0
- data/docs/expressions.md +3 -0
- data/docs/extension_types.md +41 -0
- data/docs/filter_reference.md +1702 -0
- data/docs/index.md +73 -0
- data/docs/luoma_for_template_authors.md +3 -0
- data/docs/markdown.md +111 -0
- data/docs/predicate_reference.md +3 -0
- data/docs/static_analysis.md +3 -0
- data/docs/tag_reference.md +352 -0
- data/docs/template_loaders.md +177 -0
- data/docs/undefined_variables.md +3 -0
- data/docs-requirements.txt +11 -0
- data/docs_/cycle.md +17 -0
- data/docs_/font_rendering_example.md +157 -0
- data/docs_/header_block_example.md +51 -0
- data/docs_/increment_and_decrement.md +49 -0
- data/docs_/logo_style_example.md +40 -0
- data/docs_/migration.md +11 -0
- data/docs_/notes.md +19 -0
- data/lib/luoma/cache.rb +80 -0
- data/lib/luoma/chain_hash.rb +54 -0
- data/lib/luoma/context.rb +227 -0
- data/lib/luoma/drop.rb +84 -0
- data/lib/luoma/drops/block.rb +33 -0
- data/lib/luoma/drops/expression.rb +29 -0
- data/lib/luoma/drops/html_safe.rb +36 -0
- data/lib/luoma/drops/range.rb +69 -0
- data/lib/luoma/drops/undefined.rb +119 -0
- data/lib/luoma/environment.rb +479 -0
- data/lib/luoma/errors.rb +79 -0
- data/lib/luoma/escape.rb +35 -0
- data/lib/luoma/expression.rb +1271 -0
- data/lib/luoma/filter.rb +97 -0
- data/lib/luoma/filters/array.rb +372 -0
- data/lib/luoma/filters/date.rb +19 -0
- data/lib/luoma/filters/default.rb +16 -0
- data/lib/luoma/filters/json.rb +14 -0
- data/lib/luoma/filters/math.rb +84 -0
- data/lib/luoma/filters/size.rb +13 -0
- data/lib/luoma/filters/slice.rb +52 -0
- data/lib/luoma/filters/sort.rb +113 -0
- data/lib/luoma/filters/string.rb +186 -0
- data/lib/luoma/fnv.rb +15 -0
- data/lib/luoma/lexer.rb +106 -0
- data/lib/luoma/lexer_legacy.rb +396 -0
- data/lib/luoma/lexer_unified.rb +279 -0
- data/lib/luoma/loader.rb +50 -0
- data/lib/luoma/loaders/choice_loader.rb +20 -0
- data/lib/luoma/loaders/file_system_loader.rb +75 -0
- data/lib/luoma/loaders/mixins.rb +52 -0
- data/lib/luoma/markup.rb +109 -0
- data/lib/luoma/parser.rb +252 -0
- data/lib/luoma/parser_unified.rb +979 -0
- data/lib/luoma/predicates/blank.rb +19 -0
- data/lib/luoma/predicates/defined.rb +10 -0
- data/lib/luoma/predicates/empty.rb +15 -0
- data/lib/luoma/predicates/type.rb +35 -0
- data/lib/luoma/static_analysis.rb +427 -0
- data/lib/luoma/tags/assign.rb +63 -0
- data/lib/luoma/tags/break.rb +23 -0
- data/lib/luoma/tags/capture.rb +43 -0
- data/lib/luoma/tags/case.rb +137 -0
- data/lib/luoma/tags/comment.rb +17 -0
- data/lib/luoma/tags/continue.rb +23 -0
- data/lib/luoma/tags/define.rb +42 -0
- data/lib/luoma/tags/else.rb +30 -0
- data/lib/luoma/tags/for.rb +108 -0
- data/lib/luoma/tags/if.rb +109 -0
- data/lib/luoma/tags/import.rb +91 -0
- data/lib/luoma/tags/include.rb +77 -0
- data/lib/luoma/tags/output.rb +20 -0
- data/lib/luoma/tags/raw.rb +26 -0
- data/lib/luoma/tags/render.rb +92 -0
- data/lib/luoma/tags/with.rb +55 -0
- data/lib/luoma/template.rb +178 -0
- data/lib/luoma/token.rb +84 -0
- data/lib/luoma/version.rb +5 -0
- data/lib/luoma.rb +75 -0
- data/sig/luoma/cache.rbs +44 -0
- data/sig/luoma/chain_hash.rbs +26 -0
- data/sig/luoma/context.rbs +115 -0
- data/sig/luoma/drop.rbs +44 -0
- data/sig/luoma/drops/block.rbs +10 -0
- data/sig/luoma/drops/expression.rbs +10 -0
- data/sig/luoma/drops/html_safe.rbs +14 -0
- data/sig/luoma/drops/range.rbs +15 -0
- data/sig/luoma/drops/undefined.rbs +27 -0
- data/sig/luoma/environment.rbs +212 -0
- data/sig/luoma/errors.rbs +67 -0
- data/sig/luoma/escape.rbs +15 -0
- data/sig/luoma/expression.rbs +514 -0
- data/sig/luoma/filter.rbs +66 -0
- data/sig/luoma/filters/array.rbs +74 -0
- data/sig/luoma/filters/date.rbs +9 -0
- data/sig/luoma/filters/default.rbs +8 -0
- data/sig/luoma/filters/json.rbs +7 -0
- data/sig/luoma/filters/math.rbs +37 -0
- data/sig/luoma/filters/size.rbs +6 -0
- data/sig/luoma/filters/slice.rbs +10 -0
- data/sig/luoma/filters/sort.rbs +17 -0
- data/sig/luoma/filters/string.rbs +103 -0
- data/sig/luoma/fnv.rbs +3 -0
- data/sig/luoma/lexer.rbs +42 -0
- data/sig/luoma/lexer_legacy.rbs +81 -0
- data/sig/luoma/lexer_unified.rbs +49 -0
- data/sig/luoma/loader.rbs +40 -0
- data/sig/luoma/loaders/choice_loader.rbs +9 -0
- data/sig/luoma/loaders/file_system_loader.rbs +19 -0
- data/sig/luoma/loaders/mixins.rbs +16 -0
- data/sig/luoma/markup.rbs +68 -0
- data/sig/luoma/parser.rbs +105 -0
- data/sig/luoma/parser_unified.rbs +150 -0
- data/sig/luoma/predicates/blank.rbs +6 -0
- data/sig/luoma/predicates/defined.rbs +6 -0
- data/sig/luoma/predicates/empty.rbs +6 -0
- data/sig/luoma/predicates/type.rbs +21 -0
- data/sig/luoma/static_analysis.rbs +141 -0
- data/sig/luoma/tags/assign.rbs +12 -0
- data/sig/luoma/tags/break.rbs +11 -0
- data/sig/luoma/tags/capture.rbs +14 -0
- data/sig/luoma/tags/case.rbs +36 -0
- data/sig/luoma/tags/comment.rbs +11 -0
- data/sig/luoma/tags/continue.rbs +11 -0
- data/sig/luoma/tags/define.rbs +16 -0
- data/sig/luoma/tags/else.rbs +17 -0
- data/sig/luoma/tags/for.rbs +26 -0
- data/sig/luoma/tags/if.rbs +45 -0
- data/sig/luoma/tags/import.rbs +13 -0
- data/sig/luoma/tags/include.rbs +13 -0
- data/sig/luoma/tags/output.rbs +13 -0
- data/sig/luoma/tags/raw.rbs +11 -0
- data/sig/luoma/tags/render.rbs +13 -0
- data/sig/luoma/tags/with.rbs +15 -0
- data/sig/luoma/template.rbs +117 -0
- data/sig/luoma/token.rbs +81 -0
- data/sig/luoma.rbs +11 -0
- data/zensical.toml +363 -0
- data.tar.gz.sig +0 -0
- metadata +233 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Template loaders
|
|
2
|
+
|
|
3
|
+
A template loader is responsible for finding template source text given a name or identifier and will be called upon `Luoma::Environment#get_template` or by tags such as `{% render %}` and `{% include %}`. When a template can not be found, a `Luoma::TemplateNotFoundError` or `Luoma::NoSuchTemplateError` is thrown.
|
|
4
|
+
|
|
5
|
+
Every `Luoma::Environment` has exactly one configured template loader, the default of which is an empty `Luoma::HashLoader`, meaning `Environment#get_template`, `{% render %}` and `{% include %}` will always throw an error.
|
|
6
|
+
|
|
7
|
+
!!! info
|
|
8
|
+
|
|
9
|
+
Both `NoSuchTemplateError` and `TemplateNotFoundError` inherit from `Luoma::LuomaError`. The former is a detailed error including diagnostic information about where in the parent template `{% render %}`, `{% include %}` or `{% import %}` was called.
|
|
10
|
+
|
|
11
|
+
`NoSuchTemplateError` does not include diagnostic information. It is thrown by template loaders and surfaces when there is no parent template available, like when calling `Environment#get_template` directly.
|
|
12
|
+
|
|
13
|
+
## Built-in loaders
|
|
14
|
+
|
|
15
|
+
### `HashLoader`
|
|
16
|
+
|
|
17
|
+
`Luoma::HashLoader` is a template loader that stores template source code in a hash, mapping strings to strings. Hash keys are template names and values are template source code.
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "luoma"
|
|
21
|
+
|
|
22
|
+
loader = Luoma::HashLoader.new(
|
|
23
|
+
"index" => "This is the index. {% render 'section' %}",
|
|
24
|
+
"section" => "Hi!"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
env = Luoma::Environment.new(loader: loader)
|
|
28
|
+
|
|
29
|
+
template = env.get_template("index")
|
|
30
|
+
puts template.render
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `FileSystemLoader`
|
|
34
|
+
|
|
35
|
+
`Luoma::FileSystemLoader` is a template loader that reads template source text from files on a file system.
|
|
36
|
+
|
|
37
|
+
`FileSystemLoader.new` accepts a path or array of paths to search for files containing template source code. The `default_extension` keyword argument is a default file extension appended to the template name if it does not already have an extension.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "luoma"
|
|
41
|
+
|
|
42
|
+
loader = Luoma::FileSystemLoader.new(
|
|
43
|
+
"./templates",
|
|
44
|
+
default_extension: ".luoma"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
env = Luoma::Environment.new(loader: loader)
|
|
48
|
+
|
|
49
|
+
# Look for `index.luoma` in the `templates` folder relative to the
|
|
50
|
+
# current working directory.
|
|
51
|
+
template = env.get_template("index")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `CachingFileSystemLoader`
|
|
55
|
+
|
|
56
|
+
`Luoma::CachingFileSystemLoader` is similar to `FileSystemLoader`, but caches parsed templates in an in-memory, least recently used (LRU) cache. The default cache size is 300 templates.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require "luoma"
|
|
60
|
+
|
|
61
|
+
loader = Luoma::CachingFileSystemLoader.new(
|
|
62
|
+
"./templates",
|
|
63
|
+
default_extension: ".luoma",
|
|
64
|
+
capacity: 500
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
env = Luoma::Environment.new(loader: loader)
|
|
68
|
+
|
|
69
|
+
# Look for `index.luoma` in the `templates` folder relative to the
|
|
70
|
+
# current working directory.
|
|
71
|
+
template = env.get_template("index")
|
|
72
|
+
|
|
73
|
+
# Get's cached `Luoma::Template` instance.
|
|
74
|
+
same_template = env.get_template("index")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `ChoiceLoader`
|
|
78
|
+
|
|
79
|
+
`Luoma::ChoiceLoader` is a template loader that delegates to other template loaders. Given an array of `Luoma::TemplateLoader` instances, `ChoiceLoader` will try each in turn until a template is found.
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
require "luoma"
|
|
83
|
+
|
|
84
|
+
fallback_loader = Luoma::HashLoader.new(
|
|
85
|
+
"layout" => "A default layout {% include content %}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
dynamic_loader = Luoma::CachingFileSystemLoader.new(
|
|
89
|
+
"./templates",
|
|
90
|
+
default_extension: ".luoma",
|
|
91
|
+
capacity: 500,
|
|
92
|
+
auto_reload: true
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
env = Luoma::Environment.new(
|
|
96
|
+
loader: Luoma::ChoiceLoader.new([dynamic_loader, fallback_loader])
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Look for `layout.luoma` first, fallback to the hash loader if it
|
|
100
|
+
# does not exist.
|
|
101
|
+
template = env.get_template("layout")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Custom loaders
|
|
105
|
+
|
|
106
|
+
You are encouraged to write you own template loaders to read templates from a database or process front-matter, for example. Simply extend `Luoma::TemplateLoader` and implement `#get_source`.
|
|
107
|
+
|
|
108
|
+
```ruby title="Method signature"
|
|
109
|
+
def get_source: (
|
|
110
|
+
Luoma::Environment env,
|
|
111
|
+
::String name,
|
|
112
|
+
?context: Luoma::RenderContext? context,
|
|
113
|
+
**untyped kwargs
|
|
114
|
+
) -> Luoma::TemplateSource
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This example implements a simple front matter loader, which reads data from the start of a file and template source text from the rest.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
require "luoma"
|
|
121
|
+
require "yaml"
|
|
122
|
+
|
|
123
|
+
class FrontMatterLoader < Luoma::CachingFileSystemLoader
|
|
124
|
+
RE_FRONT_MATTER = /\A\s*---\s*(.*?)\s*---\s*/m
|
|
125
|
+
|
|
126
|
+
def get_source(env, name, context: nil, **)
|
|
127
|
+
template_meta = super
|
|
128
|
+
match = RE_FRONT_MATTER.match(template_meta.source)
|
|
129
|
+
|
|
130
|
+
return template_meta unless match
|
|
131
|
+
|
|
132
|
+
# TODO: YAML error handling and validation.
|
|
133
|
+
template_meta.matter = YAML.load(match[1])
|
|
134
|
+
template_meta.source = template_meta.source.slice(match[0].length..)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Load context
|
|
140
|
+
|
|
141
|
+
Sometimes a template's name alone is not enough to identify the correct source text. For such cases, `TemplateLoader#get_source` accepts optional `context` and `kwargs` to help narrow the template search space.
|
|
142
|
+
|
|
143
|
+
`context` will be set to the current `Luoma::RenderContext`, if one is available. `context` will always be `nil` when calling `Luoma::Environment#get_template` directly, as a render context does not yet exist.
|
|
144
|
+
|
|
145
|
+
`kwargs` is an arbitrary mapping of strings to unknown objects. You can use this to bootstrap the loading sequence at parse time by passing arguments to `Environment#get_template`.
|
|
146
|
+
|
|
147
|
+
By convention, built-in tags that load partial templates - `{% include %}`, `{% render %}` and `{% import %}` - set a `tag` keyword argument to the tag name (`"include"`, `"render"` and `"import"`, respectively), so you know what is tying to load a template. We could use this convention to mimic Shopify's "snippets" convention, where `{% include %}` and `{% render %}` implicitly look in a `snippets` subfolder.
|
|
148
|
+
|
|
149
|
+
## Caching loaders
|
|
150
|
+
|
|
151
|
+
The recommended way to implement template caching is to override `Luoma::TemplateLoader#load`. `load` delegates to `get_source` and calls back to `Environment#parse`, providing a convenient hook into the template loading process.
|
|
152
|
+
|
|
153
|
+
Luoma includes `Luoma::CachingLoaderMixin`, a mixin module that adds an LRU cache to template loaders. We could implement a caching version of [`HashLoader`](#hashloader) as follows.
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
require "luoma"
|
|
157
|
+
|
|
158
|
+
class CachingHashLoader < Luoma::HashLoader
|
|
159
|
+
include CachingLoaderMixin
|
|
160
|
+
|
|
161
|
+
def initialize(
|
|
162
|
+
templates,
|
|
163
|
+
namespace_key: "",
|
|
164
|
+
capacity: 300,
|
|
165
|
+
thread_safe: false
|
|
166
|
+
)
|
|
167
|
+
super(templates)
|
|
168
|
+
|
|
169
|
+
initialize_cache(
|
|
170
|
+
auto_reload: false,
|
|
171
|
+
namespace_key: namespace_key,
|
|
172
|
+
capacity: capacity,
|
|
173
|
+
thread_safe: thread_safe
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
data/docs_/cycle.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Old:
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
{%- liquid
|
|
5
|
+
assign rendered_fonts = ''
|
|
6
|
+
|
|
7
|
+
assign primary_font = settings.type_body_font
|
|
8
|
+
assign primary_font_bold = primary_font | font_modify: 'weight', 'bold'
|
|
9
|
+
assign primary_font_italic = primary_font | font_modify: 'style', 'italic'
|
|
10
|
+
assign primary_font_bold_italic = primary_font_bold | font_modify: 'style', 'italic'
|
|
11
|
+
|
|
12
|
+
assign primary_font_face_id = '[primary_font_family]-[primary_font_weight]-[primary_font_style]|' | replace: '[primary_font_family]', primary_font.family | replace: '[primary_font_weight]', primary_font.weight | replace: '[primary_font_style]', primary_font.style
|
|
13
|
+
assign primary_font_bold_face_id = '[primary_font_bold_family]-[primary_font_bold_weight]-[primary_font_bold_style]|' | replace: '[primary_font_bold_family]', primary_font_bold.family | replace: '[primary_font_bold_weight]', primary_font_bold.weight | replace: '[primary_font_bold_style]', primary_font_bold.style
|
|
14
|
+
assign primary_font_italic_face_id = '[primary_font_italic_family]-[primary_font_italic_weight]-[primary_font_italic_style]|' | replace: '[primary_font_italic_family]', primary_font_italic.family | replace: '[primary_font_italic_weight]', primary_font_italic.weight | replace: '[primary_font_italic_style]', primary_font_italic.style
|
|
15
|
+
assign primary_font_bold_italic_face_id = '[primary_font_bold_italic_family]-[primary_font_bold_italic_weight]-[primary_font_bold_italic_style]|' | replace: '[primary_font_bold_italic_family]', primary_font_bold_italic.family | replace: '[primary_font_bold_italic_weight]', primary_font_bold_italic.weight | replace: '[primary_font_bold_italic_style]', primary_font_bold_italic.style
|
|
16
|
+
|
|
17
|
+
assign secondary_font = settings.type_subheading_font
|
|
18
|
+
assign secondary_font_bold = secondary_font | font_modify: 'weight', 'bold'
|
|
19
|
+
assign secondary_font_italic = secondary_font | font_modify: 'style', 'italic'
|
|
20
|
+
assign secondary_font_bold_italic = secondary_font_bold | font_modify: 'style', 'italic'
|
|
21
|
+
|
|
22
|
+
assign secondary_font_face_id = '[secondary_font_family]-[secondary_font_weight]-[secondary_font_style]|' | replace: '[secondary_font_family]', secondary_font.family | replace: '[secondary_font_weight]', secondary_font.weight | replace: '[secondary_font_style]', secondary_font.style
|
|
23
|
+
assign secondary_font_bold_face_id = '[secondary_font_bold_family]-[secondary_font_bold_weight]-[secondary_font_bold_style]|' | replace: '[secondary_font_bold_family]', secondary_font_bold.family | replace: '[secondary_font_bold_weight]', secondary_font_bold.weight | replace: '[secondary_font_bold_style]', secondary_font_bold.style
|
|
24
|
+
assign secondary_font_italic_face_id = '[secondary_font_italic_family]-[secondary_font_italic_weight]-[secondary_font_italic_style]|' | replace: '[secondary_font_italic_family]', secondary_font_italic.family | replace: '[secondary_font_italic_weight]', secondary_font_italic.weight | replace: '[secondary_font_italic_style]', secondary_font_italic.style
|
|
25
|
+
assign secondary_font_bold_italic_face_id = '[secondary_font_bold_italic_family]-[secondary_font_bold_italic_weight]-[secondary_font_bold_italic_style]|' | replace: '[secondary_font_bold_italic_family]', secondary_font_bold_italic.family | replace: '[secondary_font_bold_italic_weight]', secondary_font_bold_italic.weight | replace: '[secondary_font_bold_italic_style]', secondary_font_bold_italic.style
|
|
26
|
+
|
|
27
|
+
assign tertiary_font = settings.type_heading_font
|
|
28
|
+
assign tertiary_font_bold = tertiary_font | font_modify: 'weight', 'bold'
|
|
29
|
+
assign tertiary_font_italic = tertiary_font | font_modify: 'style', 'italic'
|
|
30
|
+
assign tertiary_font_bold_italic = tertiary_font_bold | font_modify: 'style', 'italic'
|
|
31
|
+
|
|
32
|
+
assign tertiary_font_face_id = '[tertiary_font_family]-[tertiary_font_weight]-[tertiary_font_style]|' | replace: '[tertiary_font_family]', tertiary_font.family | replace: '[tertiary_font_weight]', tertiary_font.weight | replace: '[tertiary_font_style]', tertiary_font.style
|
|
33
|
+
assign tertiary_font_bold_face_id = '[tertiary_font_bold_family]-[tertiary_font_bold_weight]-[tertiary_font_bold_style]|' | replace: '[tertiary_font_bold_family]', tertiary_font_bold.family | replace: '[tertiary_font_bold_weight]', tertiary_font_bold.weight | replace: '[tertiary_font_bold_style]', tertiary_font_bold.style
|
|
34
|
+
assign tertiary_font_italic_face_id = '[tertiary_font_italic_family]-[tertiary_font_italic_weight]-[tertiary_font_italic_style]|' | replace: '[tertiary_font_italic_family]', tertiary_font_italic.family | replace: '[tertiary_font_italic_weight]', tertiary_font_italic.weight | replace: '[tertiary_font_italic_style]', tertiary_font_italic.style
|
|
35
|
+
assign tertiary_font_bold_italic_face_id = '[tertiary_font_bold_italic_family]-[tertiary_font_bold_italic_weight]-[tertiary_font_bold_italic_style]|' | replace: '[tertiary_font_bold_italic_family]', tertiary_font_bold_italic.family | replace: '[tertiary_font_bold_italic_weight]', tertiary_font_bold_italic.weight | replace: '[tertiary_font_bold_italic_style]', tertiary_font_bold_italic.style
|
|
36
|
+
|
|
37
|
+
assign accent_font = settings.type_accent_font
|
|
38
|
+
assign accent_font_bold = accent_font | font_modify: 'weight', 'bold'
|
|
39
|
+
assign accent_font_italic = accent_font | font_modify: 'style', 'italic'
|
|
40
|
+
assign accent_font_bold_italic = accent_font_bold | font_modify: 'style', 'italic'
|
|
41
|
+
|
|
42
|
+
assign accent_font_face_id = '[accent_font_family]-[accent_font_weight]-[accent_font_style]|' | replace: '[accent_font_family]', accent_font.family | replace: '[accent_font_weight]', accent_font.weight | replace: '[accent_font_style]', accent_font.style
|
|
43
|
+
assign accent_font_bold_face_id = '[accent_font_bold_family]-[accent_font_bold_weight]-[accent_font_bold_style]|' | replace: '[accent_font_bold_family]', accent_font_bold.family | replace: '[accent_font_bold_weight]', accent_font_bold.weight | replace: '[accent_font_bold_style]', accent_font_bold.style
|
|
44
|
+
assign accent_font_italic_face_id = '[accent_font_italic_family]-[accent_font_italic_weight]-[accent_font_italic_style]|' | replace: '[accent_font_italic_family]', accent_font_italic.family | replace: '[accent_font_italic_weight]', accent_font_italic.weight | replace: '[accent_font_italic_style]', accent_font_italic.style
|
|
45
|
+
assign accent_font_bold_italic_face_id = '[accent_font_bold_italic_family]-[accent_font_bold_italic_weight]-[accent_font_bold_italic_style]|' | replace: '[accent_font_bold_italic_family]', accent_font_bold_italic.family | replace: '[accent_font_bold_italic_weight]', accent_font_bold_italic.weight | replace: '[accent_font_bold_italic_style]', accent_font_bold_italic.style
|
|
46
|
+
|
|
47
|
+
unless rendered_fonts contains primary_font_face_id
|
|
48
|
+
echo primary_font | font_face: font_display: 'swap'
|
|
49
|
+
assign rendered_fonts = rendered_fonts | append: primary_font_face_id
|
|
50
|
+
endunless
|
|
51
|
+
|
|
52
|
+
unless rendered_fonts contains primary_font_bold_face_id
|
|
53
|
+
echo primary_font_bold | font_face: font_display: 'swap'
|
|
54
|
+
assign rendered_fonts = rendered_fonts | append: primary_font_bold_face_id
|
|
55
|
+
endunless
|
|
56
|
+
|
|
57
|
+
unless rendered_fonts contains primary_font_italic_face_id
|
|
58
|
+
echo primary_font_italic | font_face: font_display: 'swap'
|
|
59
|
+
assign rendered_fonts = rendered_fonts | append: primary_font_italic_face_id
|
|
60
|
+
endunless
|
|
61
|
+
|
|
62
|
+
unless rendered_fonts contains primary_font_bold_italic_face_id
|
|
63
|
+
echo primary_font_bold_italic | font_face: font_display: 'swap'
|
|
64
|
+
assign rendered_fonts = rendered_fonts | append: primary_font_bold_italic_face_id
|
|
65
|
+
endunless
|
|
66
|
+
|
|
67
|
+
unless rendered_fonts contains secondary_font_face_id
|
|
68
|
+
echo secondary_font | font_face: font_display: 'swap'
|
|
69
|
+
assign rendered_fonts = rendered_fonts | append: secondary_font_face_id
|
|
70
|
+
endunless
|
|
71
|
+
|
|
72
|
+
unless rendered_fonts contains secondary_font_bold_face_id
|
|
73
|
+
echo secondary_font_bold | font_face: font_display: 'swap'
|
|
74
|
+
assign rendered_fonts = rendered_fonts | append: secondary_font_bold_face_id
|
|
75
|
+
endunless
|
|
76
|
+
|
|
77
|
+
unless rendered_fonts contains secondary_font_italic_face_id
|
|
78
|
+
echo secondary_font_italic | font_face: font_display: 'swap'
|
|
79
|
+
assign rendered_fonts = rendered_fonts | append: secondary_font_italic_face_id
|
|
80
|
+
endunless
|
|
81
|
+
|
|
82
|
+
unless rendered_fonts contains secondary_font_bold_italic_face_id
|
|
83
|
+
echo secondary_font_bold_italic | font_face: font_display: 'swap'
|
|
84
|
+
assign rendered_fonts = rendered_fonts | append: secondary_font_bold_italic_face_id
|
|
85
|
+
endunless
|
|
86
|
+
|
|
87
|
+
unless rendered_fonts contains tertiary_font_face_id
|
|
88
|
+
echo tertiary_font | font_face: font_display: 'swap'
|
|
89
|
+
assign rendered_fonts = rendered_fonts | append: tertiary_font_face_id
|
|
90
|
+
endunless
|
|
91
|
+
|
|
92
|
+
unless rendered_fonts contains tertiary_font_bold_face_id
|
|
93
|
+
echo tertiary_font_bold | font_face: font_display: 'swap'
|
|
94
|
+
assign rendered_fonts = rendered_fonts | append: tertiary_font_bold_face_id
|
|
95
|
+
endunless
|
|
96
|
+
|
|
97
|
+
unless rendered_fonts contains tertiary_font_italic_face_id
|
|
98
|
+
echo tertiary_font_italic | font_face: font_display: 'swap'
|
|
99
|
+
assign rendered_fonts = rendered_fonts | append: tertiary_font_italic_face_id
|
|
100
|
+
endunless
|
|
101
|
+
|
|
102
|
+
unless rendered_fonts contains tertiary_font_bold_italic_face_id
|
|
103
|
+
echo tertiary_font_bold_italic | font_face: font_display: 'swap'
|
|
104
|
+
assign rendered_fonts = rendered_fonts | append: tertiary_font_bold_italic_face_id
|
|
105
|
+
endunless
|
|
106
|
+
|
|
107
|
+
unless rendered_fonts contains accent_font_face_id
|
|
108
|
+
echo accent_font | font_face: font_display: 'swap'
|
|
109
|
+
assign rendered_fonts = rendered_fonts | append: accent_font_face_id
|
|
110
|
+
endunless
|
|
111
|
+
|
|
112
|
+
unless rendered_fonts contains accent_font_bold_face_id
|
|
113
|
+
echo accent_font_bold | font_face: font_display: 'swap'
|
|
114
|
+
assign rendered_fonts = rendered_fonts | append: accent_font_bold_face_id
|
|
115
|
+
endunless
|
|
116
|
+
|
|
117
|
+
unless rendered_fonts contains accent_font_italic_face_id
|
|
118
|
+
echo accent_font_italic | font_face: font_display: 'swap'
|
|
119
|
+
assign rendered_fonts = rendered_fonts | append: accent_font_italic_face_id
|
|
120
|
+
endunless
|
|
121
|
+
|
|
122
|
+
unless rendered_fonts contains accent_font_bold_italic_face_id
|
|
123
|
+
echo accent_font_bold_italic | font_face: font_display: 'swap'
|
|
124
|
+
assign rendered_fonts = rendered_fonts | append: accent_font_bold_italic_face_id
|
|
125
|
+
endunless
|
|
126
|
+
%}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
New:
|
|
130
|
+
|
|
131
|
+
```title="font_utils"
|
|
132
|
+
{% assign
|
|
133
|
+
bold = f -> (f | font_modify: 'weight', 'bold'),
|
|
134
|
+
italic = f -> (f | font_modify: 'style', 'italic'),
|
|
135
|
+
bold_italic = f -> (f | font_modify: 'weight', 'bold' | font_modify: 'style', 'italic')
|
|
136
|
+
%}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
{%- import "font_utils" -%}
|
|
141
|
+
|
|
142
|
+
{% with
|
|
143
|
+
font_types = [
|
|
144
|
+
settings.type_body_font,
|
|
145
|
+
settings.type_subheading_font,
|
|
146
|
+
settings.type_heading_font,
|
|
147
|
+
settings.type_accent_font
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
font_faces = font_types
|
|
151
|
+
| flat_map : f -> [f, (f | bold), (f | italic), (f | bold_italic)]
|
|
152
|
+
| uniq : f -> '${f.family}-${f.weight}-${f.style}'
|
|
153
|
+
| map : f -> (f | font_face: font_display: 'swap')
|
|
154
|
+
%}
|
|
155
|
+
{{- font_faces | join: "\n\n" -}}
|
|
156
|
+
{% endwith %}
|
|
157
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
From Horizon `header.liquid`
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
{% liquid
|
|
5
|
+
assign bottom_row_blocks = ''
|
|
6
|
+
|
|
7
|
+
if section.settings.menu_row == 'bottom'
|
|
8
|
+
assign bottom_row_blocks = bottom_row_blocks | append: 'menu,'
|
|
9
|
+
endif
|
|
10
|
+
|
|
11
|
+
if section.settings.search_style != 'none'
|
|
12
|
+
if section.settings.search_row == 'bottom'
|
|
13
|
+
assign bottom_row_blocks = bottom_row_blocks | append: 'search,'
|
|
14
|
+
endif
|
|
15
|
+
endif
|
|
16
|
+
|
|
17
|
+
if section.settings.show_country or section.settings.show_language
|
|
18
|
+
if section.settings.localization_row == 'bottom'
|
|
19
|
+
assign bottom_row_blocks = bottom_row_blocks | append: 'localization,'
|
|
20
|
+
endif
|
|
21
|
+
endif
|
|
22
|
+
|
|
23
|
+
assign bottom_row_blocks = bottom_row_blocks | split: ',' | compact
|
|
24
|
+
%}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
{% with settings = section.settings %}
|
|
29
|
+
{% assign
|
|
30
|
+
bottom_row_blocks = [
|
|
31
|
+
'menu' if settings.menu_row == 'bottom',
|
|
32
|
+
'search' if settings.search_style != 'none' and settings.search_row == 'bottom',
|
|
33
|
+
'localization' if (settings.show_country or settings.show_language) and settings.localization_row == 'bottom',
|
|
34
|
+
] | compact
|
|
35
|
+
%}
|
|
36
|
+
{% endwith %}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
{% with
|
|
41
|
+
s = section.settings,
|
|
42
|
+
|
|
43
|
+
blocks = [
|
|
44
|
+
'menu' if s.menu_row == 'bottom',
|
|
45
|
+
'search' if s.search_style != 'none' and s.search_row == 'bottom',
|
|
46
|
+
'localization' if (s.show_country or s.show_language) and s.localization_row == 'bottom',
|
|
47
|
+
]
|
|
48
|
+
%}
|
|
49
|
+
{% assign bottom_row_blocks = blocks | compact %}
|
|
50
|
+
{% endwith %}
|
|
51
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Old:
|
|
2
|
+
|
|
3
|
+
Start at zero, post output increment
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
{% increment foo %}
|
|
7
|
+
{% increment foo %}
|
|
8
|
+
{{ foo }}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```title="output"
|
|
12
|
+
0
|
|
13
|
+
1
|
|
14
|
+
2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Start at zero, pre output decrement
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
{% decrement foo %}
|
|
21
|
+
{% decrement foo %}
|
|
22
|
+
{{ foo }}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```title="output"
|
|
26
|
+
-1
|
|
27
|
+
-2
|
|
28
|
+
-2
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
New:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
{% assign foo = 0 -%}
|
|
35
|
+
{{ foo }}
|
|
36
|
+
{% assign foo = foo + 1 -%}
|
|
37
|
+
{{ foo }}
|
|
38
|
+
{% assign foo = foo + 1 -%}
|
|
39
|
+
{{ foo }}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
{% assign foo = 0 %}
|
|
44
|
+
{% assign foo = foo - 1 -%}
|
|
45
|
+
{{ foo }}
|
|
46
|
+
{% assign foo = foo - 1 -%}
|
|
47
|
+
{{ foo }}
|
|
48
|
+
{{ foo }}
|
|
49
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
From Horizon `_header-logo.liquid`:
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
{% liquid
|
|
5
|
+
assign logo_width = settings.logo_height | times: settings.logo.aspect_ratio | ceil
|
|
6
|
+
assign logo_width_mobile = settings.logo_height_mobile | times: settings.logo.aspect_ratio | ceil
|
|
7
|
+
assign inverse_logo_width = settings.logo_height | times: inverse_logo.aspect_ratio | ceil
|
|
8
|
+
assign inverse_logo_width_mobile = settings.logo_height_mobile | times: inverse_logo.aspect_ratio | ceil
|
|
9
|
+
assign logo_style = '--header-logo-image-width: ' | append: logo_width | append: 'px;' | append: '--header-logo-image-width-mobile: ' | append: logo_width_mobile | append: 'px; --header-logo-image-height: ' | append: settings.logo_height | append: 'px; --header-logo-image-height-mobile: ' | append: settings.logo_height_mobile | append: 'px;'
|
|
10
|
+
assign inverse_logo_style = '--header-logo-image-width: ' | append: inverse_logo_width | append: 'px;' | append: '--header-logo-image-width-mobile: ' | append: inverse_logo_width_mobile | append: 'px; --header-logo-image-height: ' | append: settings.logo_height | append: 'px; --header-logo-image-height-mobile: ' | append: settings.logo_height_mobile | append: 'px;'
|
|
11
|
+
%}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
{% with
|
|
16
|
+
logo_width = settings.logo_height * settings.logo.aspect_ratio | ceil,
|
|
17
|
+
logo_width_mobile = settings.logo_height_mobile * settings.logo.aspect_ratio | ceil,
|
|
18
|
+
inverse_logo_width = settings.logo_height * inverse_logo.aspect_ratio | ceil,
|
|
19
|
+
inverse_logo_width_mobile = settings.logo_height_mobile * inverse_logo.aspect_ratio | ceil,
|
|
20
|
+
|
|
21
|
+
logo_styles = [
|
|
22
|
+
'--header-logo-image-width: ${logo_width}px;',
|
|
23
|
+
'--header-logo-image-width-mobile: ${logo_width_mobile}px;',
|
|
24
|
+
'--header-logo-image-height: ${settings.logo_height}px;',
|
|
25
|
+
'--header-logo-image-height-mobile: ${settings.logo_height_mobile}px;',
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
inverse_logo_styles = [
|
|
29
|
+
'--header-logo-image-width: ${inverse_logo_width}px;',
|
|
30
|
+
'--header-logo-image-width-mobile: ${inverse_logo_width_mobile}px;',
|
|
31
|
+
'--header-logo-image-height: ${settings.logo_height}px;',
|
|
32
|
+
'--header-logo-image-height-mobile: ${settings.logo_height_mobile}px;',
|
|
33
|
+
]
|
|
34
|
+
%}
|
|
35
|
+
{% assign
|
|
36
|
+
logo_style = logo_styles | join,
|
|
37
|
+
inverse_logo_style = inverse_logo_styles | join,
|
|
38
|
+
%}
|
|
39
|
+
{% endwith %}
|
|
40
|
+
```
|
data/docs_/migration.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Luoma idioms
|
|
2
|
+
|
|
3
|
+
For anyone already familiar with [liquid](https://github.com/Shopify/liquid), here we show some legacy Liquid idioms and equivalent Luoma idioms side-by-side.
|
|
4
|
+
|
|
5
|
+
## Comments
|
|
6
|
+
|
|
7
|
+
Luoma does not include `{% comment %}...{% endcomment%}` or `{% # ... }` tags.
|
|
8
|
+
|
|
9
|
+
```liquid
|
|
10
|
+
{% comment %} {% endcomment %}
|
|
11
|
+
```
|
data/docs_/notes.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Notes
|
|
2
|
+
|
|
3
|
+
- There's no `{% comment %}` or `{% # %}` tag. Use `{# some comment #}` instead.
|
|
4
|
+
- There's no `{% doc %}` tag. Use `{# some doc comment #}` instead.
|
|
5
|
+
- There's no `{% liquid %}` or equivalent tag. Use compound and ternary expressions, multi assigns with the `{% assign %}` tag, and block-scoped variables with the `{% with %}` tag.
|
|
6
|
+
- There's no `{% unless %}` tag. Use `{% if not (...) %}` or `{% if a != b %}`.
|
|
7
|
+
- `{% for %}` does not accept arguments. Use the `slice` and `reverse` filters instead.
|
|
8
|
+
- The `slice` filter accepts `start`, `stop` and `step` arguments instead of `limit` and `offset`. `start`, `stop` and `step` can be positional or keyword and positive or negative.
|
|
9
|
+
- There's no `forloop` drop. Use optional index and array binding syntax: `{% for a, index, array in ... %}`.
|
|
10
|
+
- Strings are iterated and sliced like an array of characters.
|
|
11
|
+
- `{% render %}` and `{% include %}` don't accept binding or looping arguments. Use a `{% for %}` loop instead.
|
|
12
|
+
- There's no `empty` or `blank` objects. Use `thing.empty?` and `thing.blank?` instead.
|
|
13
|
+
- Filters that expect an array input do not implicitly flatten nested arrays. Use the `flatten` filter if needed.
|
|
14
|
+
|
|
15
|
+
- First-class blocks with `{% define %} ... {% enddefine%}`
|
|
16
|
+
- First-class expressions with `{% assign x = <lambda expr> %}`
|
|
17
|
+
- Type predicates
|
|
18
|
+
- Case tag accepts type predicates
|
|
19
|
+
- `{% import %}` is a cross between `{% include %}` and `{% render %}`. It requires a string argument and renders a template just for its side effects, discarding any output.
|
data/lib/luoma/cache.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Luoma
|
|
6
|
+
# A least recently used cache relying on Ruby hash insertion order.
|
|
7
|
+
class LRUCache
|
|
8
|
+
attr_reader :max_size
|
|
9
|
+
|
|
10
|
+
def initialize(max_size = 128)
|
|
11
|
+
@data = {}
|
|
12
|
+
@max_size = max_size
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Return the cached value or nil if _key_ does not exist.
|
|
16
|
+
def [](key)
|
|
17
|
+
val = @data[key]
|
|
18
|
+
return nil if val.nil?
|
|
19
|
+
|
|
20
|
+
@data.delete(key)
|
|
21
|
+
@data[key] = val
|
|
22
|
+
val
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def []=(key, value)
|
|
26
|
+
if @data.key?(key)
|
|
27
|
+
@data.delete(key)
|
|
28
|
+
elsif @data.length >= @max_size
|
|
29
|
+
@data.delete((@data.first || raise)[0])
|
|
30
|
+
end
|
|
31
|
+
@data[key] = value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def length
|
|
35
|
+
@data.length
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def keys
|
|
39
|
+
@data.keys
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A thread safe least recently used cache.
|
|
44
|
+
class ThreadSafeLRUCache < LRUCache
|
|
45
|
+
include MonitorMixin
|
|
46
|
+
|
|
47
|
+
alias unsafe_get []
|
|
48
|
+
alias unsafe_set []=
|
|
49
|
+
alias unsafe_length length
|
|
50
|
+
alias unsafe_keys keys
|
|
51
|
+
|
|
52
|
+
def initialize(max_size = 128)
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def [](key)
|
|
57
|
+
synchronize do
|
|
58
|
+
unsafe_get(key)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def []=(key, value)
|
|
63
|
+
synchronize do
|
|
64
|
+
unsafe_set(key, value)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def length
|
|
69
|
+
synchronize do
|
|
70
|
+
unsafe_length
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def keys
|
|
75
|
+
synchronize do
|
|
76
|
+
unsafe_keys
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Luoma
|
|
4
|
+
# Combine multiple hashes for sequential lookup.
|
|
5
|
+
class ChainHash
|
|
6
|
+
#: (*_Namespace) -> void
|
|
7
|
+
def initialize(*hashes)
|
|
8
|
+
@hashes = hashes.to_a
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
#: (String) -> untyped
|
|
12
|
+
def [](key)
|
|
13
|
+
index = @hashes.length - 1
|
|
14
|
+
while index >= 0
|
|
15
|
+
h = @hashes[index]
|
|
16
|
+
index -= 1
|
|
17
|
+
v = h.fetch(key, :nothing)
|
|
18
|
+
return v unless v == :nothing
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: (String) -> bool
|
|
23
|
+
def key?(key)
|
|
24
|
+
index = @hashes.length - 1
|
|
25
|
+
while index >= 0
|
|
26
|
+
h = @hashes[index]
|
|
27
|
+
index -= 1
|
|
28
|
+
v = h.fetch(key, :nothing)
|
|
29
|
+
return true unless v == :nothing
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#: (String, untyped) -> untyped
|
|
36
|
+
def fetch(key, default = :nothing)
|
|
37
|
+
index = @hashes.length - 1
|
|
38
|
+
while index >= 0
|
|
39
|
+
h = @hashes[index]
|
|
40
|
+
index -= 1
|
|
41
|
+
v = h.fetch(key, :nothing)
|
|
42
|
+
return v unless v == :nothing
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
default
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def size = @hashes.length
|
|
49
|
+
def push(hash) = @hashes << hash
|
|
50
|
+
alias << push
|
|
51
|
+
def pop = @hashes.pop
|
|
52
|
+
def nil? = false
|
|
53
|
+
end
|
|
54
|
+
end
|