yamd/lib/yamd.rb

425 lines
7.5 KiB
Ruby

# YAMD
# Copyright (C) 2025 kp2pml30
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
module YAMD
class Renderer
attr_reader :buf
def initialize()
@buf = String.new
end
def finalize
@buf.freeze
end
end
class Output
def initialize()
@buf = String.new
#@buf << "Proc.new {\n"
@ind = 0
end
def add_static(s)
return if s.empty?
@buf << ("\t" * @ind) << "str " << s.dump << "\n"
end
def add_expr(e)
@buf << ("\t" * @ind) << "str((#{e}).to_s)"
end
def add_code_nl(c)
c = c.strip
if c.start_with?("}")
@ind -= 1
end
@buf << ("\t" * @ind) << c << "\n"
if c.end_with?("{")
@ind += 1
end
end
def finalize()
#@buf << "}"
@buf.freeze
@buf
end
end
class State
attr_reader :output
def initialize(io)
@io = io
@lineStart = false
@lineNo = 0
nextLine
@output = Output.new
end
def errCtx
{
line: @lineNo
}
end
def eol?()
@line.empty?
end
def eof?()
eol? and @io.eof?
end
def lineStart?()
@lineStart
end
def nextLine()
@lineNo += 1
@lineStart = true
@line = @io.gets()
if @line.nil?
@line = String.new
end
if @line.end_with?("\n")
@line.chop!
end
if @line.end_with?("\r")
@line.chop!
end
@line.freeze
line
end
def line()
@line
end
def consume(cnt)
@lineStart = false
@line = @line[cnt..]
end
end
class Context
attr_reader :indent, :indent_str
def initialize(indent)
@indent = indent
@indent_str = "\t" * indent
self.freeze
end
def indented()
Context.new(@indent + 1)
end
end
def self.parseRaw(state, ctx)
raw = String.new
loop {
if state.eof?
break
end
if not state.line.start_with? ctx.indent_str
# whitespace line == empty line
if state.line =~ /^\s*$/
raw << "\n"
state.nextLine
next
end
# skip comment
if state.line =~ /^\s*#(?:|\s.*)$/
state.nextLine
next
end
break
end
state.consume(ctx.indent) # skip indent
raw << state.line
raw << "\n"
state.nextLine
}
raw.rstrip!
raw.freeze
end
def loopIndentedContent(state, ctx)
loop {
if not state.lineStart?
yield state.line
state.nextLine
next
end
if state.line =~ /^#(?:|\s.*)$/
state.nextLine
#yield ' '
next
end
}
end
def self.parseContent(state, ctx)
strBuf = String.new
flushStrBuf = Proc.new {
if strBuf != ''
if strBuf.end_with?(' ')
strBuf.chop!
end
state.output.add_static(strBuf)
strBuf = String.new
end
}
addWs = Proc.new {
if not strBuf.end_with? ' '
strBuf << ' '
end
}
prevParagraph = false
while not state.eof?
if state.lineStart?
if state.line =~ /^\s*$/
if not prevParagraph
prevParagraph = true
flushStrBuf.()
state.output.add_code_nl("new_line")
end
state.nextLine
next
end
if state.line =~ /^\s*#(?:|\s.*)$/
addWs.()
state.nextLine
next
end
prevParagraph = false
if not state.line.start_with?(ctx.indent_str)
flushStrBuf.()
return
end
state.consume(ctx.indent)
if state.line.start_with?("\t")
raise "unexpected indent at #{state.errCtx}"
end
if state.line.start_with?("#!")
flushStrBuf.()
state.consume(2)
state.output.add_code_nl("" + state.line.strip + " {")
state.nextLine
parseContent(state, ctx.indented)
state.output.add_code_nl("}")
next
elsif state.line.start_with?('#$')
flushStrBuf.()
state.consume(2)
line = state.line.strip
state.nextLine
state.output.add_code_nl("" + line.strip + " {")
state.output.add_code_nl("next " + parseRaw(state, ctx.indented).dump)
state.output.add_code_nl("}")
next
elsif state.line.start_with?("#%")
flushStrBuf.()
state.consume(2)
state.output.add_code_nl(state.line)
state.nextLine
next
elsif state.line.start_with?("#. ") or state.line == "#."
flushStrBuf.()
state.consume(2)
state.output.add_code_nl("list_item {")
parseContent state, ctx.indented
state.output.add_code_nl("}")
next
end
end
prevParagraph = false
if state.eol?
addWs.()
state.nextLine
elsif state.line.start_with?('##')
strBuf << '#'
state.consume 2
elsif state.line.start_with?('}#')
flushStrBuf.()
return
elsif state.line[0] == '#'
state.output.add_static(strBuf)
strBuf = String.new
self.handle_hash state, ctx
else
strBuf << state.line[0]
state.consume 1
end
end
flushStrBuf.()
end
def self.handle_hash(state, ctx)
if state.line.start_with?('#`')
state.consume(2)
code = String.new
while state.line.size > 0 and state.line[0] != '`'
code << state.line[0]
state.consume 1
end
if not state.line.start_with?('`')
raise "unterminated \#` at #{state.errCtx}"
end
state.consume 1
state.output.add_code_nl("inline_code(#{code.dump})")
elsif state.line.start_with?('#{')
state.consume 2
expr = read_balanced(state, ctx)
if not state.line.start_with?('}')
raise "unterminated \#{} at #{state.errCtx}"
end
state.consume 1
state.output.add_expr expr
elsif state.line.start_with?('#(')
state.consume 2
expr = read_balanced(state, ctx)
if not state.line.start_with?(')')
raise "unterminated \#() at #{state.errCtx}"
end
state.consume 1
state.output.add_code_nl "inline_math #{expr.dump}"
elsif state.line.start_with?(/^\#[a-zA-Z_]/)
exprBuf = String.new
state.consume 1
while not state.line.empty?
if state.line[0] == '('
exprBuf << '('
state.consume 1
exprBuf << read_balanced(state, ctx)
raise "unbalanced () at #{state.errCtx}" if state.line[0] != ')'
state.consume 1
exprBuf << ')'
elsif state.line.start_with?('#{')
state.consume 2
exprBuf << " {"
state.output.add_code_nl(exprBuf)
while state.line[0] == ' '
state.consume 1
end
self.parseContent(state, ctx.indented)
raise "unmatched \#{ }\# at #{state.errCtx}" if not state.line.start_with?('}#')
state.consume 2
state.output.add_code_nl("}")
return
else
exprBuf << state.line[0]
state.consume 1
end
end
state.output.add_code_nl exprBuf
# expression
else
raise "unexpected hash at #{state.errCtx}"
end
end
def self.read_balanced(state, ctx)
ret = String.new
stack = []
flipper = {
"(" => ")",
"{" => "}",
}
revFlipper = {}
flipper.each { |k, v|
revFlipper[v] = k
}
while state.line.size > 0 and state.line[0] != '`'
chr = state.line[0]
fromFlipper = flipper[chr]
fromRevFlipper = revFlipper[chr]
if not fromFlipper.nil?
stack.push fromFlipper
end
if not fromRevFlipper.nil?
return ret.freeze if stack.empty?
have = stack.pop
if have != chr
raise "unbalanced #{have}, got #{chr}"
end
end
ret << chr
state.consume 1
end
ret.freeze
end
end