beachio-hammer 0.1.0.pre1 → 0.1.0.pre3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fbc6c05d882741fa94434691763470a2a8ae5cec91713e0b0fb9e4e0cd25bca
4
- data.tar.gz: 22394aa9fae0d7b44a53d511a94a7a02a052c6cab1af769f51601e4c55adf166
3
+ metadata.gz: 9ff203a27e8f29525a3120a073279ab2e8578f9d60a49a7f1c62c7ebf1a59253
4
+ data.tar.gz: e706eaee172b73d6abf8c14f7d936adb48144b2c4b2fc8ebdbf44fcfb9952f81
5
5
  SHA512:
6
- metadata.gz: 8703bd2e45433905cdd2d503d70e7179d5aed28c35a6f2c32f1418dafd4267c80bd0cb6162531c18c61b0a426bd2f5a7f7d9d361a68f8caed04c414f375d9071
7
- data.tar.gz: 62021958be80b18b3bd59059dc339399ca80650d1d9ea0e07d4f908093fb21f0ac8a95981d848f9495bd556ce3718e63e799186f21a28881d985c0f686d2b292
6
+ metadata.gz: 47a316f9de1c74ec4c16881c2663ab8aa172256264fe6e7aa66aa84d95f7d26a8082bdf521bc998bcb26f5e8bc621b8b05c6301034ead326e2dbb8271e292185
7
+ data.tar.gz: 3c5cbc689cb680d0e3adf630adfc2add3f13285aebdd2f6dae258ea83deff4a7364dfe609ac816e638dab2b732ab2cf63404b9effe1d5424afb8ac182e7e35d0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- beachio-hammer (0.1.0.pre1)
4
+ beachio-hammer (0.1.0.pre2)
5
5
  json (~> 2.7)
6
6
 
7
7
  GEM
data/PUBLISH.md CHANGED
@@ -10,8 +10,21 @@ bundle exec rspec
10
10
 
11
11
  ## Capture golden Build/ trees (macOS Swift CLI)
12
12
 
13
+ From repo root:
14
+
15
+ ```bash
16
+ ./gem/scripts/capture-goldens.sh
17
+ ```
18
+
19
+ Or manually:
20
+
13
21
  ```bash
14
- ./scripts/capture-goldens.sh
22
+ xcodebuild -project Hammer-Swift.xcodeproj -scheme hammer-cli -configuration Release -derivedDataPath ./build-derived build
23
+ ./build-derived/Build/Products/Release/hammer build \
24
+ --project "Test Projects/classic-fixture" --mode export --clean --quiet
25
+ ./build-derived/Build/Products/Release/hammer build \
26
+ --project "Test Projects/content-generation-test" --mode export --clean --quiet
27
+ git add -f "Test Projects/classic-fixture/Build" "Test Projects/content-generation-test/Build"
15
28
  ```
16
29
 
17
30
  ## Publish to RubyGems
@@ -25,10 +38,63 @@ gem build beachio-hammer.gemspec
25
38
  gem push beachio-hammer-*.gem
26
39
  ```
27
40
 
41
+ Prerelease pin for executor team: **`beachio-hammer` `0.1.0.pre3`**
42
+
43
+ ### 0.1.0.pre3 changes (nested @path resolution)
44
+
45
+ - Resolve `@path` using full path segments (not basename only)
46
+ - Resolve directory paths via `index.html` lookup
47
+ - Always emit context-aware relative URLs (Swift parity)
48
+
49
+ ### 0.1.0.pre2 changes (Beachio / Linux fixes)
50
+
51
+ - Force UTF-8 text reads (`Hammer.read_text`) — fixes `invalid byte sequence in US-ASCII` on Render
52
+ - `require "date"` for content pipeline
53
+ - `find_file` skips directories (Swift parity)
54
+ - `resolve_output_path` returns empty when collection has no output pattern
55
+
28
56
  ## hammer-cloud integration
29
57
 
30
58
  Pin the published gem version in the executor Dockerfile:
31
59
 
32
60
  ```dockerfile
33
- RUN gem install beachio-hammer -v 0.1.0.pre1
61
+ RUN gem install beachio-hammer -v 0.1.0.pre3
34
62
  ```
63
+
64
+ Build step in the agent:
65
+
66
+ ```bash
67
+ hammer build --project "$WORKSPACE" --mode export --format json --clean
68
+ ```
69
+
70
+ ### CLI contract
71
+
72
+ | Flag | Default | Notes |
73
+ |------|---------|-------|
74
+ | `--project` | cwd (walk-up for markers) | Project root |
75
+ | `--output` | `<project>/Build` | Output directory |
76
+ | `--mode` | `export` | `normal`, `export`, `publish` |
77
+ | `--format` | `human` | `json` for agent parsing |
78
+ | `--clean` | off | Wipes Build/ before compile |
79
+
80
+ **Exit codes:** `0` success, `1` build errors, `2` config error, `3` internal.
81
+
82
+ **JSON stdout** (`--format json`):
83
+
84
+ ```json
85
+ {
86
+ "status": "success",
87
+ "stats": { "filesProcessed", "errors", "warnings", "durationMs", "buildSize" },
88
+ "errors": [{ "file", "message" }],
89
+ "warnings": [{ "file", "message" }]
90
+ }
91
+ ```
92
+
93
+ ### Parity fixtures
94
+
95
+ | Fixture | Path | Files |
96
+ |---------|------|-------|
97
+ | Classic | `Test Projects/classic-fixture` | 9 |
98
+ | Content | `Test Projects/content-generation-test` | 13 |
99
+
100
+ CI: `.github/workflows/gem-parity.yml` — macOS re-exports goldens, Linux runs `bundle exec rspec`.
data/exe/hammer CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ Encoding.default_external = Encoding::UTF_8
5
+ Encoding.default_internal = Encoding::UTF_8
6
+
4
7
  require "beachio-hammer"
5
8
 
6
9
  exit Hammer::CLI.run(ARGV)
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+ require "time"
5
+
3
6
  require_relative "hammer/version"
7
+ require_relative "hammer/io"
4
8
  require_relative "hammer/errors"
5
9
  require_relative "hammer/ignore"
6
10
  require_relative "hammer/tag_parser"
@@ -134,7 +134,7 @@ module Hammer
134
134
 
135
135
  case ext
136
136
  when ".html"
137
- content = File.read(src)
137
+ content = Hammer.read_text(src)
138
138
  if !@content_collections.empty? && content.include?("@loop") && content.include?("collections.")
139
139
  content = Content::PageGenerator.pre_process_collection_loops(
140
140
  content,
@@ -148,7 +148,7 @@ module Hammer
148
148
  FileUtils.mkdir_p(File.dirname(dest))
149
149
  File.write(dest, processed)
150
150
  when ".css"
151
- processed = css_parser.parse(File.read(src), relative_path: rel)
151
+ processed = css_parser.parse(Hammer.read_text(src), relative_path: rel)
152
152
  FileUtils.mkdir_p(File.dirname(dest))
153
153
  File.write(dest, processed)
154
154
  else
@@ -45,7 +45,7 @@ module Hammer
45
45
  end
46
46
 
47
47
  def self.load(path)
48
- data = JSON.parse(File.read(path))
48
+ data = JSON.parse(Hammer.read_text(path))
49
49
  collections = data.fetch("collections", {}).transform_values { |c| CollectionConfig.from_hash(c) }
50
50
  new(collections: collections, site_url: data["siteUrl"])
51
51
  end
@@ -28,7 +28,7 @@ module Hammer
28
28
 
29
29
  def date
30
30
  val = metadata["date"]
31
- return val if val.is_a?(Time) || val.is_a?(Date)
31
+ return val if val.is_a?(Time) || val.is_a?(::Date)
32
32
 
33
33
  self.class.parse_date(val.to_s) if val
34
34
  end
@@ -83,7 +83,7 @@ module Hammer
83
83
  value.map { |v| serialize_value(v) }
84
84
  when Hash
85
85
  value.transform_values { |v| serialize_value(v) }
86
- when Time, Date
86
+ when Time, ::Date
87
87
  value.utc.iso8601(3)
88
88
  else
89
89
  value
@@ -97,7 +97,7 @@ module Hammer
97
97
  when String then value
98
98
  when TrueClass, FalseClass then value.to_s
99
99
  when Numeric then value.to_s
100
- when Time, Date then value.to_s
100
+ when Time, ::Date then value.to_s
101
101
  when Document then value.slug
102
102
  else value.to_s
103
103
  end
@@ -33,7 +33,7 @@ module Hammer
33
33
  format = "medium" if format.empty? || format == "null"
34
34
 
35
35
  time = case date_val
36
- when Time, Date then date_val.to_time
36
+ when Time, ::Date then date_val.to_time
37
37
  when String then Document.parse_date(date_val)
38
38
  else nil
39
39
  end
@@ -59,6 +59,8 @@ module Hammer
59
59
 
60
60
  def resolve_output_path(doc, collection_config)
61
61
  pattern = collection_config.output_path_pattern
62
+ return "" if pattern.nil? || pattern.empty?
63
+
62
64
  slug_field = collection_config.slug&.dig("field") || "slug"
63
65
  slug = resolve_slug(doc, collection_config.slug)
64
66
  interpolate_output_path(pattern, slug, slug_field)
@@ -88,7 +90,7 @@ module Hammer
88
90
  end
89
91
 
90
92
  def generate_page(template_path, doc, output_path)
91
- template = File.read(template_path)
93
+ template = Hammer.read_text(template_path)
92
94
  site = { "title" => "My Site" }
93
95
  context = TemplateContext.new(
94
96
  site: site,
@@ -8,7 +8,7 @@ module Hammer
8
8
  end
9
9
 
10
10
  def parse_markdown_file(path)
11
- content = File.read(path)
11
+ content = Hammer.read_text(path)
12
12
  metadata, body = extract_front_matter(content)
13
13
  html_body = parse_markdown_body(body)
14
14
  collection = extract_collection(path)
@@ -22,7 +22,7 @@ module Hammer
22
22
  end
23
23
 
24
24
  def parse_yaml_file(path)
25
- metadata = parse_simple_yaml(File.read(path))
25
+ metadata = parse_simple_yaml(Hammer.read_text(path))
26
26
  Document.new(
27
27
  file_path: path,
28
28
  metadata: metadata,
@@ -35,7 +35,7 @@ module Hammer
35
35
  end
36
36
 
37
37
  @processed_includes << include_path
38
- included = File.read(File.join(@source_directory, include_path))
38
+ included = Hammer.read_text(File.join(@source_directory, include_path))
39
39
  result.sub!(match[0], "\n#{process_includes(included)}\n")
40
40
  end
41
41
 
data/lib/hammer/ignore.rb CHANGED
@@ -13,7 +13,7 @@ module Hammer
13
13
  path = File.join(source_dir, ".hammer-ignore")
14
14
  return [] unless File.file?(path)
15
15
 
16
- File.read(path, encoding: "UTF-8").each_line.map(&:strip).reject do |line|
16
+ Hammer.read_text(path).each_line.map(&:strip).reject do |line|
17
17
  line.empty? || line.start_with?("#")
18
18
  end
19
19
  end
data/lib/hammer/io.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hammer
4
+ module_function
5
+
6
+ def read_text(path)
7
+ File.binread(path).force_encoding(Encoding::UTF_8)
8
+ end
9
+ end
@@ -54,7 +54,7 @@ module Hammer
54
54
  path = File.join(@build_directory, "sitemap.xml")
55
55
  return [] unless File.file?(path)
56
56
 
57
- File.read(path).scan(%r{<loc>([^<]+)</loc>}).flatten
57
+ Hammer.read_text(path).scan(%r{<loc>([^<]+)</loc>}).flatten
58
58
  end
59
59
 
60
60
  def generate_xml(pages)
@@ -25,7 +25,7 @@ module Hammer
25
25
 
26
26
  def process_file(relative_path, output_path_for_styling: nil)
27
27
  @current_file_path = output_path_for_styling || relative_path
28
- content = File.read(File.join(@source_directory, relative_path))
28
+ content = Hammer.read_text(File.join(@source_directory, relative_path))
29
29
  process_file_content(content, relative_path, output_path_for_styling)
30
30
  end
31
31
 
@@ -167,7 +167,7 @@ module Hammer
167
167
  end
168
168
 
169
169
  @processed_includes << path
170
- included = File.read(File.join(@source_directory, path))
170
+ included = Hammer.read_text(File.join(@source_directory, path))
171
171
  child = self.class.new(
172
172
  source_directory: @source_directory,
173
173
  parent_variables: @parent_variables.merge(@variables),
@@ -349,34 +349,58 @@ module Hammer
349
349
  ref = get_variable(ref[1..]) || ref
350
350
  end
351
351
 
352
- if ref == "/"
353
- generate_context_aware_path(".")
354
- else
355
- preserve_slash = ref.end_with?("/")
356
- filename = preserve_slash ? ref.chomp("/") : ref
357
- paths, error = find_file(File.basename(filename))
358
- raise TagError, error if error
359
-
360
- if paths.empty?
361
- @delegate&.warning(@current_file_path, "File not found: #{ref}")
362
- ref
363
- else
364
- path = paths.first
365
- path = "#{path}/" if preserve_slash
366
- generate_context_aware_path(path).gsub("%20", " ").gsub(" ", "%20")
367
- end
368
- end
352
+ resolve_path_reference(ref)
369
353
  end
370
354
  end
371
355
 
356
+ def resolve_path_reference(ref)
357
+ if ref == "/"
358
+ return root_relative_path
359
+ end
360
+
361
+ preserve_slash = ref.end_with?("/")
362
+ search_path = preserve_slash ? ref.chomp("/") : ref
363
+ paths, error = find_file(search_path)
364
+ raise TagError, error if error
365
+
366
+ if preserve_slash && paths.empty?
367
+ index_path = "#{search_path}/index.html"
368
+ index_paths, index_error = find_file(index_path)
369
+ raise TagError, index_error if index_error
370
+
371
+ paths = [search_path] unless index_paths.empty?
372
+ end
373
+
374
+ relative_path = (paths.first || search_path).tr(" ", "%20")
375
+ relative_path = "#{relative_path}/" if preserve_slash && !relative_path.end_with?("/")
376
+
377
+ @delegate&.warning(@current_file_path, "File not found: #{ref}") if paths.empty?
378
+
379
+ generate_context_aware_path(relative_path, preserve_trailing_slash: preserve_slash)
380
+ .tr("%20", " ")
381
+ .gsub(" ", "%20")
382
+ end
383
+
384
+ def root_relative_path
385
+ return "./" unless @current_file_path
386
+
387
+ current_dir = current_directory_components
388
+ return "./" if current_dir.empty?
389
+
390
+ "#{"../" * current_dir.length}"
391
+ end
392
+
372
393
  def find_file(filename)
373
394
  found = []
395
+ direct_path = File.join(@source_directory, filename)
396
+ found << filename if File.file?(direct_path)
397
+
374
398
  dirs = %w[assets/img assets/images assets/media images img media]
375
399
  dirs.each do |dir|
376
400
  path = "#{dir}/#{filename}"
377
- found << path if File.exist?(File.join(@source_directory, path))
401
+ found << path if File.file?(File.join(@source_directory, path))
378
402
  end
379
- found << filename if File.exist?(File.join(@source_directory, filename))
403
+ found.uniq!
380
404
 
381
405
  if found.empty? || found.length > 1
382
406
  Dir.glob(File.join(@source_directory, "**", "*")).each do |f|
@@ -384,7 +408,9 @@ module Hammer
384
408
  rel = f.delete_prefix("#{@source_directory}/")
385
409
  next if skip_scan_path?(rel)
386
410
 
387
- found << rel if File.basename(f) == filename && !found.include?(rel)
411
+ if rel == filename || File.basename(f) == filename
412
+ found << rel unless found.include?(rel)
413
+ end
388
414
  end
389
415
  end
390
416
 
@@ -394,26 +420,37 @@ module Hammer
394
420
  [found, nil]
395
421
  end
396
422
 
397
- def generate_context_aware_path(file_path)
423
+ def current_directory_components
424
+ @current_file_path.split("/")[0...-1].reject { |part| part == "." || part.empty? }
425
+ end
426
+
427
+ def generate_context_aware_path(file_path, preserve_trailing_slash: file_path.end_with?("/"))
398
428
  return "./#{file_path}" unless @current_file_path
399
429
 
400
- path_for_processing = file_path.end_with?("/") ? file_path.chomp("/") : file_path
401
- current_dir = File.dirname(@current_file_path).split("/").reject { |p| p == "." }
430
+ path_for_processing = preserve_trailing_slash ? file_path.chomp("/") : file_path
431
+ current_dir = current_directory_components
402
432
  target_parts = path_for_processing.split("/")
403
433
  common = 0
404
434
  common += 1 while common < current_dir.length && common < target_parts.length && current_dir[common] == target_parts[common]
405
435
 
406
- levels_up = current_dir.length - common
407
- remaining = target_parts[common..]
408
- parts = ([".."] * levels_up) + remaining
409
-
410
- if parts.empty?
411
- "./#{target_parts.last}"
412
- elsif levels_up.zero? && remaining.length == 1
413
- "./#{remaining[0]}"
436
+ if current_dir == target_parts
437
+ result = "./"
414
438
  else
415
- parts.join("/")
416
- end.tap { |r| r << "/" if file_path.end_with?("/") && !r.end_with?("/") }
439
+ levels_up = current_dir.length - common
440
+ remaining = target_parts[common..]
441
+ parts = ([".."] * levels_up) + remaining
442
+
443
+ result = if parts.empty?
444
+ "./#{target_parts.last}"
445
+ elsif levels_up.zero? && remaining.length == 1
446
+ "./#{remaining[0]}"
447
+ else
448
+ parts.join("/")
449
+ end
450
+ end
451
+
452
+ result << "/" if preserve_trailing_slash && !result.end_with?("/")
453
+ result
417
454
  end
418
455
 
419
456
  def process_navigation_helpers(content)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hammer
4
- VERSION = "0.1.0.pre1"
4
+ VERSION = "0.1.0.pre3"
5
5
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "tmpdir"
5
+
6
+ RSpec.describe "UTF-8 file reads under US-ASCII locale" do
7
+ it "reads smart quotes without raising" do
8
+ Dir.mktmpdir do |dir|
9
+ path = File.join(dir, "sample.html")
10
+ File.binwrite(path, "<p>It\u2019s working \u2014 really.</p>".encode("UTF-8"))
11
+
12
+ previous_external = Encoding.default_external
13
+ previous_internal = Encoding.default_internal
14
+ Encoding.default_external = Encoding::US_ASCII
15
+ Encoding.default_internal = Encoding::US_ASCII
16
+
17
+ expect { Hammer.read_text(path) }.not_to raise_error
18
+ expect(Hammer.read_text(path)).to include("really")
19
+
20
+ Encoding.default_external = previous_external
21
+ Encoding.default_internal = previous_internal
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Hammer::TagParser do
6
+ let(:source_directory) { File.expand_path("../../Test Projects/classic-fixture", __dir__) }
7
+ let(:parser) { described_class.new(source_directory: source_directory) }
8
+
9
+ it "resolves nested @path references relative to the output page" do
10
+ parser.current_file_path = "work/mixfit/index.html"
11
+ content = '<a href="<!-- @path products/gleo/ -->">'
12
+
13
+ result = parser.send(:parse, content)
14
+
15
+ expect(result).to include('href="../../products/gleo/"')
16
+ end
17
+
18
+ it "resolves root @path references from nested pages" do
19
+ parser.current_file_path = "work/mixfit/index.html"
20
+ content = '<a href="<!-- @path / -->">'
21
+
22
+ result = parser.send(:parse, content)
23
+
24
+ expect(result).to include('href="../../"')
25
+ end
26
+ 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.pre1
4
+ version: 0.1.0.pre3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Beach.io
@@ -67,6 +67,7 @@ files:
67
67
  - lib/hammer/css_parser.rb
68
68
  - lib/hammer/errors.rb
69
69
  - lib/hammer/ignore.rb
70
+ - lib/hammer/io.rb
70
71
  - lib/hammer/output_formatter.rb
71
72
  - lib/hammer/project_resolver.rb
72
73
  - lib/hammer/sitemap_generator.rb
@@ -76,8 +77,10 @@ files:
76
77
  - scripts/test-classic.rb
77
78
  - scripts/test-content.rb
78
79
  - scripts/tree-diff.rb
80
+ - spec/encoding_spec.rb
79
81
  - spec/parity/classic_spec.rb
80
82
  - spec/parity/content_spec.rb
83
+ - spec/path_resolution_spec.rb
81
84
  - spec/spec_helper.rb
82
85
  homepage: https://github.com/beachio/hammer-swift
83
86
  licenses: