Class: Inkmark::Options
- Inherits:
-
Object
- Object
- Inkmark::Options
- Defined in:
- lib/inkmark/options.rb
Overview
Typed hash of Inkmark rendering options with a known key set. Unknown keys raise ArgumentError at every write path.
Nested policy hashes—:headings, :images, :links—group related options together and deep-merge over defaults when set, so users can tweak one sub-key without clobbering the others.
The meta :preset option (accepted in new and Options.native_hash_from) selects a named bundle from PRESETS applied before the rest of the overrides. :gfm is the default preset; see PRESETS for the full list.
Constant Summary collapse
- HEADINGS_SCHEMA =
Per-element-policy schemas. Each entry is { default:, types: }; the validators use
typesfor type checking anddefaultto seed fresh nested hashes. Keep in sync with NESTED_TO_FLAT. { attributes: {default: false, types: [TrueClass, FalseClass]}, ids: {default: false, types: [TrueClass, FalseClass]} }.freeze
- IMAGES_SCHEMA =
{ lazy: {default: false, types: [TrueClass, FalseClass]}, allowed_hosts: {default: nil, types: [NilClass, Array]}, allowed_schemes: {default: nil, types: [NilClass, Array]} }.freeze
- LINKS_SCHEMA =
{ autolink: {default: false, types: [TrueClass, FalseClass]}, nofollow: {default: false, types: [TrueClass, FalseClass]}, allowed_hosts: {default: nil, types: [NilClass, Array]}, allowed_schemes: {default: nil, types: [NilClass, Array]} }.freeze
- NESTED_SCHEMAS =
Registry of nested hash options => their schemas. Iterated by the validator and native-hash flattener to keep the three element-policy groupings uniform.
{ headings: HEADINGS_SCHEMA, images: IMAGES_SCHEMA, links: LINKS_SCHEMA }.freeze
- NESTED_TO_FLAT =
Map from (parent, child) user-facing keys to the flat key name the Rust side reads. Used by #to_native_hash / #to_native_hash_frozen to serialize the user-shaped hash into the FFI wire format.
{ [:headings, :attributes] => :heading_attributes, [:headings, :ids] => :heading_ids, [:images, :lazy] => :lazy_images, [:images, :allowed_hosts] => :allowed_image_hosts, [:images, :allowed_schemes] => :allowed_image_schemes, [:links, :autolink] => :autolink, [:links, :nofollow] => :nofollow_external_links, [:links, :allowed_hosts] => :allowed_link_hosts, [:links, :allowed_schemes] => :allowed_link_schemes }.freeze
- DEFAULTS =
Default values for every option. Top-level keys are user-facing; nested element-policy groups (
headings,images,links) hold their own default hashes built from NESTED_SCHEMAS. { # GFM conformance bundle. Enables pulldown-cmark's ENABLE_GFM and the # four core GFM extensions. Individual extensions can still be toggled # off after setting gfm: true. gfm: true, # GFM "Disallowed Raw HTML" extension. When +gfm+ and +raw_html+ are # both true, escapes the leading +<+ of nine unsafe tag names # (title, textarea, style, xmp, iframe, noembed, noframes, script, # plaintext). Required for GFM conformance; no effect when # +raw_html+ is false. gfm_tag_filter: true, # GFM pipe tables with optional column-alignment markers. tables: true, # GFM strikethrough: +~~text~~+ → +<del>+. strikethrough: true, # GFM task lists: +- [ ]+ and +- [x]+ → disabled checkboxes. tasklists: true, # Footnote references and definitions. footnotes: true, # Pass raw HTML tags through unescaped. Off by default for # untrusted-input safety. When true, the caller is fully responsible # for sanitizing output—Inkmark does not sanitize beyond the narrow # GFM tagfilter. Always run the output through a dedicated sanitizer # (Sanitize, Loofah, rails-html-sanitizer) for untrusted content. raw_html: false, # Smart punctuation: ASCII quotes/dashes/ellipses → typographic forms. smart_punctuation: false, # Heading-related options. +:attributes+ enables +# Title {#id .klass}+ # inline attribute syntax; +:ids+ auto-generates an +id+ on every # heading from its text (slug). User-supplied ids from +attributes+ # are preserved when +:ids+ fills the rest in. headings: schema_defaults(HEADINGS_SCHEMA), # Image-related options. +:lazy+ adds +loading="lazy" decoding="async"+ # to every +<img>+. +:allowed_hosts+ is a glob allowlist for # +<img src>+ hostnames (http/https); non-matching images drop to alt # text. +:allowed_schemes+ is a URL-scheme allowlist for image URLs. # Both allowlists default to +nil+ (no filtering); set +[]+ to # deny-all-external. images: schema_defaults(IMAGES_SCHEMA), # Link-related options. +:autolink+ turns bare URLs and emails into # clickable links. +:nofollow+ adds +rel="nofollow noopener"+ to # external +<a>+ tags. +:allowed_hosts+ / +:allowed_schemes+ are # glob / scheme allowlists for +<a href>+ (same semantics as the # image versions). Relative/anchor/mailto URLs are never filtered. links: schema_defaults(LINKS_SCHEMA), # Replace gemoji-style +:shortcode:+ sequences with the emoji # character. Unknown codes and codes inside code blocks are preserved. emoji_shortcodes: false, # Server-side syntax highlighting for fenced code blocks with a # language tag. Uses syntect with CSS class output—pair with # {Inkmark.highlight_css} for the theme stylesheet. syntax_highlight: false, # Treat every single newline in a paragraph as a hard line break # (+<br />+). Default is soft-break (single +\n+ → space). hard_wrap: false, # Collect a table of contents from headings. When set, {Inkmark#toc} # returns a {Inkmark::Toc} value object (+#to_markdown+ / +#to_html+ / # +#to_s+). Implicitly enables +headings[:ids]+ in the rendered HTML # so TOC anchor hrefs have matching targets. Also populates # {Inkmark#statistics} with +heading_count+. # # Accepts +true+ / +false+ for simple enable/disable, or a Hash with a # +:depth+ key to limit which heading levels appear in the rendered # TOC. +toc: { depth: 3 }+ renders h1–h3 only; +toc: {}+ or # +toc: true+ renders all levels. Depth filtering affects only the # rendered TOC; +heading_count+, +extracts[:headings]+, and # +chunks_by_heading+ still see every heading. toc: false, # Full document statistics: language detection, character/word counts, # and +*_count+ fields for headings, code blocks, images, links, and # footnote definitions. For structured arrays of records, use # {extract}. Implies +toc+ and +headings[:ids]+. statistics: false, # Opt into structured extraction of specific element kinds. Pass a # Hash with any of +:images+, +:links+, +:code_blocks+, +:headings+, # +:footnote_definitions+ set to +true+. {Inkmark#extracts} then # returns a Hash keyed by the requested kinds, each carrying an Array # of record Hashes with a +:byte_range+. +nil+ (default) disables # extraction. +extract: { headings: true }+ and +toc: true+ trigger # each other—one heading walk powers both surfaces. extract: nil, # Math: +$inline$+ and +$$display$$+ blocks → +<code class="language-math">+. math: false, # Definition lists: +term\n: definition+ → +<dl>+. definition_list: false, # Superscript: +^text^+ → +<sup>+. superscript: false, # Subscript: +~text~+ → +<sub>+ (conflicts with strikethrough—enable # only one). subscript: false, # Wiki-style links: +[[Page]]+ and +[[Page|label]]+ → +<a>+. wikilinks: false, # Frontmatter: YAML metadata at the start of the document. Recognized # +---\nkey: value\n---+ block is stripped from rendered output and # exposed as a Hash via {Inkmark#frontmatter}. frontmatter: false }.freeze
- TYPES =
Per-key class allowlist. Keys absent from this hash inherit their allowed classes from DEFAULTS: a boolean default allows
TrueClass/FalseClass, anildefault allowsNilClass, and so on. Entries here declare options whose default doesn’t fully describe the accepted type set (nil-default-but-Array-when-set, polymorphictoc, nested-hash element-policy groups). { extract: [NilClass, Hash], toc: [TrueClass, FalseClass, Hash], headings: [Hash], images: [Hash], links: [Hash] }.freeze
- EXTRACT_KINDS =
Element kinds accepted inside extract: { … }. Mirrors the match arms in Rust
stats::to_extracts_hash—changing one means changing the other. %i[ images links code_blocks headings footnote_definitions ].freeze
- PRESETS =
Named bundles of option settings. Pass preset: :name in the options hash and the preset’s values are applied first; any other keys override (deep-merging for nested element-policy hashes).
-
:gfm(the default applied by #initialize) — CommonMark + core GFM extensions (tables, strikethrough, tasklists, footnotes, tagfilter). Conservative, matches the render profile of every other major GFM engine. -
:commonmark— strict CommonMark, no GFM extensions. -
:recommended— opinionated bundle for modern web content. Enables smart punctuation, auto heading IDs, lazy-loading images, autolinks, rel=“nofollow noopener” on external links, URL scheme allowlists for links and images, emoji shortcodes, syntax highlighting, hard wraps, and frontmatter. Recommended starting point for apps; override individual options to tune. -
:trusted—:recommendedwith raw HTML pass-through enabled. The GFM tagfilter stays on. Dangerous. Use only for content the caller fully trusts (internal team-authored docs). The caller is fully responsible for sanitizing output.
-
{ commonmark: { gfm: false, gfm_tag_filter: false, tables: false, strikethrough: false, tasklists: false, footnotes: false }.freeze, gfm: { gfm: true, gfm_tag_filter: true, tables: true, strikethrough: true, tasklists: true, footnotes: true }.freeze, recommended: { gfm: true, gfm_tag_filter: true, tables: true, strikethrough: true, tasklists: true, footnotes: true, raw_html: false, smart_punctuation: true, headings: {attributes: false, ids: true}, images: {lazy: true, allowed_schemes: ["http", "https"]}, links: {autolink: true, nofollow: true, allowed_schemes: ["http", "https", "mailto"]}, emoji_shortcodes: true, syntax_highlight: true, hard_wrap: true, frontmatter: true }.freeze, trusted: { gfm: true, gfm_tag_filter: true, tables: true, strikethrough: true, tasklists: true, footnotes: true, raw_html: true, smart_punctuation: true, headings: {attributes: false, ids: true}, images: {lazy: true, allowed_schemes: ["http", "https"]}, links: {autolink: true, nofollow: true, allowed_schemes: ["http", "https", "mailto"]}, emoji_shortcodes: true, syntax_highlight: true, hard_wrap: true, frontmatter: true }.freeze }.freeze
- DEFAULT_PRESET =
Preset applied by #initialize when the caller doesn’t pass
preset:.:gfmmatches DEFAULTS, so the default constructor is equivalent to “CommonMark + core GFM, nothing else”. :gfm
Instance Attribute Summary collapse
-
#key ⇒ Object
Reader and writer for the
keyoption.
Class Method Summary collapse
- .native_hash_from(overrides) ⇒ Object
-
.schema_defaults(schema) ⇒ Object
Build a frozen defaults hash for a nested schema from its
defaultentries.
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
Compare by value equality (user-shaped view).
-
#[](key) ⇒ Object
Read an option by key.
-
#[]=(key, value) ⇒ Object
Write an option by key.
-
#initialize(overrides = {}) ⇒ Options
constructor
Build a new Options instance with defaults from DEFAULTS plus any overrides applied on top.
-
#initialize_copy(orig) ⇒ Object
Duplicate this instance, deep-copying the internal values hash so the clone is fully independent from the original.
-
#merge(other) ⇒ Inkmark::Options
Return a new Options instance with
other‘s values applied on top. -
#to_h ⇒ Hash{Symbol => Object}
Return a plain user-shaped Hash copy of the current option values.
-
#to_native_hash ⇒ Hash{Symbol => Object}
private
Return a Rust-facing flat Hash: nested element-policy hashes are expanded into their flat Rust keys via NESTED_TO_FLAT, and the internal @toc_depth is injected when set.
-
#to_native_hash_frozen ⇒ Hash{Symbol => Object}
private
Memoized frozen variant of #to_native_hash used by the hot-path FFI calls that don’t need to add per-call params.
Constructor Details
#initialize(overrides = {}) ⇒ Options
Build a new Options instance with defaults from DEFAULTS plus any overrides applied on top. Nested hash values (for :headings, :images, :links) are deep-merged over the defaults—users only need to pass the sub-keys they care about.
362 363 364 365 366 367 |
# File 'lib/inkmark/options.rb', line 362 def initialize(overrides = {}) @values = dup_with_nested(DEFAULTS) @toc_depth = nil @frozen_native_hash = nil apply_overrides!(overrides, default_preset: DEFAULT_PRESET) end |
Instance Attribute Details
#key ⇒ Object
Reader and writer for the key option. The writer routes through #[]= so key validation and (for nested groups) deep-merge apply uniformly.
474 475 476 477 |
# File 'lib/inkmark/options.rb', line 474 DEFAULTS.each_key do |key| define_method(key) { @values[key] } define_method("#{key}=") { |value| self[key] = value } end |
Class Method Details
.native_hash_from(overrides) ⇒ Object
647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 |
# File 'lib/inkmark/options.rb', line 647 def native_hash_from(overrides) overrides = overrides.to_h preset_name = overrides[:preset] || DEFAULT_PRESET cached = PRESETS_NATIVE_HASH[preset_name] unless cached raise ArgumentError, "unknown preset: #{preset_name.inspect}; expected one of #{PRESETS.keys.inspect}" end # Fast-fast path: only :preset (or nothing) → return cached frozen hash. non_preset_keys = overrides.size - (overrides.key?(:preset) ? 1 : 0) return cached if non_preset_keys.zero? h = cached.dup toc_depth = h[:toc_depth] overrides.each do |key, value| next if key == :preset validate_key!(key) validate_value!(key, value) case key when :headings, :images, :links # value is a Hash (validated); flatten each sub-key via # NESTED_TO_FLAT. The flat-hash representation of the final # state is equivalent to the deep-merged nested representation, # since the preset-cached base already has all sub-keys present. value.each do |sub_key, sub_value| h[NESTED_TO_FLAT.fetch([key, sub_key])] = sub_value end when :toc if value.is_a?(Hash) h[:toc] = true toc_depth = value[:depth] else h[:toc] = value end else h[key] = value end end if toc_depth.nil? h.delete(:toc_depth) else h[:toc_depth] = toc_depth end h.freeze end |
.schema_defaults(schema) ⇒ Object
Build a frozen defaults hash for a nested schema from its default entries.
69 70 71 |
# File 'lib/inkmark/options.rb', line 69 def self.schema_defaults(schema) schema.each_with_object({}) { |(k, v), h| h[k] = v[:default] }.freeze end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
Compare by value equality (user-shaped view).
455 456 457 |
# File 'lib/inkmark/options.rb', line 455 def ==(other) other.class == self.class && to_h == other.to_h end |
#[](key) ⇒ Object
Read an option by key. Nested element-policy keys return the nested hash as a live reference—mutating it directly bypasses cache invalidation; prefer the setter.
376 377 378 |
# File 'lib/inkmark/options.rb', line 376 def [](key) @values.fetch(key) end |
#[]=(key, value) ⇒ Object
Write an option by key. For nested element-policy keys (:headings, :images, :links) the hash is deep-merged over the current value, so callers may pass only the sub-keys they want to change.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 |
# File 'lib/inkmark/options.rb', line 390 def []=(key, value) validate_key!(key) # Deep-merge partial nested-hash overrides (+:headings+, # +:images+, +:links+) so callers pass only the sub-keys they # care about; non-Hash values fall through to validate_value! # and raise there. value = @values[key].merge(value) if NESTED_SCHEMAS.key?(key) && value.is_a?(Hash) validate_value!(key, value) @values[key] = value # Sugar: +toc: { depth: N }+ normalizes to +toc: true+ plus the # depth stashed in +@toc_depth+ (not a user-facing option). if key == :toc && value.is_a?(Hash) @values[:toc] = true @toc_depth = value[:depth] end @frozen_native_hash = nil end |
#initialize_copy(orig) ⇒ Object
Duplicate this instance, deep-copying the internal values hash so the clone is fully independent from the original.
462 463 464 465 466 467 |
# File 'lib/inkmark/options.rb', line 462 def initialize_copy(orig) super @values = dup_with_nested(orig.instance_variable_get(:@values)) @toc_depth = orig.instance_variable_get(:@toc_depth) @frozen_native_hash = nil end |
#merge(other) ⇒ Inkmark::Options
Return a new Options instance with other‘s values applied on top. Nested element-policy hashes deep-merge; top-level values replace. Accepts preset: on other to re-apply a named preset before the other overrides (unlike #initialize, no default preset is applied when other omits preset:—the receiver’s state is preserved).
447 448 449 450 451 452 |
# File 'lib/inkmark/options.rb', line 447 def merge(other) other_hash = other.is_a?(Inkmark::Options) ? other.to_h : other merged = dup merged.send(:apply_overrides!, other_hash, default_preset: nil) merged end |
#to_h ⇒ Hash{Symbol => Object}
Return a plain user-shaped Hash copy of the current option values. Nested element-policy groups are returned as nested Hashes, mirroring the input shape accepted by #initialize.
413 414 415 |
# File 'lib/inkmark/options.rb', line 413 def to_h dup_with_nested(@values) end |
#to_native_hash ⇒ Hash{Symbol => Object}
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Return a Rust-facing flat Hash: nested element-policy hashes are expanded into their flat Rust keys via NESTED_TO_FLAT, and the internal @toc_depth is injected when set. Used by the FFI layer.
424 425 426 |
# File 'lib/inkmark/options.rb', line 424 def to_native_hash build_native_hash end |
#to_native_hash_frozen ⇒ Hash{Symbol => Object}
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Memoized frozen variant of #to_native_hash used by the hot-path FFI calls that don’t need to add per-call params. The cache is invalidated in #[]= and #initialize_copy.
435 436 437 |
# File 'lib/inkmark/options.rb', line 435 def to_native_hash_frozen @frozen_native_hash ||= build_native_hash.freeze end |