Module: Carve

Defined in:
lib/carve.rb,
lib/carve/version.rb

Constant Summary collapse

EXTENSIONS =

Extension names the native binding understands.

Taken from the engine itself rather than typed out here. This was a literal array, and the native binding had its own match arm listing the same names, so an extension carve-rs gained was unreachable from Ruby until somebody updated both by hand - and nothing failed in the meantime.

The names are the engine's kebab-case keys, exposed as symbols (:"math-block"). The snake_case spellings this binding has always taken (:math_block) and its short aliases (:mermaid, :dot, :toc) still work as ARGUMENTS; this constant lists the canonical names.

_extension_names.map(&:to_sym).freeze
MODES =

Render modes the native binding understands.

:interactive (default) emits live HTML with client-script hooks (e.g. <pre class="mermaid">, <details>). :static emits self-contained HTML for print / PDF / archival: it forces disclosure (<details open>) and pre-renders client-script constructs through the renderers: callables, degrading to (escaped) source when a renderer is absent or fails.

%i[interactive static].freeze
RENDERER_KEYS =

Renderer keys accepted by the renderers: Hash (see .to_html). Each maps a construct's source to a self-contained HTML string emitted on the static path. mermaid / chart / graphviz are callables (String) -> String; math is (String, display_bool) -> String.

%i[mermaid chart graphviz math].freeze
VERSION =

The released version of this gem. The gemspec derives spec.version from it, and an embedder quoting a version in a bug report is quoting this.

It is not kept correct by hand on release: test/release_version_test.rb compares it against ext/carve/Cargo.toml and against the newest cut CHANGELOG section on every run, and .github/workflows/release.yml refuses to publish a gem whose version is not the tag being released.

"0.1.1"

Class Method Summary collapse

Class Method Details

.needs_review?(source, current_version = nil) ⇒ Boolean

Whether a document was last processed under an older spec version than this engine targets, so the [behavior] changelog entries between the two are worth reviewing.

Carve.needs_review?(stored_document)

An UNSTAMPED document answers true: its provenance is unknown, and assuming it is current is the unsafe direction. Pass current_version to compare against something other than this engine's spec version.

See https://markup-carve.github.io/carve/versioning for what a version difference means for a stored document.

Returns:

  • (Boolean)


211
212
213
# File 'lib/carve.rb', line 211

def needs_review?(source, current_version = nil)
  _stamp_needs_review(source.to_s, current_version&.to_s)
end

.parse(source) ⇒ Object

Parse Carve source into an AST: a tree of Ruby Hashes and Arrays.

Carve.parse("# Hi")
# => {type: "document",
#     children: [{type: "heading", level: 1,
#                 children: [{type: "text", value: "Hi"}], attrs: nil}],
#     srcByteLength: 4}

The root carries exactly :type, :children and :srcByteLength (spec PART 12 §7). Frontmatter and footnote definitions are block nodes in :children, not root fields: a root field cannot carry the position §4 requires of every node, and both are source an editor navigates to. Frontmatter is the first child, carrying :format and RAW :content.

Every node is a Hash with a :type key plus its fields; child collections are Arrays; :attrs is nil or a Hash of {id:, classes:, key_values:}. Keys are symbols. This is the raw parse tree (default profile, no extensions), suitable for a custom renderer (e.g. Carve -> PDF).



233
234
235
236
237
238
239
# File 'lib/carve.rb', line 233

def parse(source)
  # max_nesting: false - the engine already bounds nesting (its own
  # MAX_NESTING_DEPTH cap), and that cap exceeds Ruby JSON's default
  # max_nesting of 100. Without this, deeply-nested-but-valid documents
  # that Carve.to_html renders fine would raise JSON::NestingError here.
  JSON.parse(_to_ast_json(source.to_s), symbolize_names: true, max_nesting: false)
end

.read_stamp(source) ⇒ Object

Read a document's provenance marker, as written by carve fmt --stamp.

Carve.read_stamp(source)
# => {version: "0.1", generated_by: "carve-php 0.1.0"}
Carve.read_stamp("# Hi\n")
# => nil

Returns nil when the document carries no marker. That is the normal case for a hand-written document and means "unknown", not "current".

Both documented marker forms are read - the trailing %% line and the %%% block - and a marker written by any Carve engine reads the same, which is the point of recording it.



195
196
197
# File 'lib/carve.rb', line 195

def read_stamp(source)
  _read_stamp(source.to_s)
end

.to_html(source, extensions: nil, mode: nil, renderers: nil, symbols: nil, safe: false, profile: nil, sections: true) ⇒ Object

Render Carve source to an HTML string.

With no extensions:

Carve.to_html("# Hello")             # => "<section ...>\n  <h1>Hello</h1>..."

With extensions (Array of names as Symbols or Strings):

Carve.to_html(src, extensions: [:math_block])
Carve.to_html(src, extensions: %w[math-block list-table])

Recognized extension names: see Carve::EXTENSIONS. Names may be given snake_case (:math_block) or hyphenated ("math-block").

An unknown extension name raises ArgumentError (from the native layer).

Static render mode

Pass mode: :static (or "static") to emit self-contained HTML for print / PDF / archival. In static mode disclosure is forced (<details open>) and client-script constructs are pre-rendered through the renderers: Hash:

Carve.to_html(src, extensions: [:fenced_render], mode: :static,
            renderers: { mermaid: ->(s) { "<svg>#{s}</svg>" } })

Renderer callables (Symbol or String keys, see Carve::RENDERER_KEYS):

* +:mermaid+ / +:chart+ / +:graphviz+ -> callable `(String) -> String`
* +:math+                             -> callable `(String, display) -> String`

When a needed renderer is absent, or a renderer raises / returns a non-String, the construct degrades to its HTML-ESCAPED source (never blank, never raw HTML). Omitting mode: defaults to interactive (non-breaking).

An unknown mode or renderer key raises ArgumentError (from the native layer).

Symbols

A :name: symbol renders its literal :name: source unless the name is in the symbols: Hash (String or Symbol keys, String values):

Carve.to_html("Ship it :rocket: :shrug:", symbols: { "rocket" => "🚀" })
# => "<p>Ship it 🚀 :shrug:</p>"   (an unmapped name stays literal)

The leading word-boundary guard is unaffected by an active map: a:b:c, 10:30: and me@example.com never become symbols. A non-String value raises TypeError (from the native layer).

SECURITY: a mapped symbol value is inserted as TRUSTED RAW output in the target format - it is NOT escaped, the same trust class as a renderers: callable. { "b" => "x" } emits a real <b> element, not escaped text. This is deliberate: processor configuration is trusted. NEVER build a symbols map out of untrusted / user-supplied input.

Section wrappers

A top-level heading is wrapped, along with the content following it up to the next same-or-shallower heading, in a <section> carrying the heading's id (spec PART 9 §13). Only the id moves - {#install .featured} gives



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/carve.rb', line 154

def to_html(source, extensions: nil, mode: nil, renderers: nil, symbols: nil,
            safe: false, profile: nil, sections: true)
  list = Array(extensions)

  # Fast path: interactive (default), no extensions, no renderers, no
  # symbols, neither safe-render control in play, and section wrapping left
  # on. `sections: false` MUST fall through -- `_to_html` takes no options,
  # so short-circuiting to it would drop the flag and return wrapped output
  # with no error, which is the failure mode a caller cannot see.
  if list.empty? && (mode.nil? || mode.to_s == "interactive") &&
     (renderers.nil? || renderers.empty?) && (symbols.nil? || symbols.empty?) &&
     !safe && profile.nil? && sections
    return _to_html(source.to_s)
  end

  _to_html_safe(
    source.to_s,
    list.map(&:to_s),
    (mode || :interactive).to_s,
    renderers || {},
    symbols || {},
    !!safe,
    profile&.to_s,
    !!sections,
  )
end