initial commit
This commit is contained in:
commit
46af3c6fa9
18 changed files with 1363 additions and 0 deletions
382
lib/yamd.rb
Normal file
382
lib/yamd.rb
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
# 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
|
||||
60
lib/yamd/code.rb
Normal file
60
lib/yamd/code.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# 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/>.
|
||||
|
||||
require 'erb'
|
||||
|
||||
module YAMD::Code
|
||||
HIGHLIGHTS = {
|
||||
/\b(?:[A-Z][a-zA-Z\-_0-9]*)\b*/ => 'type',
|
||||
/#(\s|$)[^\n]*/ => 'comment',
|
||||
/\b(?:open|final|class|fn|let|var|if|else|while|loop|return|namespace|new)\b/ => 'kw',
|
||||
/\b(?:null|undefined|true|false|this|self)\b/i => 'const',
|
||||
/[:,;()\[\]{}<>]/ => 'punct',
|
||||
/0|(0x[0-9a-fA-F]+)|([1-9][0-9]*(\.[0-9]+)?)/ => 'number',
|
||||
/"(?:\\.|[^\\"])*"/ => 'str',
|
||||
/'(?:\\.|[^\\'])*'/ => 'str',
|
||||
}
|
||||
|
||||
def self.highlight(txt, highlights=HIGHLIGHTS)
|
||||
idx = 0
|
||||
res = String.new
|
||||
old_idx = 0
|
||||
flush = Proc.new {
|
||||
next if old_idx == idx
|
||||
res << ERB::Util.html_escape(txt[old_idx...idx])
|
||||
old_idx = idx
|
||||
}
|
||||
while idx < txt.size
|
||||
pattern = nil
|
||||
highlights.each { |pat, klass|
|
||||
match = pat.match txt, idx
|
||||
next if not match
|
||||
next if match.offset(0)[0] != idx
|
||||
pattern = pat
|
||||
flush.()
|
||||
res << "<span class=\"highlight-#{klass}\">" << ERB::Util.html_escape(match[0]) << "</span>"
|
||||
idx += match[0].size
|
||||
old_idx = idx
|
||||
break
|
||||
}
|
||||
if pattern.nil?
|
||||
idx += 1
|
||||
end
|
||||
end
|
||||
flush.()
|
||||
res.freeze
|
||||
end
|
||||
end
|
||||
19
lib/yamd/version.rb
Normal file
19
lib/yamd/version.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# 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
|
||||
VERSION = "0.0.1"
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue