sghtmltopdf 0.4.0 → 0.5.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.
@@ -1,30 +1,30 @@
1
- //! GVL(Global VM Lock)の解放。
1
+ //! Releasing the GVL (Global VM Lock).
2
2
  //!
3
- //! レンダリングの間はGVLを解放し、Pumaの他スレッドを止めないようにする。
3
+ //! The GVL is released during rendering so Puma's other threads are not blocked.
4
4
 
5
5
  use std::ffi::c_void;
6
6
  use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
7
7
 
8
- /// GVLを解放して`func`を実行する。
8
+ /// Release the GVL and run `func`.
9
9
  ///
10
- /// # なぜ`Send`境界が要るか
10
+ /// # Why the `Send` bound is needed
11
11
  ///
12
- /// magnusは「GVLを解放するAPIは存在しない」前提でGVL状態をスレッド
13
- /// ローカルにキャッシュしており(`magnus::api`の *assumed not to change
14
- /// because there's currently no api to unlock*)、ここで解放しても
15
- /// `Ruby::get()`は`Ok`を返してしまう。返ったハンドルでRubyに触ればUBになる。
12
+ /// magnus caches the GVL state in a thread local on the assumption that "there is no API
13
+ /// that releases the GVL" (*assumed not to change because there's currently no api to
14
+ /// unlock* in `magnus::api`), so `Ruby::get()` returns `Ok` even after we release it here.
15
+ /// Touching Ruby through the handle it returns would be UB.
16
16
  ///
17
- /// `Send`境界を課すと、magnusの値(`NonNull<RBasic>`)も`Ruby`ハンドル
18
- /// (`*mut ()`)も`!Send`なのでキャプチャがコンパイルエラーになる。
19
- /// クロージャの中で改めて`Ruby::get()`を呼ぶことまでは型では防げないが、
20
- /// 解放区間で呼ぶのは`sghtmltopdf_core`の関数だけであり、コアはRubyを
21
- /// 一切知らないため到達しない。
17
+ /// Imposing a `Send` bound makes capturing a magnus value (`NonNull<RBasic>`) or a `Ruby`
18
+ /// handle (`*mut ()`) a compile error, both being `!Send`.
19
+ /// Calling `Ruby::get()` again inside the closure is not something the types can prevent,
20
+ /// but the only things called in the released region are `sghtmltopdf_core` functions, and
21
+ /// the core knows nothing about Ruby, so it is unreachable.
22
22
  ///
23
- /// # 割り込み
23
+ /// # Interruption
24
24
  ///
25
- /// UBF(unblock function)は`None`=割り込み不可。`Kernel#trap`やCtrl-Cでの
26
- /// 中断は初期スコープ外とする。
27
- #[allow(dead_code)] // 将来のGVL解放実装で使う
25
+ /// The UBF (unblock function) is `None`, meaning uninterruptible. Interruption through
26
+ /// `Kernel#trap` or Ctrl-C is outside the initial scope.
27
+ #[allow(dead_code)] // used by the future GVL-releasing implementation
28
28
  pub fn without_gvl<F, R>(func: F) -> R
29
29
  where
30
30
  F: FnOnce() -> R + Send,
@@ -41,9 +41,9 @@ where
41
41
  R: Send,
42
42
  {
43
43
  let state = unsafe { &mut *(arg as *mut State<F, R>) };
44
- let func = state.func.take().expect("コールバックが2度呼ばれました");
45
- // パニックがFFI境界を越えるとプロセスがabortするため、ここで捕まえて
46
- // GVLを取り戻してからRust側でresumeする。
44
+ let func = state.func.take().expect("the callback was called twice");
45
+ // A panic crossing the FFI boundary aborts the process, so it is caught here and
46
+ // resumed on the Rust side after the GVL is reacquired.
47
47
  state.result = Some(catch_unwind(AssertUnwindSafe(func)));
48
48
  std::ptr::null_mut()
49
49
  }
@@ -60,26 +60,26 @@ where
60
60
  std::ptr::null_mut(),
61
61
  );
62
62
  }
63
- match state.result.expect("コールバックが実行されませんでした") {
63
+ match state.result.expect("the callback was never run") {
64
64
  Ok(value) => value,
65
65
  Err(panic) => resume_unwind(panic),
66
66
  }
67
67
  }
68
68
 
69
- /// GVLを取り戻して`func`を実行する。[`without_gvl`]の内側からだけ呼ぶ。
69
+ /// Reacquire the GVL and run `func`. Call only from inside [`without_gvl`].
70
70
  ///
71
- /// `without_gvl`と違い`Send`境界は課さない。ここはGVLを保持している=Rubyに
72
- /// 触ってよい区間だから。
71
+ /// Unlike `without_gvl` it imposes no `Send` bound, this being a region where the GVL is
72
+ /// held and Ruby may be touched.
73
73
  ///
74
- /// # 呼び出し側が守ること(libruby側の制約)
74
+ /// # What the caller must observe (libruby's constraints)
75
75
  ///
76
- /// * `func`からRubyのオブジェクトを返さない。返すとGVLを再び手放した
77
- /// あとGCのスコープから外れ、マークされない。値を持ち帰るときは
78
- /// `rb_gc_register_address`で登録したスロットへ入れること
76
+ /// * Do not return a Ruby object from `func`. Returning one puts it outside the GC's scope
77
+ /// once the GVL is released again, and it will not be marked. To carry a value back, put
78
+ /// it in a slot registered with `rb_gc_register_address`
79
79
  /// (`callback_sink::ValueSlot`)
80
- /// * `func`から例外を投げさせない。longjmpがこの関数を飛び越えると
81
- /// 未定義動作になる。Rubyの呼び出しは必ず`rb_protect`相当で包む
82
- /// (magnusの`Proc::call`は内部で`protect`しているのでそのまま使える)
80
+ /// * Do not let `func` throw an exception. A longjmp jumping over this function is undefined
81
+ /// behaviour. Every Ruby call must be wrapped in the equivalent of `rb_protect`
82
+ /// (magnus's `Proc::call` uses `protect` internally, so it can be used directly)
83
83
  pub fn with_gvl<F, R>(func: F) -> R
84
84
  where
85
85
  F: FnOnce() -> R,
@@ -94,8 +94,8 @@ where
94
94
  F: FnOnce() -> R,
95
95
  {
96
96
  let state = unsafe { &mut *(arg as *mut State<F, R>) };
97
- let func = state.func.take().expect("コールバックが2度呼ばれました");
98
- // パニックがlibrubyのフレームを越えるとプロセスがabortする。
97
+ let func = state.func.take().expect("the callback was called twice");
98
+ // A panic crossing a libruby frame aborts the process.
99
99
  state.result = Some(catch_unwind(AssertUnwindSafe(func)));
100
100
  std::ptr::null_mut()
101
101
  }
@@ -107,7 +107,7 @@ where
107
107
  unsafe {
108
108
  rb_sys::rb_thread_call_with_gvl(Some(call::<F, R>), &mut state as *mut _ as *mut c_void);
109
109
  }
110
- match state.result.expect("コールバックが実行されませんでした") {
110
+ match state.result.expect("the callback was never run") {
111
111
  Ok(value) => value,
112
112
  Err(panic) => resume_unwind(panic),
113
113
  }
@@ -1,8 +1,8 @@
1
- //! Ruby拡張のエントリポイント。
1
+ //! The Ruby extension's entry point.
2
2
  //!
3
- //! この層は薄く保つ。オプションの引数列(argv)への組み立てはRuby側が
4
- //! 行い、ここは受け取ったargvをCLI・HTTPサーバと同じパーサへ通して
5
- //! レンダリングするだけ。
3
+ //! This layer is kept thin. Assembling the option argument list (argv) is done on the Ruby
4
+ //! side, and this merely runs the argv it receives through the same parser as the CLI and
5
+ //! the HTTP server, and renders.
6
6
 
7
7
  mod callback_sink;
8
8
  mod errors;
@@ -13,81 +13,75 @@ use std::path::PathBuf;
13
13
 
14
14
  use magnus::rb_sys::AsRawValue;
15
15
  use magnus::{block::Proc, function, prelude::*, Error, RString, Ruby};
16
- use sghtmltopdf_core::cli::{self, convert};
17
- use sghtmltopdf_core::render_stack;
18
- use sghtmltopdf_core::sink::{FileSink, MemorySink};
16
+ use sghtmltopdf_core::{with_render_stack, ConvertError, Converter, FileSink};
19
17
 
20
18
  use callback_sink::{pump_to_block, BlockSlot, PendingUnwind, ValueSlot};
21
19
 
22
- /// HTMLを変換してPDFのバイト列を返す。
20
+ /// Convert HTML and return the PDF bytes.
23
21
  fn render(html: RString, argv: Vec<String>) -> Result<RString, Error> {
24
- let ruby = Ruby::get().expect("GVLを保持したまま呼ばれるはず");
25
- // GVLを解放する前にRust側へコピーする。解放中はRubyのオブジェクトに
26
- // 触れないため、`RString`のままでは持ち込めない。
22
+ let ruby = Ruby::get().expect("it should be called while holding the GVL");
23
+ // Copy to the Rust side before releasing the GVL. Ruby objects cannot be touched while
24
+ // it is released, so an `RString` cannot be carried in.
27
25
  let html = unsafe { html.as_slice() }.to_vec();
28
26
  errors::catch_panic(&ruby, move || render_inner(html, argv))
29
27
  }
30
28
 
31
29
  fn render_inner(html: Vec<u8>, argv: Vec<String>) -> Result<RString, Error> {
32
- let ruby = Ruby::get().expect("GVLを保持したまま呼ばれるはず");
33
- let (args, fonts) = cli::parse_convert_argv(&argv).map_err(|e| errors::to_ruby(&ruby, e))?;
30
+ let ruby = Ruby::get().expect("it should be called while holding the GVL");
31
+ let converter = Converter::from_args(argv).map_err(|e| errors::to_ruby(&ruby, e))?;
34
32
 
35
- // GVLを解放したうえで、さらにレンダリング専用のスタックを確保した
36
- // スレッドへ移す。Rubyのスレッドのマシンスタックは既定1MiBしかなく、
37
- // レイアウト・描画の再帰に耐えられないため(`callback_sink`のモジュール
38
- // doc参照)。この経路はRubyへコールバックしないので、そのまま移せる。
33
+ // Release the GVL and then move onto a thread with a stack allocated specifically for
34
+ // rendering. A Ruby thread's machine stack is only 1MiB by default and cannot survive the
35
+ // recursion of layout and drawing (see the module docs of `callback_sink`).
36
+ // This path never calls back into Ruby, so it can be moved as-is.
39
37
  let pdf = gvl::without_gvl(move || {
40
- render_stack::with_render_stack(move || {
41
- convert::render_to_memory(&args, &fonts, Cursor::new(html), MemorySink::new())
42
- })
38
+ with_render_stack(move || converter.render_to_vec(Cursor::new(html)))
43
39
  })
44
40
  .map_err(|e| errors::to_ruby(&ruby, e))?;
45
41
 
46
42
  Ok(ruby.str_from_slice(&pdf))
47
43
  }
48
44
 
49
- /// HTMLを変換して`path`へ書き出す。
45
+ /// Convert HTML and write it to `path`.
50
46
  ///
51
- /// 出力先は[`FileSink`]が決めるので、argvの`--output`は使われない
52
- /// (一時ファイルへ書いて成功時だけrenameするため、途中で失敗しても
53
- /// 壊れたPDFが残らない)。
47
+ /// The destination is decided by [`FileSink`]
48
+ /// (it writes to a temporary file and renames only on success, so a failure part-way through
49
+ /// leaves no broken PDF).
54
50
  fn render_to_file(html: RString, argv: Vec<String>, path: String) -> Result<(), Error> {
55
- let ruby = Ruby::get().expect("GVLを保持したまま呼ばれるはず");
51
+ let ruby = Ruby::get().expect("it should be called while holding the GVL");
56
52
  let html = unsafe { html.as_slice() }.to_vec();
57
53
  errors::catch_panic(&ruby, move || render_to_file_inner(html, argv, path))
58
54
  }
59
55
 
60
56
  fn render_to_file_inner(html: Vec<u8>, argv: Vec<String>, path: String) -> Result<(), Error> {
61
- let ruby = Ruby::get().expect("GVLを保持したまま呼ばれるはず");
62
- let (args, fonts) = cli::parse_convert_argv(&argv).map_err(|e| errors::to_ruby(&ruby, e))?;
57
+ let ruby = Ruby::get().expect("it should be called while holding the GVL");
58
+ let converter = Converter::from_args(argv).map_err(|e| errors::to_ruby(&ruby, e))?;
63
59
 
64
60
  let path = PathBuf::from(path);
65
61
  let sink = FileSink::create(&path).map_err(|e| {
66
62
  errors::to_ruby(
67
63
  &ruby,
68
- cli::CliError::Input(format!("{}の作成に失敗しました: {e}", path.display())),
64
+ ConvertError::Input(format!("failed to create {}: {e}", path.display())),
69
65
  )
70
66
  })?;
71
67
 
72
- gvl::without_gvl(move || {
73
- render_stack::with_render_stack(move || convert::render(&args, &fonts, Cursor::new(html), sink))
74
- })
75
- .map_err(|e| errors::to_ruby(&ruby, e))?;
68
+ gvl::without_gvl(move || with_render_stack(move || converter.render(Cursor::new(html), sink)))
69
+ .map_err(|e| errors::to_ruby(&ruby, e))?;
76
70
  Ok(())
77
71
  }
78
72
 
79
- /// HTMLを変換し、確定したPDFのバイト列を`chunk_size`ごとに`block`へ渡す。
73
+ /// Convert HTML and hand the settled PDF bytes to `block` in `chunk_size` pieces.
80
74
  ///
81
- /// レンダリングの間はGVLを解放し、ブロックを呼ぶ瞬間だけ取り戻す。ブロックが
82
- /// 例外を投げた場合は、その例外をそのまま呼び出し元へ伝える(エンジン側は
83
- /// 通常のエラーパスで巻き戻る)。
75
+ /// The GVL is released during rendering and reacquired only for the moment the block is
76
+ /// called. If the block throws an exception, that exception is propagated to the caller
77
+ /// unchanged (the engine unwinds through its ordinary error path).
84
78
  fn render_each(
85
79
  html: RString,
86
80
  argv: Vec<String>,
87
81
  block: Proc,
88
82
  chunk_size: usize,
89
83
  ) -> Result<(), Error> {
90
- let ruby = Ruby::get().expect("GVLを保持したまま呼ばれるはず");
84
+ let ruby = Ruby::get().expect("it should be called while holding the GVL");
91
85
  let html = unsafe { html.as_slice() }.to_vec();
92
86
  errors::catch_panic(&ruby, move || {
93
87
  render_each_inner(html, argv, block, chunk_size)
@@ -100,11 +94,11 @@ fn render_each_inner(
100
94
  block: Proc,
101
95
  chunk_size: usize,
102
96
  ) -> Result<(), Error> {
103
- let ruby = Ruby::get().expect("GVLを保持したまま呼ばれるはず");
104
- let (args, fonts) = cli::parse_convert_argv(&argv).map_err(|e| errors::to_ruby(&ruby, e))?;
97
+ let ruby = Ruby::get().expect("it should be called while holding the GVL");
98
+ let converter = Converter::from_args(argv).map_err(|e| errors::to_ruby(&ruby, e))?;
105
99
 
106
- // ブロックはGVL解放区間をまたいで生きる必要があるため、GCへ登録する
107
- // (解放後にスタックへ積んだ値は保守的GCの走査対象外)。
100
+ // The block has to survive across the GVL-released region, so it is registered with the
101
+ // GC (a value pushed onto the stack after the release is outside the conservative GC's scan).
108
102
  let block = ValueSlot::new(block.as_raw());
109
103
  let mut pending = PendingUnwind::default();
110
104
 
@@ -112,47 +106,50 @@ fn render_each_inner(
112
106
  let slot = BlockSlot::new(&block);
113
107
  let pending = &mut pending;
114
108
  gvl::without_gvl(move || {
115
- // レンダリングは専用スタックのスレッドで走り、確定したチャンクだけが
116
- // ここへ戻ってくる。ブロックの呼び出し(=GVLの再取得)は、GVLを
117
- // 手放したこのスレッドで行う必要があるため`pump_to_block`に任せる。
109
+ // Rendering runs on a thread with a dedicated stack, and only the settled chunks
110
+ // come back here. Calling the block (that is, reacquiring the GVL) has to happen
111
+ // on this thread, the one that released it, so it is left to `pump_to_block`.
118
112
  pump_to_block(slot, pending, chunk_size, move |sink| {
119
- convert::render(&args, &fonts, Cursor::new(html), sink)
113
+ converter.render(Cursor::new(html), sink)
120
114
  })
121
115
  })
122
116
  };
123
117
  drop(block);
124
118
 
125
- // ブロック由来の中断は、エンジンが返すエラーより優先して伝える
126
- // (`Sink::Error`が`io::Error`固定のため、理由はこちらに載っている)。
127
- // `break`等の脱出は`into_error`の中で`rb_jump_tag`し戻らないので、
128
- // Rust側の値はここで落としきってから呼ぶ。
119
+ // An interruption from the block is propagated in preference to whatever error the engine
120
+ // returns (`Sink::Error` is fixed to `io::Error`, so the reason rides on this instead).
121
+ // A non-local exit such as `break` calls `rb_jump_tag` inside `into_error` and never
122
+ // returns, so the Rust-side values are all dropped before it is called.
129
123
  if pending.is_pending() {
130
124
  drop(result);
131
125
  return Err(pending
132
126
  .into_error()
133
- .expect("is_pendingがtrueなら中断が入っている"));
127
+ .expect("with is_pending true, an interruption is present"));
134
128
  }
135
129
  result.map_err(|e| errors::to_ruby(&ruby, e))
136
130
  }
137
131
 
138
- /// coreへリンクできていることの確認用(疎通確認)。
132
+ /// For confirming we can link against the core (a connectivity check).
139
133
  fn core_version() -> String {
140
134
  env!("CARGO_PKG_VERSION").to_string()
141
135
  }
142
136
 
143
- /// coreのシンボルを実際に1つ呼んでリンクを確かめる。
137
+ /// Really call one of the core's symbols to confirm the link.
144
138
  fn default_page_size() -> String {
145
- let settings = sghtmltopdf_core::layout::PageSettings::default();
139
+ let settings = sghtmltopdf_core::PageSettings::default();
146
140
  format!("{}x{}", settings.size.width, settings.size.height)
147
141
  }
148
142
 
149
- /// GVLを解放して実行できることの確認用。解放中も他のRubyスレッドが
150
- /// 進めることをRuby側のテストで検証する。
143
+ /// For confirming it can run with the GVL released. That other Ruby threads make progress
144
+ /// during the release is checked by a test on the Ruby side.
151
145
  fn sleep_without_gvl(ms: u64) {
152
146
  gvl::without_gvl(|| std::thread::sleep(std::time::Duration::from_millis(ms)));
153
147
  }
154
148
 
155
- #[magnus::init]
149
+ // The exported symbol has to match the `.so` file name, which stays `sghtmltopdf`. Without
150
+ // this the name would come from the package (`sghtmltopdf-ruby`) and Ruby would look for an
151
+ // `Init_sghtmltopdf` that does not exist.
152
+ #[magnus::init(name = "sghtmltopdf")]
156
153
  fn init(ruby: &Ruby) -> Result<(), Error> {
157
154
  let module = ruby.define_module("Sghtmltopdf")?;
158
155
  errors::define(ruby, module)?;
@@ -1,24 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sghtmltopdf
4
- # グローバルな既定オプション。
4
+ # The global default options.
5
5
  #
6
6
  # Sghtmltopdf.configure do |c|
7
7
  # c.page_size = "A4"
8
8
  # c.gothic_font = "/path/to/NotoSansJP-Regular.ttf"
9
9
  # end
10
10
  #
11
- # ここで設定した値は`render`/`render_to_file`の引数で上書きできる
12
- # (マージ順はグローバル → 呼び出し時)。
11
+ # A value set here can be overridden by an argument to `render`/`render_to_file`
12
+ # (merged in the order global, then call-time).
13
13
  #
14
- # キー名の妥当性は検査しない。オプション定義はRust側(`cli/options.rs`)の
15
- # 1箇所に集約する方針のため、未知のキーはレンダリング時にclapが`UsageError`をraiseする。
14
+ # Key names are not validated. The option definitions live in one place on the Rust side
15
+ # (`cli/options.rs`), so an unknown key makes clap raise a `UsageError` at render time.
16
16
  class Configuration
17
17
  def initialize(options = {})
18
18
  @options = {}
19
- # 明示的に設定した値(@options)と、Railtieなどが流し込んだ既定値
20
- # (@defaults)は分けて持つ。読み出しは常に@optionsが勝つので、
21
- # イニシャライザの実行順に依存しない。
19
+ # Values set explicitly (@options) are kept separately from the defaults injected by
20
+ # the Railtie and others (@defaults). Reads always prefer @options, so nothing depends
21
+ # on the order the initialisers run in.
22
22
  @defaults = {}
23
23
  options.each { |key, value| self[key] = value }
24
24
  end
@@ -32,30 +32,30 @@ module Sghtmltopdf
32
32
  @options[Options.canonical_key(key)] = value
33
33
  end
34
34
 
35
- # @param with_defaults [Boolean] 流し込まれた既定値を含めるか。
36
- # HTTPサーバへ委譲するときは`false`にする。Rails向けの既定値
37
- # (`base_url`・`allow`)はローカルのファイル解決のためのもので、
38
- # サーバモードではリクエストから指定できないキーだから
35
+ # @param with_defaults [Boolean] whether to include the injected defaults.
36
+ # Set it to `false` when delegating to the HTTP server: the Rails-oriented defaults
37
+ # (`base_url` and `allow`) exist for local file resolution and are keys server mode
38
+ # cannot take from a request
39
39
  def to_h(with_defaults: true)
40
40
  with_defaults ? @defaults.merge(@options) : @options.dup
41
41
  end
42
42
 
43
- # 既定値を流し込む。Railtieが Rails向けの既定値を入れるのに使う。
44
- # 明示的に設定された値より弱い(順序に関係なく`[]=`が勝つ)。
43
+ # Inject the defaults. Used by the Railtie to set the Rails-oriented defaults.
44
+ # They are weaker than explicitly set values (`[]=` wins regardless of order).
45
45
  def apply_defaults(defaults)
46
46
  defaults.each { |key, value| @defaults[Options.canonical_key(key)] = value }
47
47
  self
48
48
  end
49
49
 
50
- # `c.page_size = "A4"`と`c.page_size`を受ける。
50
+ # Accepts both `c.page_size = "A4"` and `c.page_size`.
51
51
  def method_missing(name, *args)
52
52
  key = name.to_s
53
53
  if key.end_with?("=")
54
- raise ArgumentError, "#{name}は引数1つを取ります" unless args.size == 1
54
+ raise ArgumentError, "#{name} takes one argument" unless args.size == 1
55
55
 
56
56
  self[key.chomp("=")] = args.first
57
57
  else
58
- raise ArgumentError, "#{name}は引数を取りません" unless args.empty?
58
+ raise ArgumentError, "#{name} takes no arguments" unless args.empty?
59
59
 
60
60
  self[key]
61
61
  end
@@ -3,44 +3,42 @@
3
3
  require "uri"
4
4
 
5
5
  module Sghtmltopdf
6
- # オプションハッシュを変換する。
6
+ # Converts an options hash into:
7
7
  #
8
- # * ネイティブ拡張へ渡すCLIの引数列(argv) … [.to_argv]
9
- # * HTTPサーバモードへ渡すクエリ文字列 … [.to_query]
8
+ # * the CLI argument list (argv) passed to the native extension ... [.to_argv]
9
+ # * the query string passed to HTTP server mode ... [.to_query]
10
10
  module Options
11
- # 入力は常に標準入力を表す`-`を置く(実際のバイト列はFFIで直接渡すため
12
- # 読まれない)。出力先はRust側のSinkが決めるので、ここもダミーの`-`。
13
- # `-`入力のときCLIは`--output`を必須にするため、省略はできない。
14
- ARGV_PREFIX = ["sghtmltopdf", "-", "--output", "-"].freeze
15
11
 
16
- # Ruby側だけで解釈するキー。変換オプションではないので、argvにも
17
- # クエリにも出さない。
12
+ # The keys interpreted on the Ruby side alone. They are not conversion options, so they
13
+ # appear in neither the argv nor the query.
18
14
  TRANSPORT_KEYS = %i[server_url server_open_timeout server_read_timeout chunk_size].freeze
19
15
 
20
- # 別名のキー(値は正規名)。CLIは`--allow`を`--allow-path`の別名として
21
- # 受けるが、Ruby側は2つのキーのまま持ち回ってはいけない。既定が一方の
22
- # キー、呼び出し時の指定がもう一方のキーだと、ハッシュのマージでは
23
- # 上書きにならず両方がargvへ出てしまう(同じフラグの繰り返しは
24
- # 「置き換え」ではなく「合併」の意味になる)。
16
+ # Alias keys (values are the canonical names). The CLI accepts `--allow` as an
17
+ # alias of `--allow-path`, but the Ruby side must not carry both keys around.
18
+ # If the defaults use one key and the call uses the other, a hash merge does
19
+ # not override: both end up in argv (repeating the same flag means "union",
20
+ # not "replace").
25
21
  ALIAS_KEYS = {allow: :allow_path}.freeze
26
22
 
27
23
  module_function
28
24
 
29
- # 別名のキーを正規名へ寄せる。
25
+ # Map an alias key to its canonical name.
30
26
  def canonical_key(key)
31
27
  key = key.to_sym
32
28
  ALIAS_KEYS.fetch(key, key)
33
29
  end
34
30
 
35
- # ハッシュのキーをまとめて正規化する。
31
+ # Normalize all keys of a hash.
36
32
  def canonicalize(options)
37
33
  options.to_h { |key, value| [canonical_key(key), value] }
38
34
  end
39
35
 
40
- # @param options [Hash] Rubyのオプションハッシュ
41
- # @return [Array<String>] clapへ渡す引数列
36
+ # @param options [Hash] the Ruby options hash
37
+ # @return [Array<String>] the conversion options passed to the core's `Converter`.
38
+ # The HTML and the destination travel over FFI, so there is no program name, input
39
+ # path or `--output` here.
42
40
  def to_argv(options)
43
- argv = ARGV_PREFIX.dup
41
+ argv = []
44
42
  each_pair(options) do |name, value|
45
43
  argv.push("--#{name}")
46
44
  argv.push(value) unless value.nil?
@@ -48,18 +46,18 @@ module Sghtmltopdf
48
46
  argv
49
47
  end
50
48
 
51
- # @param options [Hash] Rubyのオプションハッシュ
52
- # @return [String] `POST /pdf`のクエリ文字列(先頭に`?`は付けない)
49
+ # @param options [Hash] the Ruby options hash
50
+ # @return [String] the query string for `POST /pdf` (with no leading `?`)
53
51
  def to_query(options)
54
52
  parts = []
55
53
  each_pair(options) do |name, value|
56
- # 値なしのフラグはキーだけを置く(サーバは値なし=真として扱う)。
54
+ # A valueless flag becomes just the key (the server treats no value as true).
57
55
  parts << (value.nil? ? escape(name) : "#{escape(name)}=#{escape(value)}")
58
56
  end
59
57
  parts.join("&")
60
58
  end
61
59
 
62
- # 1つのキーと値をargvの断片へ変換する。
60
+ # Convert one key and value into an argv fragment.
63
61
  #
64
62
  # page_size: "A4" → ["--page-size", "A4"]
65
63
  # grayscale: true → ["--grayscale"]
@@ -69,8 +67,8 @@ module Sghtmltopdf
69
67
  pairs_for(key, value).flat_map { |name, arg| arg.nil? ? ["--#{name}"] : ["--#{name}", arg] }
70
68
  end
71
69
 
72
- # 1つのキーと値を「フラグ名と値」のペアの列にする。値が`nil`のペアは
73
- # 値を取らないフラグ(`--toc`など)。
70
+ # Turn one key and value into a list of "flag name and value" pairs. A pair whose value is
71
+ # `nil` is a flag taking no value (`--toc` and the like).
74
72
  def pairs_for(key, value)
75
73
  name = flag_name(key)
76
74
  return font_pairs(value) if name == "font"
@@ -78,26 +76,25 @@ module Sghtmltopdf
78
76
  case value
79
77
  when nil, false then []
80
78
  when true then [[name, nil]]
81
- # 配列は同じオプションの繰り返し。要素ごとに同じ規則を適用する。
79
+ # An array means the same option repeated. The same rule applies to each element.
82
80
  when Array then value.flat_map { |element| pairs_for(key, element) }
83
81
  when Hash
84
- # wicked_pdfの`margin: {top: 10}`のような入れ子は受けない。対応する
85
- # CLIフラグが無く、機械的に平坦化すると綴り違いのキーまで黙って
86
- # 通ってしまう。移行時は移行ガイドの対応表を見て書き換えてもらう。
87
- # なお単位を省いた数値の解釈はwicked_pdfと同じくmm(`cli/units.rs`)。
82
+ # Nesting such as wicked_pdf's `margin: {top: 10}` is not accepted. There is no
83
+ # corresponding CLI flag, and flattening it mechanically would let even misspelled
84
+ # keys through silently. When migrating, use the correspondence table in the
85
+ # migration guide to rewrite them. A unitless number is read as mm, as in wicked_pdf (`cli/units.rs`).
88
86
  example = value.keys.first
89
87
  raise ArgumentError,
90
- "#{key}にHashは渡せません(pathとindexを取るのは:fontだけです)。" \
91
- "入れ子のオプションは平坦なキーで指定してください" \
92
- "#{": 例 #{key}_#{example}: \"…\"" if example}"
88
+ "a Hash cannot be passed for #{key} (only :font takes a path and an index). " \
89
+ "Give nested options as flat keys" \
90
+ "#{": for example #{key}_#{example}: \"...\"" if example}"
93
91
  else [[name, value.to_s]]
94
92
  end
95
93
  end
96
94
 
97
- # `--font`と`--font-index`は出現順で対応付けられる(CLIは
98
- # `ArgMatches#indices_of`で「`--font-index`より手前にある最後の`--font`」
99
- # へ結び付ける)。そのため、フェイス番号は
100
- # 必ず対応する`--font`の直後へ置く。
95
+ # `--font` and `--font-index` are paired by their order of appearance (the CLI ties each
96
+ # `--font-index` to "the last `--font` before it" via `ArgMatches#indices_of`).
97
+ # So a face index always goes immediately after its own `--font`.
101
98
  #
102
99
  # font: "a.ttf" → ["--font", "a.ttf"]
103
100
  # font: {path: "a.ttc", index: 1} → ["--font", "a.ttc", "--font-index", "1"]
@@ -113,7 +110,7 @@ module Sghtmltopdf
113
110
  when Array then value.flat_map { |element| font_pairs(element) }
114
111
  when Hash
115
112
  path = value[:path] || value["path"]
116
- raise ArgumentError, "fontのHashにはpathが必要です: #{value.inspect}" if path.nil?
113
+ raise ArgumentError, "a font Hash needs a path: #{value.inspect}" if path.nil?
117
114
 
118
115
  index = value[:index] || value["index"]
119
116
  pairs = [["font", path.to_s]]
@@ -123,12 +120,12 @@ module Sghtmltopdf
123
120
  end
124
121
  end
125
122
 
126
- # `:page_size` → `page-size`。
123
+ # `:page_size` becomes `page-size`.
127
124
  def flag_name(key)
128
125
  key.to_s.tr("_", "-")
129
126
  end
130
127
 
131
- # 変換オプションだけを、渡された順にペアとして列挙する。
128
+ # Enumerate only the conversion options, as pairs, in the order they were given.
132
129
  def each_pair(options, &block)
133
130
  options.each do |key, value|
134
131
  key = canonical_key(key)
@@ -28,12 +28,12 @@ module Sghtmltopdf
28
28
  defaults
29
29
  end
30
30
 
31
- # 読むのはinitializerの中ではなく`after_initialize`。パイプラインが
32
- # `config.assets.paths`を埋めるのは自分のinitializer(Propshaftなら
33
- # `propshaft.append_assets_path`)で、そちらの方が後に走るため。
31
+ # Read in `after_initialize`, not inside the initializer. The pipeline fills
32
+ # `config.assets.paths` in its own initializer (for Propshaft,
33
+ # `propshaft.append_assets_path`), and that one runs later.
34
34
  #
35
- # `config/initializers`より後になるが、`apply_defaults`は明示的に設定した
36
- # 値より常に弱いので、ユーザーの設定を踏むことはない。
35
+ # This runs after `config/initializers`, but `apply_defaults` is always
36
+ # weaker than explicitly set values, so it never overrides user settings.
37
37
  initializer "sghtmltopdf.defaults" do |app|
38
38
  app.config.after_initialize do
39
39
  Sghtmltopdf.config.apply_defaults(Sghtmltopdf::Railtie.default_options(app))