beachio-hammer 0.1.0.pre3 → 0.1.0.pre5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/PUBLISH.md +12 -2
- data/lib/hammer/content/helpers.rb +12 -1
- data/lib/hammer/content/page_generator.rb +88 -1
- data/lib/hammer/tag_parser.rb +90 -6
- data/lib/hammer/version.rb +1 -1
- data/spec/blog_listing_spec.rb +22 -0
- data/spec/path_resolution_spec.rb +10 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 761f1f1873cb333406eeab947ee2ae1edde559b69fa38497810c96bdf802f58e
|
|
4
|
+
data.tar.gz: 6721ab8889e4129b9c886fa875b92579d359a9012852d69aaa711fa1e29c08eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23c9ab030b80757ac305dc66d4f0a35c9df394e248fa22ce62e8164d96c9b1573c4771b49ea82770574c113a7c4fd56ccfd26e294bda7f4515ede443dca28bfa
|
|
7
|
+
data.tar.gz: 5b670417365cbd83d7a706f3a1dc03edaadbcde2ca5cff920651446f66c790565d97a798e3a784fed847493cd62e1051cd27a0a4b23d11d67cd46014fb85314b
|
data/Gemfile.lock
CHANGED
data/PUBLISH.md
CHANGED
|
@@ -38,7 +38,17 @@ gem build beachio-hammer.gemspec
|
|
|
38
38
|
gem push beachio-hammer-*.gem
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Prerelease pin for executor team: **`beachio-hammer` `0.1.0.
|
|
41
|
+
Prerelease pin for executor team: **`beachio-hammer` `0.1.0.pre5`**
|
|
42
|
+
|
|
43
|
+
### 0.1.0.pre5 changes (collection loops on static pages)
|
|
44
|
+
|
|
45
|
+
- Parse loop attributes (`sort="date:desc"`) when pre-processing `collections.*` loops
|
|
46
|
+
- Evaluate `$helpers.formatDate(...)` inside `@loop` bodies
|
|
47
|
+
- Fix loop condition checks for `post.category` syntax
|
|
48
|
+
|
|
49
|
+
### 0.1.0.pre4 changes (filename digit corruption)
|
|
50
|
+
|
|
51
|
+
- Fix `@path` URL encoding: stop using `tr("%20", " ")` which mangled filenames containing `2` (e.g. `forge-2.jpg` → `forge-%20.jpg`)
|
|
42
52
|
|
|
43
53
|
### 0.1.0.pre3 changes (nested @path resolution)
|
|
44
54
|
|
|
@@ -58,7 +68,7 @@ Prerelease pin for executor team: **`beachio-hammer` `0.1.0.pre3`**
|
|
|
58
68
|
Pin the published gem version in the executor Dockerfile:
|
|
59
69
|
|
|
60
70
|
```dockerfile
|
|
61
|
-
RUN gem install beachio-hammer -v 0.1.0.
|
|
71
|
+
RUN gem install beachio-hammer -v 0.1.0.pre5
|
|
62
72
|
```
|
|
63
73
|
|
|
64
74
|
Build step in the agent:
|
|
@@ -45,10 +45,21 @@ module Hammer
|
|
|
45
45
|
when "long" then time.strftime("%B %d, %Y")
|
|
46
46
|
when "full" then time.strftime("%A, %B %d, %Y")
|
|
47
47
|
when "iso", "iso8601" then time.utc.iso8601
|
|
48
|
-
else time.strftime(format)
|
|
48
|
+
else time.strftime(swift_date_format_to_strftime(format))
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def swift_date_format_to_strftime(format)
|
|
53
|
+
format
|
|
54
|
+
.gsub("yyyy", "%Y")
|
|
55
|
+
.gsub("MMM", "%b")
|
|
56
|
+
.gsub("MM", "%m")
|
|
57
|
+
.gsub("dd", "%d")
|
|
58
|
+
.gsub("HH", "%H")
|
|
59
|
+
.gsub("mm", "%M")
|
|
60
|
+
.gsub("ss", "%S")
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def relative_path(args)
|
|
53
64
|
from = args[0].to_s
|
|
54
65
|
to = args[1].to_s
|
|
@@ -237,7 +237,11 @@ module Hammer
|
|
|
237
237
|
collection_expr = m[2].strip
|
|
238
238
|
next if collection_expr.start_with?("_loop_")
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
base_expression, attributes = parse_loop_attributes(collection_expr)
|
|
241
|
+
collection_value = evaluator.evaluate(base_expression)
|
|
242
|
+
next if collection_value.nil?
|
|
243
|
+
|
|
244
|
+
collection_value = apply_loop_attributes(collection_value, attributes) unless attributes.empty?
|
|
241
245
|
items = collection_to_json_items(collection_value)
|
|
242
246
|
json_array = JSON.generate(items)
|
|
243
247
|
escaped = json_array.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", "\\n")
|
|
@@ -266,11 +270,94 @@ module Hammer
|
|
|
266
270
|
end
|
|
267
271
|
when Document
|
|
268
272
|
[value.to_h]
|
|
273
|
+
when NilClass
|
|
274
|
+
[]
|
|
269
275
|
else
|
|
270
276
|
[{ "value" => value.to_s }]
|
|
271
277
|
end
|
|
272
278
|
end
|
|
273
279
|
|
|
280
|
+
def parse_loop_attributes(expression)
|
|
281
|
+
attributes = {}
|
|
282
|
+
base_expression = expression.strip
|
|
283
|
+
pattern = /(\w+)\s*=\s*(["'])(.*?)\2/
|
|
284
|
+
|
|
285
|
+
expression.scan(pattern) do |name, quote, value|
|
|
286
|
+
attributes[name] = value
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
if attributes.any?
|
|
290
|
+
base_expression = expression.gsub(/\s*\w+\s*=\s*(["']).*?\1/, "").strip
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
[base_expression, attributes]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def parse_sort_attribute(sort_value)
|
|
297
|
+
sort_value.split(",").filter_map do |field_spec|
|
|
298
|
+
field, direction = field_spec.strip.split(":", 2)
|
|
299
|
+
next if field.nil? || field.empty?
|
|
300
|
+
|
|
301
|
+
order = direction&.downcase
|
|
302
|
+
order = :desc if order == "desc" || order == "descending"
|
|
303
|
+
order = :asc unless order == :desc
|
|
304
|
+
{ field: field.strip, order: order }
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def apply_loop_attributes(value, attributes)
|
|
309
|
+
return value unless attributes["sort"]
|
|
310
|
+
|
|
311
|
+
sorts = parse_sort_attribute(attributes["sort"])
|
|
312
|
+
return value if sorts.empty?
|
|
313
|
+
|
|
314
|
+
documents = case value
|
|
315
|
+
when Collection then value.documents.dup
|
|
316
|
+
when Array then value.dup
|
|
317
|
+
else return value
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
sorts.reverse_each do |sort|
|
|
321
|
+
documents.sort! do |left, right|
|
|
322
|
+
left_value = extract_sort_value(left, sort[:field])
|
|
323
|
+
right_value = extract_sort_value(right, sort[:field])
|
|
324
|
+
comparison = compare_sort_values(left_value, right_value)
|
|
325
|
+
sort[:order] == :desc ? -comparison : comparison
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
case value
|
|
330
|
+
when Collection then Collection.new(value.name, documents)
|
|
331
|
+
else documents
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def extract_sort_value(item, field)
|
|
336
|
+
case item
|
|
337
|
+
when Document
|
|
338
|
+
item.metadata[field] || (field == "date" ? item.date : nil)
|
|
339
|
+
when Hash
|
|
340
|
+
item[field] || item[field.to_sym]
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def compare_sort_values(left, right)
|
|
345
|
+
left_time = sortable_time(left)
|
|
346
|
+
right_time = sortable_time(right)
|
|
347
|
+
return left_time <=> right_time if left_time && right_time
|
|
348
|
+
|
|
349
|
+
left.to_s <=> right.to_s
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def sortable_time(value)
|
|
353
|
+
case value
|
|
354
|
+
when Time then value
|
|
355
|
+
when ::Date then value.to_time
|
|
356
|
+
when String
|
|
357
|
+
Document.parse_date(value)&.to_time
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
274
361
|
def parse_json_collection(value)
|
|
275
362
|
trimmed = value.strip
|
|
276
363
|
return value unless trimmed.start_with?("[") || trimmed.start_with?("{")
|
data/lib/hammer/tag_parser.rb
CHANGED
|
@@ -240,7 +240,7 @@ module Hammer
|
|
|
240
240
|
end
|
|
241
241
|
item_content = process_nested_properties(item_content, item_hash, item_name)
|
|
242
242
|
item_content = process_conditionals_in_loop(item_content, item_hash, item_name)
|
|
243
|
-
item_content
|
|
243
|
+
process_helper_expressions_in_loop(item_content, item_hash, item_name)
|
|
244
244
|
end.join
|
|
245
245
|
|
|
246
246
|
result.sub!(match[0], processed)
|
|
@@ -270,6 +270,89 @@ module Hammer
|
|
|
270
270
|
end
|
|
271
271
|
end
|
|
272
272
|
|
|
273
|
+
def process_helper_expressions_in_loop(content, item, item_name)
|
|
274
|
+
content.gsub(/<!--\s*\$([^>]+?)\s*-->/) do
|
|
275
|
+
expr = Regexp.last_match(1).strip
|
|
276
|
+
next Regexp.last_match(0) unless expr.include?("helpers.") || expr.start_with?("#{item_name}.")
|
|
277
|
+
|
|
278
|
+
evaluate_loop_helper_expression(expr, item, item_name) || Regexp.last_match(0)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def evaluate_loop_helper_expression(expr, item, item_name)
|
|
283
|
+
if expr.include?("helpers.") && expr.include?("(")
|
|
284
|
+
helper_name, args = parse_helper_call(expr)
|
|
285
|
+
return nil unless helper_name&.start_with?("helpers.")
|
|
286
|
+
|
|
287
|
+
evaluated_args = args.map { |arg| evaluate_loop_helper_argument(arg, item, item_name) }
|
|
288
|
+
Content::Helpers.call(helper_name.delete_prefix("helpers."), evaluated_args)
|
|
289
|
+
elsif expr.start_with?("#{item_name}.")
|
|
290
|
+
value = expr.split(".").drop(1).reduce(item) do |obj, key|
|
|
291
|
+
obj.is_a?(Hash) ? (obj[key] || obj[key.to_sym]) : nil
|
|
292
|
+
end
|
|
293
|
+
value_to_string(value)
|
|
294
|
+
end
|
|
295
|
+
rescue StandardError
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def parse_helper_call(expr)
|
|
300
|
+
open = expr.index("(")
|
|
301
|
+
name = expr[0...open].strip
|
|
302
|
+
args_str = expr[open + 1..]
|
|
303
|
+
close = find_matching_paren(args_str)
|
|
304
|
+
args = split_helper_args(args_str[0...close])
|
|
305
|
+
[name, args]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def find_matching_paren(str)
|
|
309
|
+
depth = 1
|
|
310
|
+
str.chars.each_with_index do |ch, idx|
|
|
311
|
+
depth += 1 if ch == "("
|
|
312
|
+
if ch == ")"
|
|
313
|
+
depth -= 1
|
|
314
|
+
return idx if depth.zero?
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
str.length - 1
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def split_helper_args(args_str)
|
|
321
|
+
args = []
|
|
322
|
+
current = +""
|
|
323
|
+
in_quotes = false
|
|
324
|
+
quote = nil
|
|
325
|
+
|
|
326
|
+
args_str.each_char do |ch|
|
|
327
|
+
if !in_quotes && (ch == '"' || ch == "'")
|
|
328
|
+
in_quotes = true
|
|
329
|
+
quote = ch
|
|
330
|
+
current << ch
|
|
331
|
+
elsif in_quotes && ch == quote
|
|
332
|
+
in_quotes = false
|
|
333
|
+
quote = nil
|
|
334
|
+
current << ch
|
|
335
|
+
elsif !in_quotes && ch == ","
|
|
336
|
+
args << current.strip
|
|
337
|
+
current = +""
|
|
338
|
+
else
|
|
339
|
+
current << ch
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
args << current.strip unless current.strip.empty?
|
|
343
|
+
args
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def evaluate_loop_helper_argument(arg, item, item_name)
|
|
347
|
+
trimmed = arg.strip
|
|
348
|
+
if (trimmed.start_with?('"') && trimmed.end_with?('"')) ||
|
|
349
|
+
(trimmed.start_with?("'") && trimmed.end_with?("'"))
|
|
350
|
+
return trimmed[1..-2]
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
evaluate_loop_helper_expression(trimmed, item, item_name)
|
|
354
|
+
end
|
|
355
|
+
|
|
273
356
|
def process_conditionals_in_loop(content, item, item_name)
|
|
274
357
|
pattern = /<!--\s*@(if|unless)\s+(.+?)\s*-->(.*?)(?:<!--\s*@else\s*-->(.*?))?<!--\s*@(endif|endunless|end)\s*-->/m
|
|
275
358
|
result = content.dup
|
|
@@ -288,9 +371,11 @@ module Hammer
|
|
|
288
371
|
end
|
|
289
372
|
|
|
290
373
|
def evaluate_loop_condition(condition, item, item_name)
|
|
291
|
-
if (m = condition.match(/\A
|
|
374
|
+
if (m = condition.match(/\A@?#{Regexp.escape(item_name)}\.(.+)\z/))
|
|
292
375
|
prop = m[1]
|
|
293
|
-
val = prop.split(".").reduce(item)
|
|
376
|
+
val = prop.split(".").reduce(item) do |obj, key|
|
|
377
|
+
obj.is_a?(Hash) ? (obj[key] || obj[key.to_sym]) : nil
|
|
378
|
+
end
|
|
294
379
|
return truthy?(val)
|
|
295
380
|
end
|
|
296
381
|
|
|
@@ -371,14 +456,13 @@ module Hammer
|
|
|
371
456
|
paths = [search_path] unless index_paths.empty?
|
|
372
457
|
end
|
|
373
458
|
|
|
374
|
-
relative_path =
|
|
459
|
+
relative_path = paths.first || search_path
|
|
460
|
+
relative_path = relative_path.gsub(" ", "%20")
|
|
375
461
|
relative_path = "#{relative_path}/" if preserve_slash && !relative_path.end_with?("/")
|
|
376
462
|
|
|
377
463
|
@delegate&.warning(@current_file_path, "File not found: #{ref}") if paths.empty?
|
|
378
464
|
|
|
379
465
|
generate_context_aware_path(relative_path, preserve_trailing_slash: preserve_slash)
|
|
380
|
-
.tr("%20", " ")
|
|
381
|
-
.gsub(" ", "%20")
|
|
382
466
|
end
|
|
383
467
|
|
|
384
468
|
def root_relative_path
|
data/lib/hammer/version.rb
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "blog listing page" do
|
|
6
|
+
let(:beachio) { File.expand_path("../../../../Beachio/Beachio", __dir__) }
|
|
7
|
+
|
|
8
|
+
it "renders sorted posts and formatted dates on export" do
|
|
9
|
+
skip "Beachio fixture not present locally" unless File.directory?(beachio)
|
|
10
|
+
|
|
11
|
+
site = Hammer::ProjectResolver.create_site(beachio)
|
|
12
|
+
builder = Hammer::Builder.new(site: site, mode: :export, clean: true)
|
|
13
|
+
builder.build
|
|
14
|
+
|
|
15
|
+
html = Hammer.read_text(File.join(beachio, "Build/blog/index.html"))
|
|
16
|
+
card_count = html.scan('class="blog-card"').length
|
|
17
|
+
|
|
18
|
+
expect(card_count).to be > 10
|
|
19
|
+
expect(html).to include('class="blog-card__date">03 Sep 2026</time>')
|
|
20
|
+
expect(html).not_to include("$helpers.formatDate")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -23,4 +23,14 @@ RSpec.describe Hammer::TagParser do
|
|
|
23
23
|
|
|
24
24
|
expect(result).to include('href="../../"')
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
it "does not corrupt digits in image filenames when resolving @path" do
|
|
28
|
+
parser.current_file_path = "products/forge/index.html"
|
|
29
|
+
content = '<img src="<!-- @path assets/images/products/forge-2.jpg -->" />'
|
|
30
|
+
|
|
31
|
+
result = parser.send(:parse, content)
|
|
32
|
+
|
|
33
|
+
expect(result).to include('src="../../assets/images/products/forge-2.jpg"')
|
|
34
|
+
expect(result).not_to include("forge-%20.jpg")
|
|
35
|
+
end
|
|
26
36
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: beachio-hammer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.pre5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Beach.io
|
|
@@ -77,6 +77,7 @@ files:
|
|
|
77
77
|
- scripts/test-classic.rb
|
|
78
78
|
- scripts/test-content.rb
|
|
79
79
|
- scripts/tree-diff.rb
|
|
80
|
+
- spec/blog_listing_spec.rb
|
|
80
81
|
- spec/encoding_spec.rb
|
|
81
82
|
- spec/parity/classic_spec.rb
|
|
82
83
|
- spec/parity/content_spec.rb
|