Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

on:
push:
branches:
- '**'
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby-version:
- '3.2'
- '3.3'
- '3.4'
- '4.0'

name: Ruby ${{ matrix.ruby-version }}

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true

- name: Run test suite
run: bundle exec rake test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,5 @@ Gemfile.lock
# .rubocop-https?--*
.copilot-instructions.md
.vscode
notes.txt

2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ gem 'erb'

group :test do
gem 'minitest'
gem 'rake'
gem 'simplecov', require: false
end
43 changes: 43 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require 'rake/testtask'

def apply_test_options(task)
opts = ENV['TESTOPTS'].to_s.strip
task.options = opts.split(/\s+/) unless opts.empty?
end

desc 'Run all tests'
Rake::TestTask.new(:test) do |t|
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
apply_test_options(t)
end

namespace :test do
desc 'Run CLI integration tests only'
Rake::TestTask.new(:cli) do |t|
t.libs << 'test'
t.pattern = 'test/cli_test.rb'
apply_test_options(t)
end

desc 'Run all tests with coverage'
task :coverage do
original_coverage = ENV['COVERAGE']
begin
ENV['COVERAGE'] = '1'
Rake::Task[:test].reenable
Rake::Task[:test].invoke

require 'simplecov'
SimpleCov.collate Dir['coverage/.resultset*.json'] do
track_files 'lib/**/*.rb'
end
ensure
ENV['COVERAGE'] = original_coverage
end
end
end

task default: :test
2 changes: 2 additions & 0 deletions lib/documark/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def read_document(input_path)
def parse_data_section(content)
return nil if content.nil? || content.strip.empty?
data = Psych.safe_load(content, aliases: false)
return data unless data.is_a?(Hash)

# lang is an alias of language
data['language'] ||= data['lang'] if data.key?('lang')
data
Expand Down
2 changes: 1 addition & 1 deletion lib/documark/render_html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def compose_html(options, layout, data, html)
end

def cleanup_markdown_output(content)
text = content.to_s
text = content.to_s.dup
# Remove common kramdown attribute list syntax from block and inline usage.
text.gsub!(/^[ \t]*\{:[^}\n]*\}[ \t]*\n/, '')
text.gsub!(/[ \t]*\{:[^}\n]*\}/, '')
Expand Down
132 changes: 132 additions & 0 deletions test/cli_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true

require 'open3'
require 'tmpdir'
require 'rbconfig'
require_relative 'test_helper'

class DocumarkCliTest < Minitest::Test
def run_cli(*args, stdin_data: nil)
exe = File.expand_path('../bin/documark', __dir__)
preload = File.expand_path('support/simplecov_subprocess.rb', __dir__)

env = {}
if ENV['COVERAGE'] == '1'
existing_rubyopt = ENV['RUBYOPT'].to_s
env['RUBYOPT'] = [existing_rubyopt, "-r#{preload}"].reject(&:empty?).join(' ')
env['COVERAGE'] = '1'
end

Open3.capture3(env, RbConfig.ruby, exe, *args, stdin_data: stdin_data)
end

def fixture_path(name)
File.expand_path("fixtures/#{name}", __dir__)
end

def test_exits_with_error_when_action_is_missing
_stdout, stderr, status = run_cli

refute status.success?
assert_includes stderr, 'Missing action. Usage: documark <action> @switches.'
end

def test_exits_with_error_when_required_process_options_are_missing
_stdout, stderr, status = run_cli('process')

refute status.success?
assert_includes stderr, 'Missing required option for process action: --input'
end

def test_exits_with_error_for_invalid_target
Dir.mktmpdir('documark-cli-test') do |tmpdir|
output_path = File.join(tmpdir, 'out.invalid')

_stdout, stderr, status = run_cli(
'process',
'--input', fixture_path('fmerrormissing.dm'),
'--output', output_path,
'--target', 'invalid'
)

refute status.success?
assert_includes stderr, "Invalid target 'invalid'. Supported targets:"
end
end

def test_html_target_writes_html_output
Dir.mktmpdir('documark-cli-test') do |tmpdir|
output_path = File.join(tmpdir, 'out.html')

_stdout, stderr, status = run_cli(
'process',
'--input', File.expand_path('../doc/examples/simple.dm', __dir__),
'--output', output_path,
'--target', 'html'
)

assert status.success?, stderr
html = File.read(output_path)
assert_includes html, '<!doctype html>'
assert_includes html, '<h1 id="documark-makes-word-processors-obsolete">DocuMark Makes Word Processors Obsolete!</h1>'
assert_includes html, '<title>A Simple DocuMark Document</title>'
end
end

def test_markdown_target_strips_kramdown_attribute_lists
Dir.mktmpdir('documark-cli-test') do |tmpdir|
input_path = File.join(tmpdir, 'with-attrs.dm')
output_path = File.join(tmpdir, 'out.md')

File.write(input_path, <<~DOC)
!! documark document
!! data
title: "Attr Test"
!! end

# Heading
{: .big}

Paragraph with an inline attribute{: .highlighted}
DOC

_stdout, stderr, status = run_cli(
'process',
'--input', input_path,
'--output', output_path,
'--target', 'markdown'
)

assert status.success?, stderr
output = File.read(output_path)
refute_includes output, '{: .big}'
refute_includes output, '{: .highlighted}'
assert_includes output, '# Heading'
assert_includes output, 'Paragraph with an inline attribute'
end
end

def test_fails_when_frontmatter_data_section_is_missing
_stdout, stderr, status = run_cli(
'process',
'--input', fixture_path('fmerrormissing.dm'),
'--output', '/tmp/out.html',
'--target', 'html'
)

refute status.success?
assert_includes stderr, 'missing frontmatter section'
end

def test_fails_when_data_section_is_unterminated
_stdout, stderr, status = run_cli(
'process',
'--input', fixture_path('fmerrorsimple.dm'),
'--output', '/tmp/out.html',
'--target', 'html'
)

refute status.success?
assert_includes stderr, 'Unterminated data block'
end
end
55 changes: 55 additions & 0 deletions test/config_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require 'tmpdir'
require_relative 'test_helper'
require_relative '../lib/documark/config'

class DocumarkConfigTest < Minitest::Test
def setup
@previous_debug = Documark::Config.send(:debug?)
Documark::Config.send(:debug=, true)
end

def teardown
Documark::Config.send(:debug=, @previous_debug)
end

def test_read_config_parses_mapping_and_expands_default_layout
Dir.mktmpdir('documark-config-test') do |tmpdir|
config_path = File.join(tmpdir, 'documark.conf')
File.write(config_path, <<~CONF)
---
default_layout: ./layouts/default.dml
template_folder: ~/Templates/
---
CONF

parsed = Documark::Config.read_config(config_path)

assert_equal File.expand_path('./layouts/default.dml'), parsed['default_layout']
assert_equal '~/Templates/', parsed['template_folder']
end
end

def test_read_config_requires_mapping
Dir.mktmpdir('documark-config-test') do |tmpdir|
config_path = File.join(tmpdir, 'bad.conf')
File.write(config_path, <<~CONF)
---
- item
- item
---
CONF

error = assert_raises(StandardError) do
Documark::Config.read_config(config_path)
end

assert_equal 'Config file must be a mapping', error.message
end
end

def test_executable_on_path_detects_ruby
assert_equal true, Documark::Config.executable_on_path?('ruby')
end
end
72 changes: 72 additions & 0 deletions test/parser_error_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require_relative 'test_helper'
require_relative '../lib/documark/parser'

class DocumarkParserErrorTest < Minitest::Test
def test_split_documark_sections_rejects_empty_file
error = assert_raises(StandardError) do
Documark::Parser.split_documark_sections('')
end

assert_equal 'File must not be empty', error.message
end

def test_split_documark_sections_raises_for_unterminated_data_block
content = <<~DOC
!! documark document
!! data
title: "Broken"
DOC

error = assert_raises(StandardError) do
Documark::Parser.split_documark_sections(content)
end

assert_equal 'Unterminated data block', error.message
end

def test_split_documark_sections_raises_for_unterminated_layout_block
content = <<~DOC
!! documark document
!! data
title: "Example"
!! end
!! layout
---
container_class: container
---
@media print {}
DOC

error = assert_raises(StandardError) do
Documark::Parser.split_documark_sections(content)
end

assert_equal 'Unterminated layout block', error.message
end

def test_parse_layout_section_requires_front_matter_start_delimiter
error = assert_raises(StandardError) do
Documark::Parser.parse_layout_section("container_class: container\n---\n")
end

assert_equal 'Layout section must start with ---', error.message
end

def test_parse_layout_section_requires_mapping_front_matter
layout = <<~LAYOUT
---
- one
- two
---
@media print {}
LAYOUT

error = assert_raises(StandardError) do
Documark::Parser.parse_layout_section(layout)
end

assert_equal 'Layout front matter must be a mapping', error.message
end
end
Loading