inkmark 0.1.3-aarch64-linux → 0.2.0-aarch64-linux

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: 873b571a395396a3d682aff879e9dffd0d7dd01a5e3f6067a6677af27d038485
4
- data.tar.gz: 81760a41c072a15a39734d2953edf08820df9cebdf0725bde1dd84e8a8f0aece
3
+ metadata.gz: 1a62cd9232907088d1781df13ebc9772f6d2ec3a20a3f331bf1edc07ab6cfa4e
4
+ data.tar.gz: e5e2ebb5eac180adc92c780473aacddbbd6185d8c97101bf58afa2825ce5ce73
5
5
  SHA512:
6
- metadata.gz: 493f98ec7a132115c7bc079d47f591a495ef20cc4d20cda45cd599cec35e6150c68c16ac54a129367ddbcc7dd48e6d4f65d23033c05c24c301b29f5ee23df097
7
- data.tar.gz: 56f4a258f95aade98509d9b45e59854f9957524de2682098c3171a2b162fa82f3183142afd19c8513e392d756c2569b759c9ba6f4712eb9816354861bf008852
6
+ metadata.gz: c5630ebc0b28c23efac86f7ff45cdb4367b5af804c923ff386a7b310020624e69f9cba7d824f40267e7d062e9ae45aac6c112ab330bb1737dfc0333b149eb5d1
7
+ data.tar.gz: 3e338f4b6e7418c24e4cd2b55813038c4224af2e56135b9c5a7b1dc905406ccc0c3bd0283ccb73e46d96977a39a285a3b8888b075e28dfa1f2b4c05194b076a2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.2.0] - 2026-09-06
2
+
3
+ - Ractor-safe: the native extension declares `rb_ext_ractor_safe`, and every public API works from non-main Ractors.
4
+ - `Inkmark.default_options` is now frozen; configure process-wide defaults with `Inkmark.configure { |o| ... }` or `Inkmark.default_options=`.
5
+ - Load YAML on the first `frontmatter` parse instead of at require time.
6
+ - Updated dependencies.
7
+
8
+ ## [0.1.4] - 2026-06-25
9
+
10
+ - Fix `frontmatter: true` leaking the frontmatter block into `to_markdown`, `chunks_by_heading`, and `chunks_by_size` output. Bug report by @freesteph [#3]
11
+
1
12
  ## [0.1.3] - 2026-06-21
2
13
 
3
14
  - Fix possible XSS via unescaped language tag in syntax-highlighted code blocks.
data/README.md CHANGED
@@ -38,6 +38,7 @@ A very fast, feature-packed, AI-first Markdown gem for Ruby.
38
38
  - [Plain-text extraction](#plain-text-extraction)
39
39
  - [Markdown-to-Markdown pipeline](#markdown-to-markdown-pipeline)
40
40
  - [Event handlers](#event-handlers)
41
+ - [Ractors](#ractors)
41
42
  - [Benchmarks](#benchmarks)
42
43
  - [Contributing](#contributing)
43
44
  - [Acknowledgements](#acknowledgements)
@@ -177,9 +178,15 @@ g.options.math = true
177
178
  g.options.tables = false
178
179
  g.to_html
179
180
 
180
- # Process-level defaults, to set in your application initializer
181
- Inkmark.default_options.math = true
182
- Inkmark.new(md).to_html # picks up the default
181
+ # Process-wide defaults, set once in your application initializer
182
+ Inkmark.configure do |options|
183
+ options.math = true
184
+ options.links = { nofollow: true }
185
+ end
186
+ Inkmark.new(md).to_html # picks up the defaults
187
+
188
+ # Or replace them wholesale
189
+ Inkmark.default_options = { preset: :recommended, math: true }
183
190
  ```
184
191
 
185
192
  Unknown option keys raise `ArgumentError` immediately, including via the
@@ -1029,6 +1036,21 @@ Post-render filters (`syntax_highlight`, allowlists, `images: { lazy: true }`,
1029
1036
  highlighter
1030
1037
  - Handler-set `dest=` values pass through host and scheme allowlists
1031
1038
 
1039
+ ## Ractors
1040
+
1041
+ Inkmark is Ractor-safe: every public API works inside non-main Ractors,
1042
+ so documents can be rendered in parallel without leaving the native fast
1043
+ path.
1044
+
1045
+ ```ruby
1046
+ workers = sources.each_slice(250).map do |slice|
1047
+ Ractor.new(slice) do |batch|
1048
+ batch.map { |src| Inkmark.to_html(src, options: { preset: :recommended }) }
1049
+ end
1050
+ end
1051
+ html = workers.flat_map(&:value) # Ruby 4.0; use #take on 3.3 and 3.4
1052
+ ```
1053
+
1032
1054
  ## Benchmarks
1033
1055
 
1034
1056
  Inkmark ships a benchmark harness comparing it against `kramdown`,
Binary file
Binary file
Binary file
@@ -22,22 +22,28 @@ class Inkmark
22
22
  # Per-element-policy schemas. Each entry is +{ default:, types: }+; the
23
23
  # validators use +types+ for type checking and +default+ to seed fresh
24
24
  # nested hashes. Keep in sync with {NESTED_TO_FLAT}.
25
+ #
26
+ # Every constant in this class is frozen all the way down, not just
27
+ # at the top level: a non-main Ractor may only read constants whose
28
+ # whole object graph is frozen (+Ractor.shareable?+), so the inner
29
+ # Hashes and Arrays are frozen explicitly and the larger nested
30
+ # tables go through +Ractor.make_shareable+.
25
31
  HEADINGS_SCHEMA = {
26
- attributes: {default: false, types: [TrueClass, FalseClass]},
27
- ids: {default: false, types: [TrueClass, FalseClass]}
32
+ attributes: {default: false, types: [TrueClass, FalseClass].freeze}.freeze,
33
+ ids: {default: false, types: [TrueClass, FalseClass].freeze}.freeze
28
34
  }.freeze
29
35
 
30
36
  IMAGES_SCHEMA = {
31
- lazy: {default: false, types: [TrueClass, FalseClass]},
32
- allowed_hosts: {default: nil, types: [NilClass, Array]},
33
- allowed_schemes: {default: nil, types: [NilClass, Array]}
37
+ lazy: {default: false, types: [TrueClass, FalseClass].freeze}.freeze,
38
+ allowed_hosts: {default: nil, types: [NilClass, Array].freeze}.freeze,
39
+ allowed_schemes: {default: nil, types: [NilClass, Array].freeze}.freeze
34
40
  }.freeze
35
41
 
36
42
  LINKS_SCHEMA = {
37
- autolink: {default: false, types: [TrueClass, FalseClass]},
38
- nofollow: {default: false, types: [TrueClass, FalseClass]},
39
- allowed_hosts: {default: nil, types: [NilClass, Array]},
40
- allowed_schemes: {default: nil, types: [NilClass, Array]}
43
+ autolink: {default: false, types: [TrueClass, FalseClass].freeze}.freeze,
44
+ nofollow: {default: false, types: [TrueClass, FalseClass].freeze}.freeze,
45
+ allowed_hosts: {default: nil, types: [NilClass, Array].freeze}.freeze,
46
+ allowed_schemes: {default: nil, types: [NilClass, Array].freeze}.freeze
41
47
  }.freeze
42
48
 
43
49
  # Registry of nested hash options => their schemas. Iterated by the
@@ -52,7 +58,7 @@ class Inkmark
52
58
  # Map from +(parent, child)+ user-facing keys to the flat key name the
53
59
  # Rust side reads. Used by {#to_native_hash} / {#to_native_hash_frozen}
54
60
  # to serialize the user-shaped hash into the FFI wire format.
55
- NESTED_TO_FLAT = {
61
+ NESTED_TO_FLAT = Ractor.make_shareable({
56
62
  [:headings, :attributes] => :heading_attributes,
57
63
  [:headings, :ids] => :heading_ids,
58
64
  [:images, :lazy] => :lazy_images,
@@ -62,7 +68,7 @@ class Inkmark
62
68
  [:links, :nofollow] => :nofollow_external_links,
63
69
  [:links, :allowed_hosts] => :allowed_link_hosts,
64
70
  [:links, :allowed_schemes] => :allowed_link_schemes
65
- }.freeze
71
+ })
66
72
 
67
73
  # Build a frozen defaults hash for a nested schema from its +default+
68
74
  # entries.
@@ -200,11 +206,11 @@ class Inkmark
200
206
  # the accepted type set (nil-default-but-Array-when-set, polymorphic
201
207
  # +toc+, nested-hash element-policy groups).
202
208
  TYPES = {
203
- extract: [NilClass, Hash],
204
- toc: [TrueClass, FalseClass, Hash],
205
- headings: [Hash],
206
- images: [Hash],
207
- links: [Hash]
209
+ extract: [NilClass, Hash].freeze,
210
+ toc: [TrueClass, FalseClass, Hash].freeze,
211
+ headings: [Hash].freeze,
212
+ images: [Hash].freeze,
213
+ links: [Hash].freeze
208
214
  }.freeze
209
215
 
210
216
  # Element kinds accepted inside +extract: { ... }+. Mirrors the match
@@ -237,7 +243,7 @@ class Inkmark
237
243
  # The GFM tagfilter stays on. **Dangerous.** Use only for content
238
244
  # the caller fully trusts (internal team-authored docs). The
239
245
  # caller is fully responsible for sanitizing output.
240
- PRESETS = {
246
+ PRESETS = Ractor.make_shareable({
241
247
  commonmark: {
242
248
  gfm: false,
243
249
  gfm_tag_filter: false,
@@ -245,7 +251,7 @@ class Inkmark
245
251
  strikethrough: false,
246
252
  tasklists: false,
247
253
  footnotes: false
248
- }.freeze,
254
+ },
249
255
 
250
256
  gfm: {
251
257
  gfm: true,
@@ -254,7 +260,7 @@ class Inkmark
254
260
  strikethrough: true,
255
261
  tasklists: true,
256
262
  footnotes: true
257
- }.freeze,
263
+ },
258
264
 
259
265
  recommended: {
260
266
  gfm: true,
@@ -272,7 +278,7 @@ class Inkmark
272
278
  syntax_highlight: true,
273
279
  hard_wrap: true,
274
280
  frontmatter: true
275
- }.freeze,
281
+ },
276
282
 
277
283
  trusted: {
278
284
  gfm: true,
@@ -290,8 +296,8 @@ class Inkmark
290
296
  syntax_highlight: true,
291
297
  hard_wrap: true,
292
298
  frontmatter: true
293
- }.freeze
294
- }.freeze
299
+ }
300
+ })
295
301
 
296
302
  # Preset applied by {#initialize} when the caller doesn't pass
297
303
  # +preset:+. +:gfm+ matches {DEFAULTS}, so the default constructor
@@ -387,7 +393,16 @@ class Inkmark
387
393
  # hashes; the input value as-is otherwise)
388
394
  # @raise [ArgumentError] if +key+ is unknown, or the value (or any
389
395
  # nested sub-value) has the wrong type
396
+ # @raise [FrozenError] if this instance is frozen (as
397
+ # {Inkmark.default_options} always is)
390
398
  def []=(key, value)
399
+ if frozen?
400
+ raise FrozenError.new(
401
+ "can't modify frozen #{self.class}: use Inkmark.configure to change " \
402
+ "the process-wide defaults, or dup for a mutable copy",
403
+ receiver: self
404
+ )
405
+ end
391
406
  validate_key!(key)
392
407
  # Deep-merge partial nested-hash overrides (+:headings+,
393
408
  # +:images+, +:links+) so callers pass only the sub-keys they
@@ -429,11 +444,17 @@ class Inkmark
429
444
  # FFI calls that don't need to add per-call params. The cache is
430
445
  # invalidated in {#[]=} and {#initialize_copy}.
431
446
  #
432
- # @return [Hash{Symbol => Object}] frozen, shared across calls until
433
- # a mutation invalidates it
447
+ # The hash is frozen all the way down, as a copy: nested Arrays and
448
+ # Hashes are duplicated before freezing so caller-supplied values
449
+ # (an +allowed_hosts+ Array, say) stay mutable in the caller's hands.
450
+ # A deeply frozen memo is what lets a frozen +Options+ be shared
451
+ # across Ractors.
452
+ #
453
+ # @return [Hash{Symbol => Object}] deeply frozen, shared across calls
454
+ # until a mutation invalidates it
434
455
  # @api private
435
456
  def to_native_hash_frozen
436
- @frozen_native_hash ||= build_native_hash.freeze
457
+ @frozen_native_hash ||= deep_frozen_copy(build_native_hash)
437
458
  end
438
459
 
439
460
  # Return a new Options instance with +other+'s values applied on top.
@@ -457,6 +478,18 @@ class Inkmark
457
478
  end
458
479
  alias_method :eql?, :==
459
480
 
481
+ # Freeze this instance. The memoized FFI hash is computed first, while
482
+ # the instance is still mutable, so {#to_native_hash_frozen} never has
483
+ # to write to a frozen object. +Ractor.make_shareable+ calls +freeze+
484
+ # on every object it visits, which is what makes a shared
485
+ # {Inkmark.default_options} renderable from any Ractor.
486
+ #
487
+ # @return [self]
488
+ def freeze
489
+ to_native_hash_frozen
490
+ super
491
+ end
492
+
460
493
  # Duplicate this instance, deep-copying the internal values hash so the
461
494
  # clone is fully independent from the original.
462
495
  def initialize_copy(orig)
@@ -466,14 +499,26 @@ class Inkmark
466
499
  @frozen_native_hash = nil
467
500
  end
468
501
 
469
- # @!macro [attach] inkmark_options_accessor
470
- # @!attribute [rw] $1
471
- # Reader and writer for the +$1+ option. The writer routes through
472
- # {#[]=} so key validation and (for nested groups) deep-merge apply
473
- # uniformly.
502
+ # Reader and writer for every option key. The writer routes through
503
+ # {#[]=} so key validation and (for nested groups) deep-merge apply
504
+ # uniformly.
505
+ #
506
+ # Generated from source strings rather than +define_method+ blocks:
507
+ # a block-defined method carries its Proc, and Ruby refuses to call
508
+ # such a method from a non-main Ractor ("defined with an un-shareable
509
+ # Proc in a different Ractor"). Plain +def+ bodies have no such
510
+ # baggage. +key+ is always a Symbol from {DEFAULTS}, so interpolating
511
+ # it is safe.
474
512
  DEFAULTS.each_key do |key|
475
- define_method(key) { @values[key] }
476
- define_method("#{key}=") { |value| self[key] = value }
513
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
514
+ def #{key} # def tables
515
+ @values[:#{key}] # @values[:tables]
516
+ end # end
517
+
518
+ def #{key}=(value) # def tables=(value)
519
+ self[:#{key}] = value # self[:tables] = value
520
+ end # end
521
+ RUBY
477
522
  end
478
523
 
479
524
  private
@@ -517,6 +562,19 @@ class Inkmark
517
562
  def validate_key!(key) = self.class.send(:validate_key!, key)
518
563
  def validate_value!(key, value) = self.class.send(:validate_value!, key, value)
519
564
 
565
+ # Return a frozen copy of +value+ with every nested Hash, Array, and
566
+ # String frozen too. Scalars (booleans, nil, Integers, Symbols) are
567
+ # returned as is. Only the container shapes {#build_native_hash} can
568
+ # produce are handled.
569
+ def deep_frozen_copy(value)
570
+ case value
571
+ when Hash then value.transform_values { |v| deep_frozen_copy(v) }.freeze
572
+ when Array then value.map { |v| deep_frozen_copy(v) }.freeze
573
+ when String then -value
574
+ else value
575
+ end
576
+ end
577
+
520
578
  # Build the Rust-facing flat hash: nested element-policy hashes expand
521
579
  # into their flat keys via {NESTED_TO_FLAT}; the internal +@toc_depth+
522
580
  # is injected when set.
@@ -624,10 +682,13 @@ class Inkmark
624
682
  # +options: { preset: :name }+ call pattern, which would otherwise
625
683
  # build a fresh +Options+ instance (seed defaults, 6–14 +[]=+
626
684
  # with validation, +build_native_hash+) on every call. The cached
627
- # hashes are frozen and safe to share across threads.
628
- PRESETS_NATIVE_HASH = PRESETS.keys.each_with_object({}) do |name, h|
629
- h[name] = new(preset: name).to_native_hash_frozen
630
- end.freeze
685
+ # hashes are deeply frozen and safe to share across threads and
686
+ # Ractors.
687
+ PRESETS_NATIVE_HASH = Ractor.make_shareable(
688
+ PRESETS.keys.each_with_object({}) do |name, h|
689
+ h[name] = new(preset: name).to_native_hash_frozen
690
+ end
691
+ )
631
692
 
632
693
  # Build a Rust-facing flat hash from +overrides+ without allocating
633
694
  # an +Options+ instance or walking +build_native_hash+. Starts from
@@ -2,5 +2,5 @@
2
2
 
3
3
  class Inkmark
4
4
  # Current gem version.
5
- VERSION = "0.1.3"
5
+ VERSION = "0.2.0"
6
6
  end
data/lib/inkmark.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
-
5
3
  # Inkmark is a very fast, feature-rich, AI-first CommonMark/GFM
6
4
  # markdown renderer backed by the Rust pulldown-cmark parser.
7
5
  #
@@ -64,12 +62,11 @@ class Inkmark
64
62
  # This is a class-method fast path that skips Inkmark instance and
65
63
  # Options copy allocation for the common one-shot render pattern.
66
64
  # When the caller passes +options: nil+ (the default), we reuse the
67
- # cached frozen hash that {Inkmark::Options#to_native_hash_frozen}
68
- # returns; the cache lives on the Options instance itself and is
69
- # invalidated by the Options mutation methods, so
70
- # +Inkmark.default_options.tables = false+ followed by
71
- # +Inkmark.to_html(src)+ picks up the new value without stale-cache
72
- # bugs.
65
+ # frozen hash memoized on {default_options} by
66
+ # {Inkmark::Options#to_native_hash_frozen}. {configure} and
67
+ # {default_options=} install a whole new frozen instance rather than
68
+ # mutating the shared one, so the next render sees the new values
69
+ # without any stale-cache bugs.
73
70
  #
74
71
  # **Raw HTML safety.** +raw_html: false+ (the default) escapes
75
72
  # every raw HTML tag in the source—safe for untrusted input.
@@ -351,33 +348,65 @@ class Inkmark
351
348
  end
352
349
 
353
350
  # Return an array of available syntax-highlighting theme names.
354
- # Memoized—the theme list is fixed at compile time.
351
+ # Memoized—the theme list is fixed at compile time. The memo is
352
+ # warmed while this file loads (see the bottom of the file), so
353
+ # non-main Ractors only ever read it.
355
354
  #
356
- # @return [Array<String>]
355
+ # @return [Array<String>] frozen, with frozen elements
357
356
  def highlight_themes
358
- @highlight_themes ||= _syntax_themes.freeze
357
+ @highlight_themes ||= _syntax_themes.each(&:freeze).freeze
359
358
  end
360
359
 
361
- # The class-level default options used when no per-instance options are given.
360
+ # The process-wide default options, used when a render is given no
361
+ # options of its own. The instance is frozen all the way down and
362
+ # shared by every thread and Ractor in the process; change it with
363
+ # {configure} or {default_options=}, never in place.
362
364
  #
363
- # @return [Inkmark::Options]
365
+ # @return [Inkmark::Options] frozen
364
366
  def default_options
365
- @default_options ||= Inkmark::Options.new
367
+ @default_options || DEFAULT_OPTIONS
366
368
  end
367
369
 
368
- # Replace the class-level default options.
370
+ # Replace the process-wide default options. The value is copied and
371
+ # frozen all the way down (+Ractor.make_shareable+), so the caller
372
+ # keeps its own object mutable and worker Ractors can read the
373
+ # result. Call this on the main Ractor before spawning workers.
369
374
  #
370
- # @param value [Hash, Inkmark::Options] new defaults; a Hash is converted to
371
- # Inkmark::Options, a Inkmark::Options is duped
372
- # @return [Inkmark::Options] the stored options object
375
+ # @param value [Hash, Inkmark::Options] new defaults; a Hash is
376
+ # converted to Inkmark::Options
377
+ # @return [Inkmark::Options] the stored, frozen options
373
378
  # @raise [TypeError] if +value+ is not a Hash or Inkmark::Options
379
+ # @example
380
+ # Inkmark.default_options = { preset: :recommended, math: true }
374
381
  def default_options=(value)
375
- @default_options =
382
+ options =
376
383
  case value
377
- when Inkmark::Options then value.dup
384
+ when Inkmark::Options then value
378
385
  when Hash then Inkmark::Options.new(value)
379
386
  else raise TypeError, "default_options must be a Hash or Inkmark::Options, got #{value.class}"
380
387
  end
388
+ # Copy-on-write of a shareable value: main-Ractor configuration.
389
+ @default_options = Ractor.make_shareable(options, copy: true) # audition:disable class-level-state
390
+ end
391
+
392
+ # Adjust the process-wide default options. Yields a mutable copy of
393
+ # the current {default_options}; the result is stored frozen through
394
+ # {default_options=}. Successive calls build on each other. Call this
395
+ # on the main Ractor before spawning workers.
396
+ #
397
+ # @yieldparam options [Inkmark::Options] a mutable copy of the
398
+ # current defaults
399
+ # @return [Inkmark::Options] the stored, frozen options
400
+ # @example In an application initializer
401
+ # Inkmark.configure do |options|
402
+ # options.math = true
403
+ # options.links = { nofollow: true }
404
+ # end
405
+ def configure
406
+ options = default_options.dup
407
+ yield options
408
+ self.default_options = options
409
+ default_options
381
410
  end
382
411
 
383
412
  private
@@ -413,12 +442,18 @@ class Inkmark
413
442
  end
414
443
  end
415
444
 
445
+ # Built-in defaults, served by {default_options} until {configure} or
446
+ # {default_options=} installs a replacement. Frozen all the way down
447
+ # (which also memoizes its FFI hash, see {Inkmark::Options#freeze}).
448
+ DEFAULT_OPTIONS = Ractor.make_shareable(Inkmark::Options.new)
449
+ private_constant :DEFAULT_OPTIONS
450
+
416
451
  # Create a new renderer for +source+.
417
452
  #
418
453
  # @param source [String, nil] markdown source; +nil+ is treated as an
419
454
  # empty string
420
455
  # @param options [Hash, Inkmark::Options, nil] rendering options; falls back
421
- # to a dup of {Inkmark.default_options} when nil
456
+ # to a mutable copy of {Inkmark.default_options} when nil
422
457
  # @raise [TypeError] if +options+ is not a Hash, Inkmark::Options, or nil
423
458
  def initialize(source = nil, options: nil)
424
459
  self.source = source
@@ -459,8 +494,8 @@ class Inkmark
459
494
 
460
495
  # Set rendering options.
461
496
  #
462
- # @param value [Hash, Inkmark::Options, nil] new options; nil resets to a dup
463
- # of {Inkmark.default_options}
497
+ # @param value [Hash, Inkmark::Options, nil] new options; nil resets to a
498
+ # mutable copy of {Inkmark.default_options}
464
499
  # @return [Inkmark::Options] the stored options object
465
500
  # @raise [TypeError] if +value+ is not a Hash, Inkmark::Options, or nil
466
501
  def options=(value)
@@ -681,6 +716,9 @@ class Inkmark
681
716
  # The raw YAML text is extracted by Rust during the event walk;
682
717
  # parsing uses Ruby's stdlib +YAML.safe_load+ so all standard YAML
683
718
  # types (strings, numbers, arrays, nested hashes) are supported.
719
+ # Psych is loaded here, on first use, rather than with the gem: front
720
+ # matter is opt-in, and this keeps Psych's load time and its own
721
+ # constants out of processes that never enable it.
684
722
  #
685
723
  # @return [Hash, nil] parsed frontmatter or nil
686
724
  # @example
@@ -691,7 +729,10 @@ class Inkmark
691
729
  return @frontmatter if defined?(@frontmatter)
692
730
  return @frontmatter = nil unless @options[:frontmatter]
693
731
  to_html unless @frontmatter_raw
694
- @frontmatter = @frontmatter_raw ? YAML.safe_load(@frontmatter_raw) : nil
732
+ return @frontmatter = nil unless @frontmatter_raw
733
+
734
+ require "yaml" unless defined?(::YAML) # audition:disable runtime-require
735
+ @frontmatter = YAML.safe_load(@frontmatter_raw)
695
736
  end
696
737
 
697
738
  private
@@ -715,3 +756,8 @@ class Inkmark
715
756
  extract.is_a?(Hash) && extract.any? { |_, v| v }
716
757
  end
717
758
  end
759
+
760
+ # Warm the class-level memo on the main Ractor while loading, so worker
761
+ # Ractors only ever read it (a first write from a worker would raise
762
+ # Ractor::IsolationError).
763
+ Inkmark.highlight_themes
data/sig/inkmark.rbs CHANGED
@@ -179,6 +179,7 @@ class Inkmark
179
179
  def self.highlight_themes: () -> Array[String]
180
180
  def self.default_options: () -> Inkmark::Options
181
181
  def self.default_options=: (options_input value) -> Inkmark::Options
182
+ def self.configure: () { (Inkmark::Options options) -> void } -> Inkmark::Options
182
183
 
183
184
  # ── Native bindings (private in spirit; registered on the class) ──────
184
185
  def self._native_to_html: (String source, Hash[Symbol, untyped]? opts_hash) -> String
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inkmark
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Yaroslav Markin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-21 00:00:00.000000000 Z
11
+ date: 2026-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.9'
47
+ version: '4.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.9'
54
+ version: '4.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: yard
57
57
  requirement: !ruby/object:Gem::Requirement