382 lines
6.6 KiB
Ruby
382 lines
6.6 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 { |__renderer|\n"
|
|
|
|
@ind = 1
|
|
end
|
|
|
|
def add_static(s)
|
|
return if s.empty?
|
|
|
|
@buf << ("\t" * @ind) << "__renderer.str " << s.dump << "\n"
|
|
end
|
|
|
|
def add_expr(e)
|
|
@buf << ("\t" * @ind) << "__renderer.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("__renderer.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?("#!")
|
|
state.consume(2)
|
|
|
|
state.output.add_code_nl("__renderer." + state.line.strip + " {")
|
|
state.nextLine
|
|
parseContent(state, ctx.indented)
|
|
state.output.add_code_nl("}")
|
|
|
|
next
|
|
elsif state.line.start_with?('#$')
|
|
state.consume(2)
|
|
|
|
line = state.line.strip
|
|
state.nextLine
|
|
state.output.add_code_nl("__renderer." + 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?("#. ")
|
|
flushStrBuf.()
|
|
state.consume(3)
|
|
|
|
state.output.add_code_nl("__renderer.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[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("__renderer.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 "__renderer.inline_math #{expr.dump}"
|
|
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
|