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.
Files changed (152) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +2 -0
  3. data/.rdoc_options +16 -0
  4. data/CHANGELOG.md +5 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +68 -0
  7. data/Rakefile +69 -0
  8. data/Steepfile +41 -0
  9. data/certs/jgrp.pem +27 -0
  10. data/docs/configuration.md +180 -0
  11. data/docs/custom_filters.md +3 -0
  12. data/docs/custom_tags.md +3 -0
  13. data/docs/expressions.md +3 -0
  14. data/docs/extension_types.md +41 -0
  15. data/docs/filter_reference.md +1702 -0
  16. data/docs/index.md +73 -0
  17. data/docs/luoma_for_template_authors.md +3 -0
  18. data/docs/markdown.md +111 -0
  19. data/docs/predicate_reference.md +3 -0
  20. data/docs/static_analysis.md +3 -0
  21. data/docs/tag_reference.md +352 -0
  22. data/docs/template_loaders.md +177 -0
  23. data/docs/undefined_variables.md +3 -0
  24. data/docs-requirements.txt +11 -0
  25. data/docs_/cycle.md +17 -0
  26. data/docs_/font_rendering_example.md +157 -0
  27. data/docs_/header_block_example.md +51 -0
  28. data/docs_/increment_and_decrement.md +49 -0
  29. data/docs_/logo_style_example.md +40 -0
  30. data/docs_/migration.md +11 -0
  31. data/docs_/notes.md +19 -0
  32. data/lib/luoma/cache.rb +80 -0
  33. data/lib/luoma/chain_hash.rb +54 -0
  34. data/lib/luoma/context.rb +227 -0
  35. data/lib/luoma/drop.rb +84 -0
  36. data/lib/luoma/drops/block.rb +33 -0
  37. data/lib/luoma/drops/expression.rb +29 -0
  38. data/lib/luoma/drops/html_safe.rb +36 -0
  39. data/lib/luoma/drops/range.rb +69 -0
  40. data/lib/luoma/drops/undefined.rb +119 -0
  41. data/lib/luoma/environment.rb +479 -0
  42. data/lib/luoma/errors.rb +79 -0
  43. data/lib/luoma/escape.rb +35 -0
  44. data/lib/luoma/expression.rb +1271 -0
  45. data/lib/luoma/filter.rb +97 -0
  46. data/lib/luoma/filters/array.rb +372 -0
  47. data/lib/luoma/filters/date.rb +19 -0
  48. data/lib/luoma/filters/default.rb +16 -0
  49. data/lib/luoma/filters/json.rb +14 -0
  50. data/lib/luoma/filters/math.rb +84 -0
  51. data/lib/luoma/filters/size.rb +13 -0
  52. data/lib/luoma/filters/slice.rb +52 -0
  53. data/lib/luoma/filters/sort.rb +113 -0
  54. data/lib/luoma/filters/string.rb +186 -0
  55. data/lib/luoma/fnv.rb +15 -0
  56. data/lib/luoma/lexer.rb +106 -0
  57. data/lib/luoma/lexer_legacy.rb +396 -0
  58. data/lib/luoma/lexer_unified.rb +279 -0
  59. data/lib/luoma/loader.rb +50 -0
  60. data/lib/luoma/loaders/choice_loader.rb +20 -0
  61. data/lib/luoma/loaders/file_system_loader.rb +75 -0
  62. data/lib/luoma/loaders/mixins.rb +52 -0
  63. data/lib/luoma/markup.rb +109 -0
  64. data/lib/luoma/parser.rb +252 -0
  65. data/lib/luoma/parser_unified.rb +979 -0
  66. data/lib/luoma/predicates/blank.rb +19 -0
  67. data/lib/luoma/predicates/defined.rb +10 -0
  68. data/lib/luoma/predicates/empty.rb +15 -0
  69. data/lib/luoma/predicates/type.rb +35 -0
  70. data/lib/luoma/static_analysis.rb +427 -0
  71. data/lib/luoma/tags/assign.rb +63 -0
  72. data/lib/luoma/tags/break.rb +23 -0
  73. data/lib/luoma/tags/capture.rb +43 -0
  74. data/lib/luoma/tags/case.rb +137 -0
  75. data/lib/luoma/tags/comment.rb +17 -0
  76. data/lib/luoma/tags/continue.rb +23 -0
  77. data/lib/luoma/tags/define.rb +42 -0
  78. data/lib/luoma/tags/else.rb +30 -0
  79. data/lib/luoma/tags/for.rb +108 -0
  80. data/lib/luoma/tags/if.rb +109 -0
  81. data/lib/luoma/tags/import.rb +91 -0
  82. data/lib/luoma/tags/include.rb +77 -0
  83. data/lib/luoma/tags/output.rb +20 -0
  84. data/lib/luoma/tags/raw.rb +26 -0
  85. data/lib/luoma/tags/render.rb +92 -0
  86. data/lib/luoma/tags/with.rb +55 -0
  87. data/lib/luoma/template.rb +178 -0
  88. data/lib/luoma/token.rb +84 -0
  89. data/lib/luoma/version.rb +5 -0
  90. data/lib/luoma.rb +75 -0
  91. data/sig/luoma/cache.rbs +44 -0
  92. data/sig/luoma/chain_hash.rbs +26 -0
  93. data/sig/luoma/context.rbs +115 -0
  94. data/sig/luoma/drop.rbs +44 -0
  95. data/sig/luoma/drops/block.rbs +10 -0
  96. data/sig/luoma/drops/expression.rbs +10 -0
  97. data/sig/luoma/drops/html_safe.rbs +14 -0
  98. data/sig/luoma/drops/range.rbs +15 -0
  99. data/sig/luoma/drops/undefined.rbs +27 -0
  100. data/sig/luoma/environment.rbs +212 -0
  101. data/sig/luoma/errors.rbs +67 -0
  102. data/sig/luoma/escape.rbs +15 -0
  103. data/sig/luoma/expression.rbs +514 -0
  104. data/sig/luoma/filter.rbs +66 -0
  105. data/sig/luoma/filters/array.rbs +74 -0
  106. data/sig/luoma/filters/date.rbs +9 -0
  107. data/sig/luoma/filters/default.rbs +8 -0
  108. data/sig/luoma/filters/json.rbs +7 -0
  109. data/sig/luoma/filters/math.rbs +37 -0
  110. data/sig/luoma/filters/size.rbs +6 -0
  111. data/sig/luoma/filters/slice.rbs +10 -0
  112. data/sig/luoma/filters/sort.rbs +17 -0
  113. data/sig/luoma/filters/string.rbs +103 -0
  114. data/sig/luoma/fnv.rbs +3 -0
  115. data/sig/luoma/lexer.rbs +42 -0
  116. data/sig/luoma/lexer_legacy.rbs +81 -0
  117. data/sig/luoma/lexer_unified.rbs +49 -0
  118. data/sig/luoma/loader.rbs +40 -0
  119. data/sig/luoma/loaders/choice_loader.rbs +9 -0
  120. data/sig/luoma/loaders/file_system_loader.rbs +19 -0
  121. data/sig/luoma/loaders/mixins.rbs +16 -0
  122. data/sig/luoma/markup.rbs +68 -0
  123. data/sig/luoma/parser.rbs +105 -0
  124. data/sig/luoma/parser_unified.rbs +150 -0
  125. data/sig/luoma/predicates/blank.rbs +6 -0
  126. data/sig/luoma/predicates/defined.rbs +6 -0
  127. data/sig/luoma/predicates/empty.rbs +6 -0
  128. data/sig/luoma/predicates/type.rbs +21 -0
  129. data/sig/luoma/static_analysis.rbs +141 -0
  130. data/sig/luoma/tags/assign.rbs +12 -0
  131. data/sig/luoma/tags/break.rbs +11 -0
  132. data/sig/luoma/tags/capture.rbs +14 -0
  133. data/sig/luoma/tags/case.rbs +36 -0
  134. data/sig/luoma/tags/comment.rbs +11 -0
  135. data/sig/luoma/tags/continue.rbs +11 -0
  136. data/sig/luoma/tags/define.rbs +16 -0
  137. data/sig/luoma/tags/else.rbs +17 -0
  138. data/sig/luoma/tags/for.rbs +26 -0
  139. data/sig/luoma/tags/if.rbs +45 -0
  140. data/sig/luoma/tags/import.rbs +13 -0
  141. data/sig/luoma/tags/include.rbs +13 -0
  142. data/sig/luoma/tags/output.rbs +13 -0
  143. data/sig/luoma/tags/raw.rbs +11 -0
  144. data/sig/luoma/tags/render.rbs +13 -0
  145. data/sig/luoma/tags/with.rbs +15 -0
  146. data/sig/luoma/template.rbs +117 -0
  147. data/sig/luoma/token.rbs +81 -0
  148. data/sig/luoma.rbs +11 -0
  149. data/zensical.toml +363 -0
  150. data.tar.gz.sig +0 -0
  151. metadata +233 -0
  152. 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
+ ```
@@ -0,0 +1,3 @@
1
+ # Undefined variables
2
+
3
+ TODO:
@@ -0,0 +1,11 @@
1
+ click==8.4.2
2
+ deepmerge==2.1.0
3
+ Jinja2==3.1.6
4
+ Markdown==3.10.2
5
+ MarkupSafe==3.0.3
6
+ Pygments==2.20.0
7
+ pygments-liquid2==0.2.0
8
+ pymdown-extensions==11.0.1
9
+ PyYAML==6.0.3
10
+ tomli==2.4.1
11
+ zensical==0.0.51
data/docs_/cycle.md ADDED
@@ -0,0 +1,17 @@
1
+
2
+ Old:
3
+
4
+ ```
5
+ {% cycle "some", "thing" %}
6
+ {% cycle "some", "thing" %}
7
+ {% cycle "some", "thing" %}
8
+ ```
9
+
10
+ ```title="output"
11
+ some
12
+ thing
13
+ some
14
+ ```
15
+
16
+ New:
17
+
@@ -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
+ ```
@@ -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.
@@ -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