natsukantou 0.2.2 → 0.2.3
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/.claude/skills/add-translator-backend/SKILL.md +84 -0
- data/.claude/skills/add-translator-backend/example-backend.md +137 -0
- data/.claude/skills/run-and-record-specs/SKILL.md +55 -0
- data/.claude/skills/wizard-config-conventions/SKILL.md +69 -0
- data/.rubocop.yml +8 -1
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +67 -0
- data/exe/natsukantou +11 -2
- data/lib/natsukantou/chat_gpt.rb +27 -7
- data/lib/natsukantou/setup/config_load_or_prompt.rb +1 -1
- data/lib/natsukantou/setup/config_prompt.rb +34 -6
- data/lib/natsukantou/substitude_glossary.rb +5 -2
- data/lib/natsukantou/version.rb +1 -1
- data/lib/natsukantou.rb +0 -2
- data/natsukantou.gemspec +2 -2
- metadata +13 -13
- data/lib/monkey_patch/builder.rb +0 -14
- data/lib/monkey_patch/runner.rb +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cad5c6f1f3972aa1ea7f4d793ff42bd3f08b96b292a3475ebc8be378d68e3e8b
|
|
4
|
+
data.tar.gz: cbcdce19fe10dcb3e6658959c9be61363a3ab30aa3096465c41a069811fb2cee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04e7580ca7090cd615ce9e51432c97b7159824394dbc0cedea74c4426426b7e78101d97f121fda63d02ad023907df6e2c3cbfab9349b08789f3dc4346eecd50f
|
|
7
|
+
data.tar.gz: 2cd1b3c1d8456a8c586baddf78711f6a3e52a8c8e3cafe16eadbb49bf0306ca5af2fd47378aec514aab6aea2f6ae046b9a58f323cd5f31f3564658baee74e7c8
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-translator-backend
|
|
3
|
+
description: Use when adding a new translation service backend (e.g. "add Google Translate", "add Claude as a translator") or a new middleware filter to Natsukantou. Covers the middleware contract, the lang-attribute invariant, wizard registration, gemspec, and the required specs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a translator backend (or middleware filter)
|
|
7
|
+
|
|
8
|
+
A backend is just a middleware class; there is no base class to inherit. Full annotated
|
|
9
|
+
example (class + spec): [example-backend.md](example-backend.md). Closest real models:
|
|
10
|
+
`lib/natsukantou/open_ai.rb` (chunked translation), `lib/natsukantou/deep_l.rb`
|
|
11
|
+
(whole-document translation).
|
|
12
|
+
|
|
13
|
+
## Procedure
|
|
14
|
+
|
|
15
|
+
1. **Create `lib/natsukantou/<snake_name>.rb`** with class `Natsukantou::<CamelName>`:
|
|
16
|
+
- `include UtilityBase` (gives `dom`, `dom_node`, `logger`).
|
|
17
|
+
- `initialize(app, **keyword_args)` — `app` is the only positional arg. Store it as
|
|
18
|
+
`@app`. Every other arg must be a keyword arg.
|
|
19
|
+
- **Doc-comment rules are load-bearing** — the setup wizard YARD-parses this file at
|
|
20
|
+
runtime. Follow `.claude/skills/wizard-config-conventions/SKILL.md` (short version:
|
|
21
|
+
`@param` tag for every kwarg except `app`; string defaults in double quotes).
|
|
22
|
+
- `call(env)` must, in order:
|
|
23
|
+
a. Translate `env.dom.to_xml` whole, **or** if you support `translate_by_section:`
|
|
24
|
+
(a CSS selector), loop `env[:dom].css(selector)` skipping nodes with empty
|
|
25
|
+
`.text`, replacing each via `node.replace(dom_node(translated_xml))`.
|
|
26
|
+
b. Set `env[:dom].lang = env[:lang_to].code` — required invariant, spec-enforced.
|
|
27
|
+
c. End with `@app.call(env)`.
|
|
28
|
+
- Language codes: `env.lang_from.code` is lowercase `xx` or `xx-yy`; re-case for your
|
|
29
|
+
API if needed (DeepL upcases). Compare with `env[:lang_to].is?('zh-TW')` (asymmetric:
|
|
30
|
+
`en-gb`.is?(`en`) → true, reverse → false).
|
|
31
|
+
- Parse API responses with `dom(...)`/`dom_node(...)`, never `Oga.parse_xml`.
|
|
32
|
+
- Keep to the Ruby floor in the gemspec's `required_ruby_version`; `.gitlab-ci.yml`
|
|
33
|
+
lists the versions CI builds.
|
|
34
|
+
|
|
35
|
+
2. **Register it** in `lib/natsukantou.rb` under the `# Translators` (or `# Middlewares`)
|
|
36
|
+
section:
|
|
37
|
+
```ruby
|
|
38
|
+
autoload_and_register :translator, :CamelName, "natsukantou/snake_name"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. **Add the API client gem** (if any) to `natsukantou.gemspec` as
|
|
42
|
+
`spec.add_dependency 'gem-name', '~> x.y'` with a comment naming the service
|
|
43
|
+
(match the existing `# DeepL` / `# Minhon` style), then `bundle install`.
|
|
44
|
+
|
|
45
|
+
4. **Write the unit spec** `spec/natsukantou/<snake_name>_spec.rb` — see
|
|
46
|
+
[example-backend.md](example-backend.md). Minimum: `:middleware` metadata, stub the
|
|
47
|
+
private `translate` method (or the client), and `include_examples 'lang attribute'`
|
|
48
|
+
(this is what enforces invariant 1b). No network, no secrets, no VCR needed.
|
|
49
|
+
|
|
50
|
+
5. **README.md**: add the service to the `### Translators` (or `### Middlewares`) list.
|
|
51
|
+
|
|
52
|
+
6. Do NOT touch `CHANGELOG.md` / `version.rb` unless asked to cut a release.
|
|
53
|
+
|
|
54
|
+
## Verify
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
bundle exec rspec spec/natsukantou/<snake_name>_spec.rb # all green
|
|
58
|
+
bundle exec rspec # no failures beyond the baseline you saw before starting
|
|
59
|
+
bundle exec rubocop lib/natsukantou/<snake_name>.rb spec/natsukantou/<snake_name>_spec.rb
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then confirm the wizard can parse your params (should print each kwarg with its default,
|
|
63
|
+
no exception):
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
bundle exec ruby -Ilib -e '
|
|
67
|
+
require "natsukantou"
|
|
68
|
+
require "natsukantou/setup/config_prompt"
|
|
69
|
+
comp = Natsukantou::Setup::Component.new(:CamelName, "lib/natsukantou/snake_name.rb")
|
|
70
|
+
comp.initialize_method_param_tags.each do |t|
|
|
71
|
+
d = t.default_value_in_string
|
|
72
|
+
p [t.name, (d && d != "nil") ? t.default_value_in_tty : d]
|
|
73
|
+
end
|
|
74
|
+
'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Gotchas
|
|
78
|
+
|
|
79
|
+
- Copying `open_ai.rb` as a template? Don't copy its trailing `ChatGPT = OpenAi` alias
|
|
80
|
+
line — the `:ChatGpt` registry entry does not match it.
|
|
81
|
+
- A `Hash`-typed param (like `request_params`) is fine — the wizard treats it as
|
|
82
|
+
raw-Ruby input automatically.
|
|
83
|
+
- If the service returns invalid XML on large documents (Minhon does), offer
|
|
84
|
+
`translate_by_section:` instead of trying to repair output.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Reference: a complete backend + unit spec
|
|
2
|
+
|
|
3
|
+
Modeled on `lib/natsukantou/open_ai.rb` and `spec/natsukantou/open_ai_spec.rb`.
|
|
4
|
+
Replace `Acme` / `acme` throughout.
|
|
5
|
+
|
|
6
|
+
## `lib/natsukantou/acme.rb`
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# frozen_string_literal: true
|
|
10
|
+
|
|
11
|
+
require 'acme-client-gem' # the API client added to natsukantou.gemspec
|
|
12
|
+
|
|
13
|
+
# Machine translator
|
|
14
|
+
#
|
|
15
|
+
# Acme Translate
|
|
16
|
+
# https://acme.example.com
|
|
17
|
+
module Natsukantou
|
|
18
|
+
class Acme
|
|
19
|
+
include UtilityBase
|
|
20
|
+
|
|
21
|
+
# @param app [Hash]
|
|
22
|
+
# @param api_key [String] API key for Acme Translate
|
|
23
|
+
# @param host [String] URL of API endpoint
|
|
24
|
+
# @param request_params [Hash] other optional request parameters
|
|
25
|
+
#
|
|
26
|
+
# @param translate_by_section [String] CSS selector to translate matched elements
|
|
27
|
+
# one by one, e.g. "body>p", for services that choke on large documents.
|
|
28
|
+
def initialize(
|
|
29
|
+
app, api_key:,
|
|
30
|
+
host: "https://api.acme.example.com", # String defaults MUST be double-quoted
|
|
31
|
+
request_params: {},
|
|
32
|
+
translate_by_section: nil # nil default => optional in the wizard
|
|
33
|
+
)
|
|
34
|
+
@app = app
|
|
35
|
+
@api_key = api_key
|
|
36
|
+
@host = host
|
|
37
|
+
@request_params = request_params
|
|
38
|
+
|
|
39
|
+
### Non request related setting
|
|
40
|
+
@translate_by_section = translate_by_section
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_reader :env
|
|
44
|
+
attr_reader :api_key, :host, :request_params
|
|
45
|
+
attr_reader :translate_by_section
|
|
46
|
+
|
|
47
|
+
def call(env)
|
|
48
|
+
@env = env
|
|
49
|
+
|
|
50
|
+
if translate_by_section
|
|
51
|
+
env[:dom].css(translate_by_section).each do |node|
|
|
52
|
+
next if node.text.empty?
|
|
53
|
+
|
|
54
|
+
translated_xml = translate(node.to_xml)
|
|
55
|
+
node.replace(dom_node(translated_xml)) if translated_xml
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
translated_xml = translate(env[:dom].to_xml)
|
|
59
|
+
env[:dom] = dom(translated_xml)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Required invariant — enforced by the 'lang attribute' shared example.
|
|
63
|
+
env[:dom].lang = env[:lang_to].code
|
|
64
|
+
|
|
65
|
+
@app.call(env)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Keep the network call isolated in one private method named `translate`,
|
|
71
|
+
# so the unit spec can stub it (the existing specs rely on this shape).
|
|
72
|
+
def translate(text)
|
|
73
|
+
# env.lang_from.code / env.lang_to.code are lowercase "xx" or "xx-yy";
|
|
74
|
+
# upcase or map here if the API needs it.
|
|
75
|
+
client.translate(
|
|
76
|
+
text,
|
|
77
|
+
from: env.lang_from.code,
|
|
78
|
+
to: env.lang_to.code,
|
|
79
|
+
**request_params,
|
|
80
|
+
)
|
|
81
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
82
|
+
logger.error(e.message)
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def client
|
|
87
|
+
@client ||= ::AcmeClient.new(api_key: api_key, host: host)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Registration line for `lib/natsukantou.rb` (under `# Translators`):
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
autoload_and_register :translator, :Acme, "natsukantou/acme"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## `spec/natsukantou/acme_spec.rb`
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# frozen_string_literal: true
|
|
103
|
+
|
|
104
|
+
require 'rspec'
|
|
105
|
+
|
|
106
|
+
RSpec.describe Natsukantou::Acme, :middleware do
|
|
107
|
+
let(:dom) { Oga.parse_xml('<foo>surface</foo>') }
|
|
108
|
+
let(:app) { ->(env) { env } }
|
|
109
|
+
|
|
110
|
+
subject do
|
|
111
|
+
described_class.new(
|
|
112
|
+
app,
|
|
113
|
+
api_key: 'api_key',
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
context 'lang attribute' do
|
|
118
|
+
before do
|
|
119
|
+
allow(subject).to receive(:translate).and_return(html)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
include_examples 'lang attribute'
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Notes on the spec machinery:
|
|
128
|
+
|
|
129
|
+
- `:middleware` metadata pulls in `spec/support/middleware_context.rb`
|
|
130
|
+
(default env `en → ja`, `expect_xml_to_eq` helper). The `let(:app)` /
|
|
131
|
+
`subject` above override its defaults where needed.
|
|
132
|
+
- `include_examples 'lang attribute'` comes from `spec/support/lang_attribute.rb`;
|
|
133
|
+
it defines `html` and asserts the `lang` attribute lands on the `<html>` node.
|
|
134
|
+
It requires the translation itself to be stubbed — hence the `allow(...)`.
|
|
135
|
+
- `RSpec.describe`, never bare `describe` — `disable_monkey_patching!` is on.
|
|
136
|
+
- No VCR/request spec is required for a new backend — the unit spec suffices; if you add
|
|
137
|
+
one, follow `.claude/skills/run-and-record-specs/SKILL.md`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-and-record-specs
|
|
3
|
+
description: Use when running the test suite, when a spec fails with VCR::Errors::UnhandledHTTPRequestError, when adding or re-recording a VCR cassette, or when specs complain about secrets. Covers VCR replay, re-recording, and the secret-handling rules.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Run the specs / work with VCR cassettes
|
|
7
|
+
|
|
8
|
+
## Running
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bundle exec rspec
|
|
12
|
+
bundle exec rspec <files touched>
|
|
13
|
+
bundle exec rake # spec + rubocop, same as GitLab CI
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If `VCR::Errors::UnhandledHTTPRequestError` is raised,
|
|
17
|
+
do not loosen VCR matchers globally or delete the cassette.
|
|
18
|
+
Re-record it (below), and only when the user asks.
|
|
19
|
+
|
|
20
|
+
Rubocop: run `bundle exec rubocop <files touched>` rather than chasing the full-repo output.
|
|
21
|
+
|
|
22
|
+
## Spec setup (spec/spec_helper.rb)
|
|
23
|
+
|
|
24
|
+
- Unit specs (`spec/natsukantou/`) stub all network — no secrets or cassettes needed.
|
|
25
|
+
- Request specs (`spec/requests/`) tag `:vcr`; the cassette path is derived from the
|
|
26
|
+
describe/context strings, e.g. `RSpec.describe 'DeepL'` + `it 'translates'` →
|
|
27
|
+
`spec/cassettes/DeepL/translates.yml`. Renaming an example orphans its cassette.
|
|
28
|
+
- Record mode is `:once`: cassette exists → replay only; missing → record (needs real
|
|
29
|
+
creds). webmock blocks all other real HTTP.
|
|
30
|
+
- Secrets: specs read the `SECRET` hash from `spec/secret.rb` (gitignored, real keys).
|
|
31
|
+
If absent, `spec/secret.example.rb` (dummy values) is loaded — replay still works
|
|
32
|
+
because requests are matched on method+URI, not credentials.
|
|
33
|
+
- Every value in `SECRET` is scrubbed from recorded cassettes via
|
|
34
|
+
`filter_sensitive_data` → placeholders like `<key>`, `<secret>`, `<url>`. Caveat:
|
|
35
|
+
`minhon` and `deepl` both use the key name `key`, so both values map to the same
|
|
36
|
+
`<key>` placeholder — fine for replay of existing specs, but rename the key in
|
|
37
|
+
`SECRET` if you add a section whose overlap would be ambiguous.
|
|
38
|
+
|
|
39
|
+
## Recording cassette
|
|
40
|
+
|
|
41
|
+
1. Ensure `spec/secret.rb` exists (based from `spec/secret.example.rb`) with real credentials. Ask the user for keys — never invent them).
|
|
42
|
+
2. When re-recording, delete only the target cassette
|
|
43
|
+
3. Run just that spec example, which performs a real, billable API call.
|
|
44
|
+
4. **Scrub check** — the real secret must not appear in the new cassette, e.g.:
|
|
45
|
+
```sh
|
|
46
|
+
grep -rF "$(ruby -e 'require_relative "spec/secret"; print SECRET.dig(:deepl, :key)')" spec/cassettes/ && echo LEAKED || echo clean
|
|
47
|
+
```
|
|
48
|
+
5. Re-run the spec to confirm green replay, then commit the cassette YAML only.
|
|
49
|
+
|
|
50
|
+
Never commit or print `spec/secret.rb`; never hardcode credentials in specs — always go through `SECRET.dig(...)`.
|
|
51
|
+
|
|
52
|
+
## Verify
|
|
53
|
+
|
|
54
|
+
`bundle exec rspec` → nothing failing that was not already failing before you started.
|
|
55
|
+
A new failure is your change.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wizard-config-conventions
|
|
3
|
+
description: Use before editing the `initialize` method or its doc comment on any class under lib/natsukantou/ (translators/middlewares), and when authoring or editing a translator_config.rb. The setup wizard YARD-parses these doc comments at runtime — a wrong quote style or missing @param silently breaks the wizard and no test catches it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Wizard schema rules (YARD doc comments are executable)
|
|
7
|
+
|
|
8
|
+
`Setup::ConfigPrompt` (`lib/natsukantou/setup/config_prompt.rb`) builds the interactive
|
|
9
|
+
setup wizard by YARD-parsing each registered component's **source file** and reading the
|
|
10
|
+
`@param` tags on `initialize`. The file path comes from `autoload_and_register` in
|
|
11
|
+
`lib/natsukantou.rb`. Consequences when touching any component's `initialize`:
|
|
12
|
+
|
|
13
|
+
- Every keyword argument except `app` needs a `@param name [Type] description` tag.
|
|
14
|
+
The description becomes the on-screen prompt text ("Enter 'auth_key', <description>").
|
|
15
|
+
- **String defaults must be double-quoted in the method signature**, e.g.
|
|
16
|
+
`host: "https://..."`. The wizard reads the raw source and `JSON.parse`s it
|
|
17
|
+
(`config_prompt.rb` `default_value_in_tty`) — a single-quoted default raises at
|
|
18
|
+
wizard runtime. RuboCop won't flag it (`Style/StringLiterals` is disabled).
|
|
19
|
+
- Default handling: no default → required answer; default `nil` → optional/skippable;
|
|
20
|
+
any other default → pre-filled, and unchanged answers are omitted from the generated
|
|
21
|
+
config (the code default applies).
|
|
22
|
+
- Types drive input handling: `Integer`/`Float` → converted; `Pathname` → file path
|
|
23
|
+
with existence validation; `Hash` → entered as a raw Ruby expression. To force
|
|
24
|
+
raw-Ruby entry for other params, list them in a `@fields_to_input_ruby name1 name2`
|
|
25
|
+
tag on `initialize` (custom tag defined in `config_prompt.rb`; `substitude_glossary.rb`
|
|
26
|
+
uses it). Raw-Ruby params are hidden from users who answer no to "Do you know Ruby?".
|
|
27
|
+
- Renaming a class or file requires updating its `autoload_and_register` line, or the
|
|
28
|
+
wizard crashes (`Component#klass` is nil).
|
|
29
|
+
|
|
30
|
+
## translator_config.rb shape
|
|
31
|
+
|
|
32
|
+
Generated by `lib/natsukantou/setup/output.erb`; loaded by
|
|
33
|
+
`Setup::ConfigLoadOrPrompt` via plain Ruby `load`, then checked with
|
|
34
|
+
`defined?(NatsukantouTranslator)`. Real example: `spec/fixtures/translator_config.rb`.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
::NatsukantouTranslator = Middleware::Builder.new do
|
|
38
|
+
use Natsukantou::HandleRubyMarkup
|
|
39
|
+
use Natsukantou::SubstitudeGlossary, filepath: "glossary.tsv" # filters first
|
|
40
|
+
|
|
41
|
+
use Natsukantou::DeepL, auth_key: "..." # translator LAST
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Rules: assign to the top-level constant `::NatsukantouTranslator`; middleware filters
|
|
46
|
+
before the translator; kwargs only. Used as `natsukantou -c CONFIG_FILE XML_FILE`. The
|
|
47
|
+
file is gitignored at repo root (it contains API keys) — never commit one outside
|
|
48
|
+
`spec/fixtures/`, and never put real keys in the fixture.
|
|
49
|
+
|
|
50
|
+
## Verify
|
|
51
|
+
|
|
52
|
+
After editing any `initialize` signature/doc comment, confirm the wizard can still parse
|
|
53
|
+
it (prints each kwarg + default, no exception) — substitute your class and path:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
bundle exec ruby -Ilib -e '
|
|
57
|
+
require "natsukantou"
|
|
58
|
+
require "natsukantou/setup/config_prompt"
|
|
59
|
+
comp = Natsukantou::Setup::Component.new(:DeepL, "lib/natsukantou/deep_l.rb")
|
|
60
|
+
comp.initialize_method_param_tags.each do |t|
|
|
61
|
+
d = t.default_value_in_string
|
|
62
|
+
p [t.name, (d && d != "nil") ? t.default_value_in_tty : d]
|
|
63
|
+
end
|
|
64
|
+
'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A single-quoted string default fails this with "Please specify keyword argument default
|
|
68
|
+
value in double quotes"; a missing registration or renamed class fails with
|
|
69
|
+
`NoMethodError` on nil.
|
data/.rubocop.yml
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
inherit_mode:
|
|
2
|
+
merge:
|
|
3
|
+
- Exclude
|
|
4
|
+
|
|
1
5
|
AllCops:
|
|
2
6
|
TargetRubyVersion: 2.7
|
|
3
7
|
Exclude:
|
|
@@ -60,11 +64,14 @@ Style/IfUnlessModifier:
|
|
|
60
64
|
Style/Documentation:
|
|
61
65
|
Enabled: false
|
|
62
66
|
|
|
67
|
+
Style/KeywordParametersOrder:
|
|
68
|
+
Enabled: false
|
|
69
|
+
|
|
63
70
|
Layout/ArgumentAlignment:
|
|
64
71
|
EnforcedStyle: with_fixed_indentation
|
|
65
72
|
|
|
66
73
|
Layout/LineLength:
|
|
67
|
-
|
|
74
|
+
Enabled: false
|
|
68
75
|
|
|
69
76
|
Layout/MultilineMethodCallIndentation:
|
|
70
77
|
EnforcedStyle: indented
|
data/CHANGELOG.md
CHANGED
|
@@ -29,3 +29,14 @@
|
|
|
29
29
|
## [0.2.2] - 2023-10-10
|
|
30
30
|
|
|
31
31
|
- Fix ChatGPT not able to distinguish content to be translated and our instructions.
|
|
32
|
+
|
|
33
|
+
## [0.2.3] - 2026-09-08
|
|
34
|
+
|
|
35
|
+
- Upgrade to deepl-rb 3.0 (maintainership change only, no migration needed)
|
|
36
|
+
- Switch to ibsciss-middleware fork, fixing Ruby 3 keyword-argument errors
|
|
37
|
+
- ChatGPT: configurable `model` and `temperature` (defaults `gpt-3.5-turbo`, `0.5`)
|
|
38
|
+
- ChatGPT: report API and timeout errors and leave the node untranslated instead of aborting the run
|
|
39
|
+
- ChatGPT: raise request timeout to 500s for slow single-paragraph translations
|
|
40
|
+
- Accept extended language subtags such as `zh-hant`
|
|
41
|
+
- Setup wizard: validate language codes, verify config and glossary files exist,
|
|
42
|
+
support parameters with multiple declared types, and cast Integer/Float inputs correctly
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Natsukantou — development guide
|
|
2
|
+
|
|
3
|
+
Ruby gem that translates XML/EPUB documents between human languages via pluggable
|
|
4
|
+
backends (DeepL, TexTra/Minhon, OpenAI) and middleware filters.
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
- **Everything is a Rack-style middleware.** There is no Translator base class. A
|
|
9
|
+
component (backend or filter) is a class with `initialize(app, **keyword_args)` and
|
|
10
|
+
`call(env)` that ends with `@app.call(env)`. Backends mix in `UtilityBase`
|
|
11
|
+
(`lib/natsukantou/utility/utility_base.rb`) for `dom`/`dom_node`/`logger`.
|
|
12
|
+
- **`env`** is `Natsukantou::Env` (`lib/natsukantou/utility/env.rb`): a Hash delegator
|
|
13
|
+
requiring `:dom` (Oga document), `:lang_from`, `:lang_to` (coerced to `LanguageCode`).
|
|
14
|
+
- **Pipeline** = user config file: `::NatsukantouTranslator = Middleware::Builder.new do
|
|
15
|
+
use ... end` — filters first, translator last. `exe/natsukantou` parses the XML, runs
|
|
16
|
+
the stack, writes `file.<lang_to>.ext`.
|
|
17
|
+
- **Registration**: `lib/natsukantou.rb` `autoload_and_register :translator|:middleware,
|
|
18
|
+
:Constant, "path"`. This also records the source path so the setup wizard can
|
|
19
|
+
**YARD-parse the file at runtime** — doc comments on `initialize` are executable
|
|
20
|
+
schema, not documentation. See `.claude/skills/wizard-config-conventions/SKILL.md`.
|
|
21
|
+
- `lib/monkey_patch/oga_document.rb` adds `lang`/`lang=` to Oga elements/documents,
|
|
22
|
+
`Document#interlace` (bilingual output), and a UTF-8 serialization fix.
|
|
23
|
+
|
|
24
|
+
## Load-bearing conventions
|
|
25
|
+
|
|
26
|
+
- `SubstitudeGlossary` — the misspelling ("substitude") is intentional public API
|
|
27
|
+
(README, registry, config files). Never rename or "fix" it.
|
|
28
|
+
- A translator's `call` MUST set `env[:dom].lang = env[:lang_to].code` after
|
|
29
|
+
translating — enforced by the shared example in `spec/support/lang_attribute.rb`.
|
|
30
|
+
- Parse XML with `dom(xml)` / `dom_node(xml)` from `UtilityBase`, never
|
|
31
|
+
`Oga.parse_xml` directly (they do strict parse → lenient fallback with logging).
|
|
32
|
+
- `<skip>` tags mark already-translated content: `SubstitudeGlossary` emits them,
|
|
33
|
+
DeepL is configured with `ignore_tags: 'skip'` (`lib/natsukantou/deep_l.rb`).
|
|
34
|
+
- `LanguageCode#is?` is asymmetric: `en-gb` is `en`, but `en` is not `en-gb`.
|
|
35
|
+
- `lib/` must stay compatible with the oldest Ruby the gemspec's
|
|
36
|
+
`required_ruby_version` allows and `.gitlab-ci.yml` builds. Read both rather than
|
|
37
|
+
assuming; local dev runs a newer Ruby than either.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
- `bundle exec rspec` — run it before you change anything so you know the baseline; a
|
|
42
|
+
failure that was not there before is yours. Pre-existing failures are usually stale
|
|
43
|
+
VCR cassettes — see `.claude/skills/run-and-record-specs/SKILL.md`.
|
|
44
|
+
- `bundle exec rubocop <files-you-touched>` — the full run has pre-existing offenses;
|
|
45
|
+
only keep your own files clean.
|
|
46
|
+
- `bundle exec rake` — spec + rubocop; what GitLab CI runs (`.gitlab-ci.yml`).
|
|
47
|
+
- Release: bump `lib/natsukantou/version.rb`, append a CHANGELOG.md entry **at the
|
|
48
|
+
bottom** (this file is ordered oldest→newest), then `bundle exec rake release`
|
|
49
|
+
(git tag + irreversible push to rubygems.org — requires maintainer confirmation).
|
|
50
|
+
|
|
51
|
+
## Known issues — do not trip on these
|
|
52
|
+
|
|
53
|
+
This section records the state at the time of writing and goes stale fastest; confirm
|
|
54
|
+
an item still holds before relying on it.
|
|
55
|
+
|
|
56
|
+
- `Natsukantou::ChatGpt` does not resolve: `lib/natsukantou.rb` registers `:ChatGpt`
|
|
57
|
+
while `open_ai.rb` names the alias `ChatGPT`. Use `Natsukantou::OpenAi` instead.
|
|
58
|
+
- The DeepL cassette (`spec/cassettes/DeepL/translates.yml`) predates deepl-rb 3.x's
|
|
59
|
+
request format; re-recording requires a real API key.
|
|
60
|
+
- `spec/secret.rb` (gitignored) holds real credentials — never commit or print it.
|
|
61
|
+
|
|
62
|
+
## Skills
|
|
63
|
+
|
|
64
|
+
- `.claude/skills/add-translator-backend/` — add a new backend or middleware filter.
|
|
65
|
+
- `.claude/skills/run-and-record-specs/` — run the suite, handle VCR/secrets, re-record.
|
|
66
|
+
- `.claude/skills/wizard-config-conventions/` — edit `initialize` signatures/doc
|
|
67
|
+
comments or author a `translator_config.rb`.
|
data/exe/natsukantou
CHANGED
|
@@ -40,8 +40,17 @@ Natsukantou::Setup::ConfigLoadOrPrompt.new.execute(
|
|
|
40
40
|
config_path: options[:config_path],
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
def prompt.ask_lang(target)
|
|
44
|
+
ask("Language code to translate #{target}?") do |q|
|
|
45
|
+
q.validate(
|
|
46
|
+
/\A([[:alpha:]]{2}|[[:alpha:]]{2}-[[:alpha:]]*)\z/,
|
|
47
|
+
"Language code should be in the form of xx or xx-yy"
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
lang_from = prompt.ask_lang('from')
|
|
53
|
+
lang_to = prompt.ask_lang('to')
|
|
45
54
|
|
|
46
55
|
env = Natsukantou::Env.new(
|
|
47
56
|
dom: dom, lang_from: lang_from, lang_to: lang_to
|
data/lib/natsukantou/chat_gpt.rb
CHANGED
|
@@ -18,15 +18,21 @@ module Natsukantou
|
|
|
18
18
|
|
|
19
19
|
# @param app [Hash]
|
|
20
20
|
# @param access_token [String] API access token
|
|
21
|
+
# @param model [String] LLM model name
|
|
22
|
+
# @param temperature [Float] Between 0 and 1. Higher temperatures introduce randomness, which is beneficial for creative writing. In contrast, a temperature of zero ensures consistent responses.
|
|
21
23
|
#
|
|
22
24
|
# @param translate_by_section [String] CSS selector to translate matched elements one by one.
|
|
23
25
|
# Specify a css path (e.g. "body>p") to translate in smaller chunks.
|
|
24
26
|
def initialize(
|
|
25
27
|
app, access_token:,
|
|
28
|
+
model: "gpt-3.5-turbo",
|
|
29
|
+
temperature: 0.5,
|
|
26
30
|
translate_by_section:
|
|
27
31
|
)
|
|
28
32
|
@app = app
|
|
29
33
|
@access_token = access_token
|
|
34
|
+
@model = model
|
|
35
|
+
@temperature = temperature
|
|
30
36
|
|
|
31
37
|
### Non request related setting
|
|
32
38
|
@translate_by_section = translate_by_section
|
|
@@ -34,6 +40,8 @@ module Natsukantou
|
|
|
34
40
|
|
|
35
41
|
attr_reader :env
|
|
36
42
|
attr_reader :access_token
|
|
43
|
+
attr_reader :model
|
|
44
|
+
attr_reader :temperature
|
|
37
45
|
attr_reader :translate_by_section
|
|
38
46
|
|
|
39
47
|
def call(env)
|
|
@@ -44,7 +52,11 @@ module Natsukantou
|
|
|
44
52
|
next if node.text.empty?
|
|
45
53
|
|
|
46
54
|
translated_xml = translate(node.to_xml)
|
|
47
|
-
|
|
55
|
+
next unless translated_xml
|
|
56
|
+
|
|
57
|
+
new_node = dom_node(translated_xml)
|
|
58
|
+
new_node.lang = env[:lang_to].code
|
|
59
|
+
node.replace(new_node)
|
|
48
60
|
end
|
|
49
61
|
else
|
|
50
62
|
# TODO
|
|
@@ -63,28 +75,36 @@ module Natsukantou
|
|
|
63
75
|
)
|
|
64
76
|
|
|
65
77
|
parameters = {
|
|
66
|
-
model:
|
|
78
|
+
model: model,
|
|
67
79
|
messages: [
|
|
68
80
|
{ role: "system", content: system_message },
|
|
69
81
|
{ role: "user", content: text },
|
|
70
82
|
],
|
|
71
|
-
temperature:
|
|
83
|
+
temperature: temperature,
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
response = client.chat(parameters: parameters)
|
|
75
87
|
|
|
88
|
+
raise response['error'].to_s if response.key?('error')
|
|
89
|
+
|
|
76
90
|
translated_xml = response.dig("choices", 0, "message", "content")
|
|
77
91
|
|
|
78
92
|
translated_xml.strip!
|
|
79
93
|
|
|
80
94
|
translated_xml
|
|
81
|
-
rescue Net::OpenTimeout
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
96
|
+
puts e.message
|
|
97
|
+
nil
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
puts e.message
|
|
100
|
+
nil
|
|
84
101
|
end
|
|
85
102
|
|
|
86
103
|
def client
|
|
87
|
-
@client ||= ::OpenAI::Client.new(
|
|
104
|
+
@client ||= ::OpenAI::Client.new(
|
|
105
|
+
access_token: access_token,
|
|
106
|
+
request_timeout: 500
|
|
107
|
+
)
|
|
88
108
|
end
|
|
89
109
|
end
|
|
90
110
|
end
|
|
@@ -18,7 +18,7 @@ module Natsukantou
|
|
|
18
18
|
puts "Saved as translator_config.rb, which you can specify with -c flag next time.\n\n"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
Kernel.eval(config_content)
|
|
21
|
+
Kernel.eval(config_content) # rubocop:disable Security/Eval -- safe code generated by us
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
NatsukantouTranslator
|
|
@@ -38,7 +38,7 @@ module Natsukantou
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Both a decorator to Yard tag object,
|
|
41
|
+
# Both a decorator to Yard @param tag object,
|
|
42
42
|
# and where the user input is stored.
|
|
43
43
|
# Assumption: only dealing with keyword argument
|
|
44
44
|
class MethodParamTagDecorator < SimpleDelegator
|
|
@@ -49,7 +49,7 @@ module Natsukantou
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def enter_ruby?
|
|
52
|
-
return true if
|
|
52
|
+
return true if primary_type?('Hash')
|
|
53
53
|
|
|
54
54
|
list = initialize_method.tag('fields_to_input_ruby')
|
|
55
55
|
|
|
@@ -59,23 +59,28 @@ module Natsukantou
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def convert
|
|
62
|
-
|
|
63
|
-
when Integer
|
|
62
|
+
if primary_type? 'Integer'
|
|
64
63
|
:integer
|
|
65
|
-
|
|
64
|
+
elsif primary_type? 'Float'
|
|
66
65
|
:float
|
|
67
66
|
else
|
|
68
67
|
nil # do not convert and assume Ruby code
|
|
69
68
|
end
|
|
70
69
|
end
|
|
71
70
|
|
|
71
|
+
# @return [String] keyword argument default value **in Ruby syntax** as is.
|
|
72
|
+
# This means for string args, the quote characters (' or ") will also be included.
|
|
72
73
|
def default_value_in_string
|
|
73
74
|
initialize_method.parameters.find { |p| p.first == "#{name}:" }&.last
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
# tty-prompt's `value` field requires String
|
|
77
78
|
def default_value_in_tty
|
|
78
|
-
if
|
|
79
|
+
if primary_type? 'String'
|
|
80
|
+
if !default_value_in_string.start_with?('"')
|
|
81
|
+
raise "Please specify keyword argument default value in double quotes for the wizard parsing to work."
|
|
82
|
+
end
|
|
83
|
+
|
|
79
84
|
JSON.parse(default_value_in_string)
|
|
80
85
|
else
|
|
81
86
|
default_value_in_string
|
|
@@ -89,6 +94,12 @@ module Natsukantou
|
|
|
89
94
|
user_input.inspect
|
|
90
95
|
end
|
|
91
96
|
end
|
|
97
|
+
|
|
98
|
+
# @param type_string [String] whether @param type has type_string as primary type
|
|
99
|
+
# TODO: use internal YARD parser to obtain list of types
|
|
100
|
+
def primary_type?(type_string)
|
|
101
|
+
type.start_with?(type_string)
|
|
102
|
+
end
|
|
92
103
|
end
|
|
93
104
|
|
|
94
105
|
class ConfigPrompt
|
|
@@ -167,6 +178,7 @@ module Natsukantou
|
|
|
167
178
|
printf "Enter '#{tag.name}', #{tag.text}"
|
|
168
179
|
|
|
169
180
|
ask_params[:convert] = tag.convert if tag.convert
|
|
181
|
+
set_file_path!(tag, ask_params)
|
|
170
182
|
|
|
171
183
|
default = tag.default_value_in_string
|
|
172
184
|
if default == 'nil'
|
|
@@ -199,6 +211,22 @@ module Natsukantou
|
|
|
199
211
|
|
|
200
212
|
answers
|
|
201
213
|
end
|
|
214
|
+
|
|
215
|
+
# @param params [Hash] ask params, this will be modified
|
|
216
|
+
def set_file_path!(tag, params)
|
|
217
|
+
return if !tag.primary_type?('Pathname')
|
|
218
|
+
|
|
219
|
+
params[:convert] = :filepath
|
|
220
|
+
params[:validate] = proc do |input|
|
|
221
|
+
if File.exist?(input)
|
|
222
|
+
true
|
|
223
|
+
else
|
|
224
|
+
# TODO: https://github.com/piotrmurach/tty-prompt/issues/197
|
|
225
|
+
puts 'File does not exist, please input again: '
|
|
226
|
+
false
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
202
230
|
end
|
|
203
231
|
end
|
|
204
232
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tsv'
|
|
4
|
+
require 'pathname'
|
|
4
5
|
|
|
5
6
|
# Glossary isn't supported in some language
|
|
6
7
|
# combinations (e.g. DeepL ja to zh).
|
|
@@ -16,14 +17,16 @@ module Natsukantou
|
|
|
16
17
|
ja zh th lo
|
|
17
18
|
}.freeze
|
|
18
19
|
|
|
19
|
-
# @param filepath [String] path to TSV glossary file.
|
|
20
|
+
# @param filepath [Pathname, String] path to TSV glossary file.
|
|
20
21
|
# @param glossary [Array(Array(String, String))] array representing glossary, e.g. [['book', '本']]
|
|
21
22
|
# @fields_to_input_ruby glossary
|
|
22
23
|
def initialize(app, filepath: nil, glossary: [])
|
|
23
24
|
@app = app
|
|
24
25
|
|
|
25
26
|
@glossary = glossary
|
|
26
|
-
|
|
27
|
+
if filepath
|
|
28
|
+
@glossary.concat(parse_tsv(Pathname(filepath)))
|
|
29
|
+
end
|
|
27
30
|
|
|
28
31
|
@glossary.uniq! { |row| row[0] }
|
|
29
32
|
|
data/lib/natsukantou/version.rb
CHANGED
data/lib/natsukantou.rb
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'logger'
|
|
4
4
|
require 'middleware'
|
|
5
|
-
require_relative "monkey_patch/builder"
|
|
6
5
|
require 'oga'
|
|
7
6
|
require_relative "monkey_patch/oga_document"
|
|
8
|
-
require_relative "monkey_patch/runner"
|
|
9
7
|
|
|
10
8
|
require_relative "natsukantou/version"
|
|
11
9
|
require_relative "natsukantou/setup/registry"
|
data/natsukantou.gemspec
CHANGED
|
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
30
|
spec.require_paths = ["lib"]
|
|
31
31
|
|
|
32
|
-
spec.add_dependency "middleware", "~> 0.
|
|
32
|
+
spec.add_dependency "ibsciss-middleware", "~> 0.4.3"
|
|
33
33
|
spec.add_dependency "oga", "~> 3.4"
|
|
34
34
|
spec.add_dependency "tsv", "~> 1.0"
|
|
35
35
|
|
|
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
|
|
|
37
37
|
spec.add_dependency "yard", "~> 0.9"
|
|
38
38
|
|
|
39
39
|
# DeepL
|
|
40
|
-
spec.add_dependency "deepl-rb", "~>
|
|
40
|
+
spec.add_dependency "deepl-rb", "~> 3.0"
|
|
41
41
|
|
|
42
42
|
# Minhon
|
|
43
43
|
spec.add_dependency 'oauth', '~> 0.5.1'
|
metadata
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: natsukantou
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- lulalala
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: middleware
|
|
13
|
+
name: ibsciss-middleware
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.
|
|
18
|
+
version: 0.4.3
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.
|
|
25
|
+
version: 0.4.3
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: oga
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -86,14 +85,14 @@ dependencies:
|
|
|
86
85
|
requirements:
|
|
87
86
|
- - "~>"
|
|
88
87
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
88
|
+
version: '3.0'
|
|
90
89
|
type: :runtime
|
|
91
90
|
prerelease: false
|
|
92
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
92
|
requirements:
|
|
94
93
|
- - "~>"
|
|
95
94
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
95
|
+
version: '3.0'
|
|
97
96
|
- !ruby/object:Gem::Dependency
|
|
98
97
|
name: oauth
|
|
99
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -131,17 +130,20 @@ executables:
|
|
|
131
130
|
extensions: []
|
|
132
131
|
extra_rdoc_files: []
|
|
133
132
|
files:
|
|
133
|
+
- ".claude/skills/add-translator-backend/SKILL.md"
|
|
134
|
+
- ".claude/skills/add-translator-backend/example-backend.md"
|
|
135
|
+
- ".claude/skills/run-and-record-specs/SKILL.md"
|
|
136
|
+
- ".claude/skills/wizard-config-conventions/SKILL.md"
|
|
134
137
|
- ".rspec"
|
|
135
138
|
- ".rubocop.yml"
|
|
136
139
|
- CHANGELOG.md
|
|
140
|
+
- CLAUDE.md
|
|
137
141
|
- Gemfile
|
|
138
142
|
- LICENSE.txt
|
|
139
143
|
- README.md
|
|
140
144
|
- Rakefile
|
|
141
145
|
- exe/natsukantou
|
|
142
|
-
- lib/monkey_patch/builder.rb
|
|
143
146
|
- lib/monkey_patch/oga_document.rb
|
|
144
|
-
- lib/monkey_patch/runner.rb
|
|
145
147
|
- lib/natsukantou.rb
|
|
146
148
|
- lib/natsukantou/chat_gpt.rb
|
|
147
149
|
- lib/natsukantou/deep_l.rb
|
|
@@ -168,7 +170,6 @@ metadata:
|
|
|
168
170
|
homepage_uri: https://gitlab.com/lulalala/natsukantou
|
|
169
171
|
source_code_uri: https://gitlab.com/lulalala/natsukantou
|
|
170
172
|
changelog_uri: https://gitlab.com/lulalala/natsukantou/-/releases
|
|
171
|
-
post_install_message:
|
|
172
173
|
rdoc_options: []
|
|
173
174
|
require_paths:
|
|
174
175
|
- lib
|
|
@@ -183,8 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
183
184
|
- !ruby/object:Gem::Version
|
|
184
185
|
version: '0'
|
|
185
186
|
requirements: []
|
|
186
|
-
rubygems_version: 3.
|
|
187
|
-
signing_key:
|
|
187
|
+
rubygems_version: 3.6.9
|
|
188
188
|
specification_version: 4
|
|
189
189
|
summary: human language translation library for XML documents
|
|
190
190
|
test_files: []
|
data/lib/monkey_patch/builder.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Middleware
|
|
2
|
-
class Builder
|
|
3
|
-
def use(middleware, *args, **kwargs, &block)
|
|
4
|
-
if middleware.kind_of?(Builder)
|
|
5
|
-
# Merge in the other builder's stack into our own
|
|
6
|
-
self.stack.concat(middleware.stack)
|
|
7
|
-
else
|
|
8
|
-
self.stack << [middleware, args, kwargs, block]
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
self
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
data/lib/monkey_patch/runner.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Middleware
|
|
2
|
-
class Runner
|
|
3
|
-
protected
|
|
4
|
-
|
|
5
|
-
def build_call_chain(stack)
|
|
6
|
-
stack.reverse.inject(EMPTY_MIDDLEWARE) do |next_middleware, current_middleware|
|
|
7
|
-
# Unpack the actual item
|
|
8
|
-
klass, args, kwargs, block = current_middleware
|
|
9
|
-
|
|
10
|
-
# Default the arguments to an empty array. Otherwise in Ruby 1.8
|
|
11
|
-
# a `nil` args will actually pass `nil` into the class. Not what
|
|
12
|
-
# we want!
|
|
13
|
-
args ||= []
|
|
14
|
-
|
|
15
|
-
if klass.is_a?(Class)
|
|
16
|
-
# If the klass actually is a class, then instantiate it with
|
|
17
|
-
# the app and any other arguments given.
|
|
18
|
-
klass.new(next_middleware, *args, **kwargs, &block)
|
|
19
|
-
elsif klass.respond_to?(:call)
|
|
20
|
-
# Make it a lambda which calls the item then forwards up
|
|
21
|
-
# the chain.
|
|
22
|
-
lambda do |env|
|
|
23
|
-
klass.call(env)
|
|
24
|
-
next_middleware.call(env)
|
|
25
|
-
end
|
|
26
|
-
else
|
|
27
|
-
raise "Invalid middleware, doesn't respond to `call`: #{klass.inspect}"
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|