# 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 . 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