Markdown Composer

Markdown Composer is a headless Ruby gem for selecting, composing, validating, and transforming Markdown-first document fragments.

It is framework-independent. It does not depend on Rails, an ORM, an authorization system, a template engine, a component framework, or a UI. Host applications pass plain Markdown/HTML sources into the gem and own source lookup, permissions, sanitization, asset handling, and presentation.

Requirements

  • Ruby >= 3.1
  • commonmarker >= 0.23.10, < 3
  • nokogiri >= 1.13.10, < 2

Development dependencies are limited to test and benchmark tooling.

Installation

From RubyGems:

# Gemfile
gem "markdown_composer"

Then run:

bundle install

For a standalone Ruby script:

gem install markdown_composer

From a Git repository before a RubyGems release:

# Gemfile
gem "markdown_composer", git: "https://github.com/SocIt2Em/markdown_composer.git"

If installing from source before a RubyGems release, point Bundler at the standalone repository:

# Gemfile
gem "markdown_composer", git: "https://github.com/SocIt2Em/markdown_composer.git"

Quick Start

require "markdown_composer"

sources = MarkdownComposer.source_list do
  current File.read("source_current.md")
  explicit :guide, File.read("guide.md")
end

plan = MarkdownComposer.plan do
  from :current
  select "h2_section[first:1]"
  include "all"
  set

  from({ type: "explicit", key: "guide" })
  select "h2_section"
  include "all"
  append
end

result = MarkdownComposer.compose(sources: sources, plan: plan)

puts result.markdown

MarkdownComposer.compose always receives source content through the sources: keyword. Plan-row inline sources are the exception: they embed small source content directly in the plan.

Sources

Runtime sources are plain hashes or values produced by MarkdownComposer.source_list.

sources = MarkdownComposer.source_list do
  current current_markdown
  explicit :guide, guide_markdown
  explicit :faq, faq_markdown
  inherited :site_intro, inherited_markdown
end

This returns the same shape as:

[
  { "key" => "current", "type" => "current", "markdown" => current_markdown },
  { "key" => "guide", "type" => "explicit", "markdown" => guide_markdown },
  { "key" => "faq", "type" => "explicit", "markdown" => faq_markdown },
  { "key" => "site_intro", "type" => "inherited", "markdown" => inherited_markdown }
]

Supported runtime source types:

Type Purpose
current Main document supplied by the caller.
explicit Named source selected by key.
inherited Named source resolved by the host application.

Plan-only source references such as previous, buffer, and inline are not runtime source records.

Plans

Plans can be authored with the Ruby DSL:

plan = MarkdownComposer.plan do
  output :markdown

  from :current
  select 'heading_2_section where none(title:equals("Table of Contents"))'
  include "heading_title"
  set

  from :buffer
  select 'heading_2 where text:starts_with("1.1")'
  remove_buffer_target
end

Structured config is also supported for storage, import, and export:

config = {
  "output" => "markdown",
  "compose" => [
    {
      "source" => "current",
      "select" => "h2_section[first:1]",
      "include" => "all",
      "action" => "set"
    }
  ]
}

result = MarkdownComposer.compose(sources: sources, config: config)

YAML and JSON helpers are available:

plan = MarkdownComposer.parse_yaml(yaml)
yaml = MarkdownComposer.to_yaml(plan)

plan = MarkdownComposer.parse_json(json)
json = MarkdownComposer.to_json(plan)

Row-style hashes are accepted for spreadsheet or GUI workflows:

plan = MarkdownComposer.parse_rows([
  { "Source" => "current", "Select" => "h2_section[first:1]", "Include" => "all", "Action" => "set" }
])

Public API

Main entry points:

MarkdownComposer.compose(sources:, config: nil, plan: nil, options: {})
MarkdownComposer.validate(config:, sources: [], options: {})
MarkdownComposer.normalise(config_or_rows)
MarkdownComposer.normalize(config_or_rows)
MarkdownComposer.capabilities(options: {})
MarkdownComposer.plan(&block)
MarkdownComposer.source_list(&block)
MarkdownComposer.parse_yaml(yaml_string)
MarkdownComposer.parse_json(json_string)
MarkdownComposer.parse_rows(row_hashes)
MarkdownComposer.to_yaml(plan)
MarkdownComposer.to_json(plan)
MarkdownComposer.to_rows(plan)

validate returns valid, the normalized plan, diagnostics, and errors. capabilities returns JSON-compatible metadata for building host UI controls.

Actions And Transforms

Compose actions place selected content into the composition buffer:

  • set, append, prepend
  • insert_before, insert_after, insert_between
  • replace, copy, move
  • modify
  • remove_buffer_target, transform_buffer_target

Transforms run after composition or inside modify/transform_buffer_target rows. Supported transform families include heading numbering, heading levels, text replacement, link handling, content insertion/removal, dedupe, ordering, sanitise, and adapter-owned transforms.

Use MarkdownComposer.capabilities as the source of truth for enabled actions, tokens, transform modes, required options, aliases, support status, and policy gates.

Framework Integration

Markdown Composer does not ship Rails, ViewComponent, React, Vue, or other framework-specific integrations.

Host applications are responsible for:

  • resolving documents into plain Markdown/HTML source hashes;
  • enforcing authorization, tenant, visibility, and workflow rules;
  • preparing source Markdown when templates or variables are involved;
  • sanitizing output and resolving assets;
  • rendering previews and mapping diagnostics into host UI.

Rails, Hotwire, React, Vue, CLIs, and scripts should wrap the same headless API.

Validation And Diagnostics

Validation covers portable Composer configuration: source references, selectors, include rules, targets, actions, transforms, output formats, diagnostics, and policy-gated features.

Example diagnostic:

{
  code: "transform.option_missing",
  message: "Missing required option from",
  path: "transform[0].options.from",
  severity: :error,
  details: nil
}

Current diagnostic families include:

  • action.*
  • json.*
  • output.*
  • source.*
  • take.*
  • target.*
  • token.*
  • transform.*
  • where.*
  • yaml.*

Format Support

Markdown is the source-fidelity format. HTML input is accepted through a best-effort HTML-to-Markdown path and may lose fidelity for structures that do not map cleanly to Markdown.

From To Status Notes
Markdown Markdown Supported Source-fidelity selection and transforms.
Markdown HTML Supported Rendered via CommonMarker when available, fallback renderer otherwise.
HTML Markdown Best effort Uses Nokogiri-backed conversion.
HTML HTML Best effort Converts through Markdown, then renders back to HTML.

Host-specific sanitization, custom attributes, and asset policies should live outside the gem.

Development

Run the test suite:

bundle exec rake test

Build the gem package:

gem build markdown_composer.gemspec

The release test suite covers public helper APIs, diagnostics, transform capability coverage, Markdown/HTML conversion fixtures, syntax checks, and coupling guards.

License

Markdown Composer is released under the MIT License. See LICENSE.txt.