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 +4 -4
- data/CHANGELOG.md +13 -0
- data/exe/natsuzora-difftest-worker +79 -0
- data/lib/natsuzora/data/lexers/template.lkt1 +1 -1
- data/lib/natsuzora/errors.rb +2 -2
- data/lib/natsuzora/lexer.rb +31 -3
- data/lib/natsuzora/lexers/template.rb +6 -1
- data/lib/natsuzora/parser.rb +2 -2
- data/lib/natsuzora/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e53d2581700430dcb3c1e3c617027e2ba09e4e32e5fc89437fda4f73ac72008
|
|
4
|
+
data.tar.gz: 17513673d3259129e062528abeb98880d14b391f6df5ff5d02d373568c70aa47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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":
|
|
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="}
|
data/lib/natsuzora/errors.rb
CHANGED
data/lib/natsuzora/lexer.rb
CHANGED
|
@@ -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
|
|
80
|
-
|
|
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
|
-
|
|
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, '#'
|
data/lib/natsuzora/parser.rb
CHANGED
|
@@ -257,7 +257,7 @@ module Natsuzora
|
|
|
257
257
|
|
|
258
258
|
token = current_token
|
|
259
259
|
if current_type == :INVALID
|
|
260
|
-
raise
|
|
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
|
|
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
|
|
data/lib/natsuzora/version.rb
CHANGED
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|