natsuzora 0.4.0 → 0.4.1

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: c392b524dfcc75df005176d7e8dc65337be7d94f26575f9b0816c40a7e061d4c
4
- data.tar.gz: f660922c286c77a2b2ac6e863ae4a8f2427eb50ba7a70c50838d0d56e524c898
3
+ metadata.gz: 3e53d2581700430dcb3c1e3c617027e2ba09e4e32e5fc89437fda4f73ac72008
4
+ data.tar.gz: 17513673d3259129e062528abeb98880d14b391f6df5ff5d02d373568c70aa47
5
5
  SHA512:
6
- metadata.gz: 56c8ad664c64fb5f94992d1eadb163276c14494ab7c452b19d32d2a7fd81fcaedbf5eb89707cd0d9b0151b90ef7d20efce805a680b594bfd1be7be644e5d383d
7
- data.tar.gz: 0f68bef0e391fc137904d96287d5daad54bbd29de293477b705138436bb5c6ff0efe80cbf94d584b2a69d459aa748418b7a5f43aeeacd501df6cef80ef0f6be9
6
+ metadata.gz: f5038da577f2103e420942316ab84cdc6a2beba76a7aeecb917494871489df1e8a59d4bde14dcedd8376c5b6d1d4ce0a9ba49c18f04c20745fff956573336992
7
+ data.tar.gz: d0df3745de164a007ee5ce248ab5c6f3077181337bf276f57f127af785114c4ae670d209846b30a1042278878d4189cd4493ee10bbf63511e7972a442fd0b6d9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Added
6
+ - `exe/natsuzora-difftest-worker`: JSONL worker for the
7
+ cross-implementation differential tests (`spec/difftest.md`).
8
+
9
+ ### Changed
10
+ - Comment content is raw-scanned to the first `]}`; any characters are
11
+ now allowed inside `{[% ... ]}`.
12
+ - `LexerError` is now a subclass of `ParseError`; the parser raises
13
+ `ParseError` for include-path violations.
14
+ - Requires `lexer_kit` >= 0.6.1.
15
+
3
16
  ## 0.4.0
4
17
 
5
18
  ### Added
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # JSONL worker for differential testing (see spec/difftest.md).
5
+ #
6
+ # Reads one request per line from stdin:
7
+ # {"id": 1, "template": "...", "data": {...}, "partials": {"/name": "..."}}
8
+ # Writes one response per line to stdout:
9
+ # {"id": 1, "ok": true, "output": "..."}
10
+ # {"id": 1, "ok": false, "error": "<canonical error type>"}
11
+ #
12
+ # Non-Natsuzora exceptions are intentionally not rescued: a crash of this
13
+ # worker is a harness error, not a render outcome.
14
+
15
+ require 'json'
16
+ require 'tmpdir'
17
+ require 'fileutils'
18
+ require 'natsuzora'
19
+
20
+ module DifftestWorker
21
+ module_function
22
+
23
+ # Maps a Natsuzora exception to the canonical error type of the
24
+ # difftest protocol. Order matters: subclasses before parents.
25
+ def canonical_error(error)
26
+ case error
27
+ when Natsuzora::ReservedWordError then 'ReservedWordError'
28
+ when Natsuzora::ParseError then 'ParseError' # includes LexerError
29
+ when Natsuzora::UndefinedVariableError then 'UndefinedVariable'
30
+ when Natsuzora::TypeError then 'TypeError'
31
+ when Natsuzora::ShadowingError then 'ShadowingError'
32
+ when Natsuzora::IncludeError then 'IncludeError'
33
+ else "Unmapped(#{error.class})"
34
+ end
35
+ end
36
+
37
+ # Same materialization rule as the shared spec tests:
38
+ # "/a/b" -> <root>/a/_b.ntzr
39
+ def materialize_partials(partials, dir)
40
+ partials.each do |name, content|
41
+ segments = name.split('/').reject(&:empty?)
42
+ segments[-1] = "_#{segments[-1]}"
43
+ path = "#{File.join(dir, *segments)}.ntzr"
44
+ FileUtils.mkdir_p(File.dirname(path))
45
+ File.write(path, content)
46
+ end
47
+ end
48
+
49
+ def render(request)
50
+ template = request.fetch('template')
51
+ data = request.fetch('data')
52
+ partials = request['partials']
53
+
54
+ return Natsuzora.render(template, data) if partials.nil? || partials.empty?
55
+
56
+ Dir.mktmpdir('natsuzora_difftest') do |dir|
57
+ materialize_partials(partials, dir)
58
+ Natsuzora.render(template, data, include_root: dir)
59
+ end
60
+ end
61
+
62
+ def handle(request)
63
+ { id: request['id'], ok: true, output: render(request) }
64
+ rescue Natsuzora::Error => e
65
+ { id: request['id'], ok: false, error: canonical_error(e) }
66
+ end
67
+
68
+ def run(input: $stdin, output: $stdout)
69
+ output.sync = true
70
+ input.each_line do |line|
71
+ line = line.strip
72
+ next if line.empty?
73
+
74
+ output.puts(JSON.generate(handle(JSON.parse(line))))
75
+ end
76
+ end
77
+ end
78
+
79
+ DifftestWorker.run if $PROGRAM_NAME == __FILE__
@@ -1 +1 @@
1
- {"format":"lkt1","codec":"deflate+base64","kind":"program","table_version":2,"uncompressed_len":2226,"sha256":"f670fe5c25083a9642759f0aa0cd16670997d72b94f1323c676001b403fba8d3","data":"eJzdlW1PFEkQx/89s7M7sICIZj0vJqfx8YUCPmEuhuw2M427x+ws7OwCSsgGFZ/FB/SezCX7/r4Un+C+kvevnj3pAyX4ztjkx1R11VRXV0/1Jgudq/ABKJzCWK/XNplpL5u4N93rodRIl3XSiP9nuEaDq1/fo9/Yo9/co8/s0W9RL3TMagelRdOOTNpBIdZZHUGUtDKDQt0qWSKP0Tmd3ul108xE3bbBiFUbaZR0Y4OyWY0S3dSdRitFsLDSa8xjiI9umpgsQ4miSRjRCjqqWx+dwY9bHa7WajY1ArPU1QnCpa7JbJzhlXqjY7JFHRkEjVjS81B6uPlo4/2LdxiWxHtPt7Y237rTMlwTEALePxiH92ENZboGH9b4t/4X1HmoK/BEOgs1pc7R+SgZt0HURzvs+TA+nyeJ78wXHDlw5KIjlxw5dOQhRx627wMnrLw7X3bkEUcedeQxRz7iyOM2D+CHM7KBpxL5Pf95W7KHbUl+U1Z9IG5iKNlKydyQuJTFMPJCFhPr2EMJv2njztgqMNS+Ku3KXl52/CQLq0d25k9bjTyrL7+3Kx+u0lJJ4IIkqSRxT3L2JdOC7DLYtl5/D/Ip7T/ZQ6yXn9CVA09ZTguYlIXVhnhKHt72p2RsoZ9Y18MWcITclFhqOy/NJNRlqFmoqtjVKUbx8ZVDfRqDCe+LA3mBDrR/S8OzeLZdOVSF1dlNUjas9r/0manvceS1ySsjhRgFIsyd51fLz3p0nc19FpiYAo7zvqiwa39kR53hl3uJH+D0JHDrMvDzLHC7Csyy5apsiSqjVE8TDeg5EpGYGDJP7pA6aZBfyAJJSJOkpEUWyRJpk4x0SJcskxWySu6Se+Q+YSNp3kaal4p+TNhT+hl5Tthz+iXhRaBfkdfkDeHdptlDmr8O+lfyG/md/EF4LelBkerMH8dYnRpvC/4YlCaoT1OXO1S6knPz4sM9o4J+FHu1sIIdPvthRSHie+WrYlO1KPbBOfr4tTBV9PH7YezvhHEBIesB1ieUj69Z6J9mTaK4UAvj4s5/tqGB7WJu67u24YFtKrcxZgDXXh7YZ6w9yONWfJGZp8/cAtGZW5E5+sytWAs9cL7Y55N+9D/OLc/lew8/Dvb/L/ErdH4="}
1
+ {"format":"lkt1","codec":"deflate+base64","kind":"program","table_version":2,"uncompressed_len":2890,"sha256":"36ab7ec483cab141868691c5d10a754e6eb8ef5974f065f29953f8819905b718","data":"eJztVmtvE0cUPTO7a68TJ4SCLFUtLYhXP5AEKIQvTfHau2bXWdskayehCFm0hJZH00dKX6iSv/dP+Xt/FD13dkOm5mWQKoHEWMdz79y7d86cte44XetfgANA4Rjmh8ONKIs2NqNweH44RDnpbgZpEv4ncJEB2/98wr804V+e8Fcm/Cv03X603Ue12et0om5/2OiFN7Cw7w26zbSXRSHcMMhieMaDGxsnS2WaawTda0zMouZgI0LVuAmfG4QRZqPtZhp0gn7S68Jb2xomLVQ4DbpplGUo04xSVjRG0IxNTpDBCXt97kYWAbxofRCk8NcHUWbqzGzFST/KrgfNCF4Skic0ynd27t5+9PAXzMh5hvd2d3d+tpdl2CHAB/Q/OAT9+CZmmeo9vsnPrb+gFqFlOgm1rE4x8zCxYCqoJ2aYd6alAo4TjrXuWrZn2SXLLlu2b9kVy54xzwCfGPtgfdayq5Y9Z9nzln3IshfM3sCnJ+QA96TyI37pXTnDnpDfEdbfyNYSKBuZZK0iKbMSqD6UzSQ6f0fK75i6K0YFlnpGpQNbmxMCZ2Rjddes/FkwKr30uQN7OqVz9RaFpBLiWjg7wtSVU3p7Juvv/Ddg9p94s1PsJ28CuPTStyxvCLgsG6vbkik89N5TMkbo70zqtALOEV9ILbWXS7MEdQ5qFeoqCUJdJB0X0ww1VdazQ78i7rxh3f9n6OJbk5cu5gN9VG5TRPe9atZ4nmq5XzBVH3N+bc7q6djf54XDpL8i/jYNjVwflWunaka5/SEHfs4v501/TO/YyLXJlREh2MPaaJxmB10Ejt4CaieBD5eBj3gvHePtcIKd+zN2yGU2uitLwJfngPoq0LgKhLxCWmzvLVZpHScCIG4QTSIkIqJFXCNiIiHaxBqREh2iS/SI68Q6sUFkRJ8YEJvEFrFN3CC+Ir4m2LBj3noxL6/4W4K9O75PPCDY2+PvCV448Q/Ej8RPBO/QmL065l+Q+FfiN+J34g+C119ciNQjfxyhOnXeSvzTUf6A/nn6cldL9+daR3J4ZiS5kuBZDouiHdSP8GxtPlu5wPVQj/waRu1QjzmPZb0q6zWFdujU/Zqqcx75XcUcZ+yHLoi6L3WpXZk1dccdn5WaoTvywzL2Y34RW8pjYztWKWIrJuaxple34zNFfDWP53VrekybPB2uleg75FYiR4fcSiNfk39YGnOmz/yjlKOR6+I/KbT5F/9Mfuo="}
@@ -19,10 +19,10 @@ module Natsuzora
19
19
  end
20
20
  end
21
21
 
22
- class LexerError < Error; end
23
-
24
22
  class ParseError < Error; end
25
23
 
24
+ class LexerError < ParseError; end
25
+
26
26
  class ReservedWordError < ParseError; end
27
27
 
28
28
  class RenderError < Error; end
@@ -50,6 +50,16 @@ module Natsuzora
50
50
  line, col = stream.line_col
51
51
  raise LexerError.new("Unexpected character: '#{text}'", line: line, column: col)
52
52
 
53
+ when :COMMENT_BODY
54
+ line, col = stream.line_col
55
+ result.concat(comment_tokens(text, line, col))
56
+
57
+ when :COMMENT_UNCLOSED
58
+ # No `]}` before EOF: emit PERCENT only; the TokenProcessor
59
+ # reports the unclosed comment at this location.
60
+ line, col = stream.line_col
61
+ result << Token.new(:PERCENT, '%', line: line, column: col)
62
+
53
63
  else
54
64
  line, col = stream.line_col
55
65
  result << Token.new(name, text, line: line, column: col)
@@ -65,6 +75,24 @@ module Natsuzora
65
75
  text.gsub(ESCAPE_SEQUENCE, ESCAPED_VALUE)
66
76
  end
67
77
 
78
+ # Expands a raw comment-body match ("%...]}") into the token stream
79
+ # the TokenProcessor expects: PERCENT [DASH] CLOSE. A `-` immediately
80
+ # before `]}` is the right-trim flag, never content (spec 4.5.4).
81
+ def comment_tokens(text, line, column)
82
+ body = text[1..-3]
83
+ trim = body.end_with?('-')
84
+ content = trim ? body[0..-2] : body
85
+
86
+ tokens = [Token.new(:PERCENT, '%', line: line, column: column)]
87
+ pos_line, pos_col = position_after(line, column, "%#{content}")
88
+ if trim
89
+ tokens << Token.new(:DASH, '-', line: pos_line, column: pos_col)
90
+ pos_col += 1
91
+ end
92
+ tokens << Token.new(:CLOSE, ']}', line: pos_line, column: pos_col)
93
+ tokens
94
+ end
95
+
68
96
  def add_eof(tokens)
69
97
  if tokens.empty?
70
98
  tokens << Token.new(:EOF, nil, line: 1, column: 1)
@@ -76,10 +104,10 @@ module Natsuzora
76
104
  end
77
105
 
78
106
  def position_after_value(token)
79
- line = token.line
80
- column = token.column
81
- value = token.value || ''
107
+ position_after(token.line, token.column, token.value || '')
108
+ end
82
109
 
110
+ def position_after(line, column, value)
83
111
  value.each_char do |char|
84
112
  if char == "\n"
85
113
  line += 1
@@ -4,7 +4,12 @@ require 'lexer_kit'
4
4
 
5
5
  LexerKit.build do
6
6
  delimited :TEXT, delimiter: '{[', escape: '{[{]}' do
7
- token :PERCENT, '%'
7
+ # Comment body is raw-scanned to the first `]}` (spec 4.5.4): the
8
+ # tempered pattern cannot consume a `]}`, so it always terminates at
9
+ # the first occurrence. COMMENT_UNCLOSED catches the no-`]}` case
10
+ # (longest match guarantees COMMENT_BODY wins when a `]}` exists).
11
+ token :COMMENT_BODY, /%([^\]]|\]+[^}\]])*\]*\]\}/, pop: true
12
+ token :COMMENT_UNCLOSED, /%([^\]]|\]+[^}\]])*\]*/
8
13
  token :DASH, '-'
9
14
  token :CLOSE, ']}', pop: true
10
15
  token :HASH, '#'
@@ -257,7 +257,7 @@ module Natsuzora
257
257
 
258
258
  token = current_token
259
259
  if current_type == :INVALID
260
- raise LexerError.new("Invalid character in include path: '#{token.value}'",
260
+ raise ParseError.new("Invalid character in include path: '#{token.value}'",
261
261
  line: token.line, column: token.column)
262
262
  end
263
263
  unless current_type == :IDENT
@@ -266,7 +266,7 @@ module Natsuzora
266
266
 
267
267
  ident_token = consume(:IDENT)
268
268
  if ident_token.value.start_with?('_')
269
- raise LexerError.new("Include segment cannot start with underscore: #{ident_token.value}",
269
+ raise ParseError.new("Include segment cannot start with underscore: #{ident_token.value}",
270
270
  line: ident_token.line, column: ident_token.column)
271
271
  end
272
272
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Natsuzora
4
- VERSION = '0.4.0'
4
+ VERSION = '0.4.1'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: natsuzora
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aozora Bunko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-05-04 00:00:00.000000000 Z
10
+ date: 2026-06-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: lexer_kit
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.5.0
18
+ version: 0.6.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 0.5.0
25
+ version: 0.6.1
26
26
  description: Natsuzora is a minimal, display-only template language designed for static
27
27
  HTML generation and Rails preview templates.
28
28
  email:
@@ -35,6 +35,7 @@ files:
35
35
  - ".rubocop.yml"
36
36
  - CHANGELOG.md
37
37
  - Rakefile
38
+ - exe/natsuzora-difftest-worker
38
39
  - lib/natsuzora.rb
39
40
  - lib/natsuzora/ast.rb
40
41
  - lib/natsuzora/context.rb