inkmark 0.1.3 → 0.2.0

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.
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,7 +1,7 @@
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: ruby
6
6
  authors:
7
7
  - Yaroslav Markin
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.9.126
18
+ version: 0.9.130
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.9.126
25
+ version: 0.9.130
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rake
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '3.9'
60
+ version: '4.0'
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '3.9'
67
+ version: '4.0'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: yard
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -202,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
202
  - !ruby/object:Gem::Version
203
203
  version: '0'
204
204
  requirements: []
205
- rubygems_version: 4.0.10
205
+ rubygems_version: 4.0.16
206
206
  specification_version: 4
207
207
  summary: Very fast, feature-packed, AI-first Markdown gem for Ruby.
208
208
  test_files: []