Module: Yaml::Converter

Defined in:
lib/yaml/converter.rb,
lib/yaml/converter/config.rb,
lib/yaml/converter/parser.rb,
lib/yaml/converter/version.rb,
lib/yaml/converter/validation.rb,
lib/yaml/converter/state_machine.rb,
lib/yaml/converter/markdown_emitter.rb,
lib/yaml/converter/streaming_emitter.rb,
lib/yaml/converter/renderer/pdf_prawn.rb,
lib/yaml/converter/renderer/pandoc_shell.rb

Defined Under Namespace

Modules: Config, Renderer, Validation, Version Classes: Error, InvalidArgumentsError, MarkdownEmitter, PandocNotFoundError, Parser, RendererUnavailableError, StateMachine, StreamingEmitter

Constant Summary collapse

VERSION =

The gem version, exposed at the root of the Yaml::Converter namespace.

Returns:

  • (String)
Version::VERSION

Class Method Summary collapse

Class Method Details

.convert(input_path:, output_path:, options: {}) ⇒ Hash

Convert a YAML file to a target format determined by the output extension.

Supported extensions (Phase 1): .md, .html, .pdf (native), .docx (pandoc).
Other formats may be produced via pandoc when :use_pandoc is true.

Examples:

HTML conversion

Yaml::Converter.convert(input_path: "blueprint.yaml", output_path: "blueprint.html", options: {})

Parameters:

  • input_path (String)

    Path to existing .yaml source file.

  • output_path (String)

    Destination file (extension decides rendering strategy).

  • options (Hash) (defaults to: {})

    See #to_markdown plus pandoc/pdf specific keys.

Options Hash (options:):

  • :use_pandoc (Boolean) — default: false

    Enable pandoc for non-native conversions.

  • :pandoc_args (Array<String>) — default: ["-N", "--toc"]

    Extra pandoc CLI args.

  • :pandoc_path (String, nil) — default: nil

    Explicit pandoc binary path (auto-detected if nil).

  • :pdf_two_column_notes (Boolean) — default: false

    Layout notes beside YAML in PDF.

  • :streaming (Boolean) — default: false

    Force streaming mode for markdown conversion.

  • :streaming_threshold_bytes (Integer) — default: 5_000_000

    Auto-enable streaming over this size when not forced.

Returns:

  • (Hash)

    { status: Symbol, output_path: String, validation: Hash }

Raises:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/yaml/converter.rb', line 97

def convert(input_path:, output_path:, options: {})
  raise InvalidArgumentsError, "input file not found: #{input_path}" unless File.exist?(input_path)

  ext = File.extname(output_path)
  if ext == ".yaml"
    raise InvalidArgumentsError, "Output must not be .yaml"
  end

  opts = Config.resolve(options)
  yaml_string = nil

  if File.exist?(output_path) && ENV["KETTLE_TEST_SILENT"] != "true"
    warn("Overwriting existing file: #{output_path}")
  end

  auto_stream = !opts[:streaming] && File.size?(input_path) && File.size(input_path) >= opts[:streaming_threshold_bytes]

  if ext == ".md" || ext == ""
    # Direct markdown output path: stream to file for large inputs
    File.open(output_path, "w") do |io|
      if opts[:streaming] || auto_stream
        to_markdown_streaming(input_path, io, options: opts)
      else
        yaml_string = File.read(input_path)
        io.write(to_markdown(yaml_string, options: opts))
      end
    end
    validation_result = if opts[:validate]
      yaml_string ? Validation.validate_string(yaml_string) : Validation.validate_file(input_path)
    else
      {status: :ok, error: nil}
    end
    return {status: :ok, output_path: output_path, validation: validation_result}
  end

  # For non-markdown outputs, we still produce an intermediate markdown string.
  yaml_string = File.read(input_path) if yaml_string.nil?
  markdown = to_markdown(yaml_string, options: opts)

  # Helper lambda to build standard success response
  success = lambda do
    {status: :ok, output_path: output_path, validation: (opts[:validate] ? Validation.validate_string(yaml_string) : {status: :ok, error: nil})}
  end

  case ext
  when ".html"
    require "kramdown"
    require "kramdown-parser-gfm"

    body_html = Kramdown::Document.new(markdown, input: "GFM").to_html
    note_style = ""
    if markdown.include?("> NOTE:")
      note_style = "<style>.yaml-note{font-style:italic;color:#555;margin-left:1em;}</style>\n"
      body_html = body_html.gsub("<blockquote>", '<blockquote class="yaml-note">')
    end
    html = <<~HTML
      <!DOCTYPE html>
      <html>
      <head>
      <meta charset="utf-8">
      #{note_style}</head>
      <body>
      #{body_html}
      </body>
      </html>
    HTML
    File.write(output_path, html)
    success.call
  when ".pdf"
    if opts[:use_pandoc]
      tmp_md = output_path + ".md"
      File.write(tmp_md, markdown)
      require_relative "converter/renderer/pandoc_shell"
      ok = Renderer::PandocShell.render(md_path: tmp_md, out_path: output_path, pandoc_path: opts[:pandoc_path], args: opts[:pandoc_args])
      File.delete(tmp_md) if File.exist?(tmp_md)
      raise PandocNotFoundError, "pandoc not found in PATH" unless ok
    else
      require_relative "converter/renderer/pdf_prawn"
      ok = Renderer::PdfPrawn.render(markdown: markdown, out_path: output_path, options: opts)
      raise RendererUnavailableError, "PDF rendering failed" unless ok
    end
    success.call
  when ".docx"
    tmp_md = output_path + ".md"
    File.write(tmp_md, markdown)
    require_relative "converter/renderer/pandoc_shell"
    pandoc_path = Renderer::PandocShell.which("pandoc")
    if pandoc_path
      ok = Renderer::PandocShell.render(md_path: tmp_md, out_path: output_path, pandoc_path: pandoc_path, args: [])
      File.delete(tmp_md) if File.exist?(tmp_md)
      raise RendererUnavailableError, "pandoc failed to generate DOCX" unless ok
      success.call
    else
      File.delete(tmp_md) if File.exist?(tmp_md)
      raise RendererUnavailableError, "DOCX requires pandoc; install pandoc or use .md/.html/.pdf"
    end
  else
    tmp_md = output_path + ".md"
    File.write(tmp_md, markdown)
    if opts[:use_pandoc]
      require_relative "converter/renderer/pandoc_shell"
      ok = Renderer::PandocShell.render(md_path: tmp_md, out_path: output_path, pandoc_path: opts[:pandoc_path], args: opts[:pandoc_args])
      File.delete(tmp_md) if File.exist?(tmp_md)
      raise PandocNotFoundError, "pandoc not found in PATH" unless ok
      success.call
    else
      raise RendererUnavailableError, "Renderer for #{ext} not implemented. Pass use_pandoc: true or use .md/.html/.pdf."
    end
  end
end

.to_markdown(yaml_string, options: {}) ⇒ String

Convert a YAML string into Markdown with optional validation & notes extraction.

Examples:

Simple conversion

Yaml::Converter.to_markdown("foo: 1 #note: first")

Parameters:

Options Hash (options:):

  • :validate (Boolean) — default: true

    Whether to attempt YAML parsing and inject validation status.

  • :max_line_length (Integer) — default: 70

    Maximum line length before truncation.

  • :truncate (Boolean) — default: true

    Whether to truncate overly long lines.

  • :margin_notes (Symbol) — default: :auto

    How to handle notes (:auto, :inline, :ignore).

  • :current_date (Date) — default: Date.today

    Deterministic date injection for specs.

Returns:

  • (String)

    Markdown document including fenced YAML and extracted notes.



38
39
40
41
42
43
44
45
46
47
# File 'lib/yaml/converter.rb', line 38

def to_markdown(yaml_string, options: {})
  opts = Config.resolve(options)
  emitter = MarkdownEmitter.new(opts)
  if opts[:validate]
    validation = Validation.validate_string(yaml_string)
    status = (validation && validation[:status] == :ok) ? :ok : :fail
    emitter.set_validation_status(status)
  end
  emitter.emit(yaml_string.lines).join("\n")
end

.to_markdown_streaming(input_path, io, options: {}) ⇒ void

This method returns an undefined value.

Stream a YAML file to Markdown into an IO target.
Automatically injects validation status if enabled.

Parameters:

  • input_path (String)
  • io (#<<)
  • options (Hash) (defaults to: {})


55
56
57
58
59
60
61
62
63
64
65
# File 'lib/yaml/converter.rb', line 55

def to_markdown_streaming(input_path, io, options: {})
  opts = Config.resolve(options)
  emitter = StreamingEmitter.new(opts, io)
  if opts[:validate]
    validation = Validation.validate_file(input_path)
    status = (validation && validation[:status] == :ok) ? :ok : :fail
    emitter.set_validation_status(status)
  end
  emitter.emit_file(input_path)
  nil
end

.validate(yaml_string) ⇒ Hash{Symbol=>Object}

Validate a YAML string returning a structured status.

Examples:

Yaml::Converter.validate("foo: bar") #=> { status: :ok, error: nil }

Parameters:

  • yaml_string (String)

Returns:

  • (Hash{Symbol=>Object})
    Hash with :status (:ok :fail) and :error (Exception or nil)


73
74
75
# File 'lib/yaml/converter.rb', line 73

def validate(yaml_string)
  Validation.validate_string(yaml_string)
end