This commit is contained in:
Yessiest 2025-03-07 23:11:28 +00:00
parent 41680e45e1
commit 9ac12573a3
5 changed files with 176 additions and 9 deletions

170
bin/mmmdpp Executable file
View File

@ -0,0 +1,170 @@
#!/bin/ruby
# frozen_string_literal: true
require 'io/console/size'
require 'optionparser'
require 'json'
require 'mmmd'
class ParserError < StandardError
end
class OptionNavigator
def initialize
@options = {}
end
# Read a definition
# @param define [String]
def read_definition(define)
define.split(";").each do |part|
locstring, _, value = part.partition(":")
locstring = deconstruct(locstring.strip)
assign(locstring, JSON.parse(value))
end
end
attr_reader :options
private
def check_unescaped(str, index)
return true if index.zero?
reverse_index = index - 1
count = 0
while str[reverse_index] == "\\"
break if reverse_index.zero?
count += 1
reverse_index -= 1
end
count.even?
end
def find_unescaped(str, pattern, index)
found = str.index(pattern, index)
return nil unless found
until check_unescaped(str, found)
index = found + 1
found = str.index(pattern, index)
return nil unless found
end
found
end
def deconstruct(locstring)
parts = []
buffer = ""
part = nil
until locstring.empty?
case locstring[0]
when '"'
raise ParserError, 'separator missing' unless buffer.empty?
closepart = find_unescaped(locstring, '"', 1)
raise ParserError, 'unclosed string' unless closepart
buffer = locstring[0..closepart]
part = buffer[1..-2]
locstring = locstring[closepart + 1..]
when '.'
parts.append(part)
buffer = ""
part = nil
locstring = locstring[1..]
when '['
raise ParserError, 'separator missing' unless buffer.empty?
closepart = find_unescaped(locstring, ']', 1)
raise ParserError, 'unclosed index' unless closepart
buffer = locstring[0..closepart]
part = locstring[1..-2].to_i
locstring = locstring.delete_prefix(buffer)
else
raise ParserError, 'separator missing' unless buffer.empty?
buffer = locstring.match(/^[\w_]+/)[0]
part = buffer.to_sym
locstring = locstring.delete_prefix(buffer)
end
end
parts.append(part) if part
parts
end
def assign(keys, value)
current = @options
while keys.length > 1
current_key = keys.shift
unless current[current_key]
next_key = keys.first
case next_key
when Integer
current[current_key] = []
when String
current[current_key] = {}
when Symbol
current[current_key] = {}
end
end
current = current[current_key]
end
current[keys.shift] = value
end
end
return unless $PROGRAM_NAME == __FILE__
options = {
include: [],
nav: OptionNavigator.new
}
parser = OptionParser.new do |opts|
opts.banner = "Usage: mmmdpp [OPTIONS] (input|-) (output|-)"
opts.on("-r", "--renderer [STRING]", String,
"Specify renderer to use for this document") do |renderer|
options[:renderer] = renderer
end
opts.on("-i", "--include [STRING]", String,
"Script to execute before rendering.\
May be specified multiple times.") do |inc|
options[:include].append(inc)
end
opts.on("-o", "--option [STRING]", String,
"Add option string. Can be repeated. Format: <key>: <JSON value>\n"\
"<key>: (<\"string\">|<symbol>|<[integer]>)"\
"[.(<\"string\"|<symbol>|<[integer]>[...]]\n"\
"Example: \"style\".\"CodeBlock\".literal.[0]: 50") do |value|
options[:nav].read_definition(value) if value
end
end
parser.parse!
unless ARGV[1]
warn parser.help
exit 1
end
Renderers = {
"HTML" => -> { ::MMMD::Renderers::HTML },
"Plainterm" => -> { ::MMMD::Renderers::Plainterm }
}.freeze
options[:include].each { |name| Kernel.load(name) }
renderer_opts = options[:nav].options
renderer_opts["hsize"] ||= IO.console_size[1]
input = ARGV[0] == "-" ? $stdin.read : File.read(ARGV[0])
output = ARGV[1] == "-" ? $stdout : File.open(ARGV[1], "w")
doc = MMMD.parse(input)
rclass = Renderers[options[:renderer] || "PlainTerm"]
raise StandardError, "unknown renderer: #{options[:renderer]}" unless rclass
renderer = rclass.call.new(doc, renderer_opts)
output.puts(renderer.render)
output.close

View File

@ -6,6 +6,6 @@ module MMMD
# Renderers from Markdown to expected output format
module Renderers
autoload :HTML, 'renderers/html'
autoload :PlainTerm, 'renderers/plainterm'
autoload :Plainterm, 'renderers/plainterm'
end
end

View File

@ -229,7 +229,9 @@ module MMMD
text = if element.children.empty?
element.content
else
literal = @mapping[element.class.name][:inline] || literaltext
literal = @mapping[element.class.name]
&.fetch(:inline, false) ||
literaltext
element.children.map do |child|
_render(child, options, inline: inline,
level: level,

View File

@ -396,7 +396,7 @@ module MMMD
@effect_priority = style_manager.effect_priority
@effects = @effect_priority.to_a.sort_by(&:last).map(&:first)
@options = options
@options[:hsize] ||= 80
@options["hsize"] ||= 80
end
# Return rendered text
@ -430,7 +430,7 @@ module MMMD
element_style = @style[element.class.name]
return text unless element_style
hsize = 80 - (4 * level)
hsize = @options["hsize"] - (4 * level)
text = wordwrap(text, hsize) if modeswitch
params = element_style.dup
params[:hsize] = hsize

View File

@ -1,5 +0,0 @@
#!/bin/ruby
# frozen_string_literal: true
require 'optionparser'