おれおれマークダウンの整理
おれおれマークダウンは深く考えずにとりあえず作る方針でぐちゃぐちゃになっていたので整理しました。マークダウンは上から読んでいって、今はコードブロックの中とかの状態に依存するから、Stateパターンがぴったりの案件ですよね。GoF本を引っ張り出してきて、めんどくさいのでほとんど読まずに実装しましたが、けっこうスッキリ書けたと思います。まあ、変更前がひどかったのもありますが。
クラス図にするとこんな感じ。State<|--List
とスペースを開けないで書くとistクラスがStateの横に書かれました。PlantUMLの書式を覚えていないからな。
State <|-- BlockState
BlockState <|-- PlantUML
BlockState <|-- Code
State <|-- Normal
State <|-- List
State <|-- Quote
State <|-- Paragraph
State <|-- Footnote
BlockStateの並びに1つクラスを入れるときれいな感じもするけど、特にやることも無いのでこうなりました。
行頭が-
で始まっていたらLine Stateなので、もし前の状態が違ったらfinishして状態変更してstartするだけです。change_state
なのに必ず変更するとは限らないので名前が不適切ですね。でも、いいのが思いつかないので。
class Context
def initialize
...
@list = List.new
@normal = Normal.new
@state = @normal
end
def change_state(state)
if @state != state then
@state.finish
@state = state
@state.start
end
end
...
end
class State
def start
printf self.class::HEADER
end
def finish
if self.class::FOOTER.length > 0 then
printf "#{self.class::FOOTER}\n"
end
end
def block?
false
end
end
class List < State
HEADER = "<ul>"
FOOTER = "</ul>"
end
Listクラスは定数2つだけです。親クラスのメソッドで子クラスの定数を使おうとするとself.class::HEADER
みたいな書き方になるんですね。別に定数じゃなくてもよかった気もするけど書いちゃったからね。
仮想関数を書こうとして、Rubyには無いのに気が付きました。あれはそういう関数を持っている型の宣言だから、型の無いRubyには必要ないのか。
まだ足りない機能も多いけど、それらはおいおい。
変更後のソース
require 'optparse'
require 'open3'
class Context
def initialize
@plant_uml = PlantUML.new
@code = Code.new
@list = List.new
@quote = Quote.new
@paragraph = Paragraph.new
@footnote = Footnote.new
@normal = Normal.new
@state = @normal
end
def change_state(state)
if @state != state then
@state.finish
@state = state
@state.start
end
end
def block?
@state.block?
end
def process(line)
@state.process(line)
end
def plant_uml
change_state(@plant_uml)
end
def code
change_state(@code)
end
def normal
change_state(@normal)
end
def list(content)
change_state(@list)
printf "<li>" << parse_line(content) << "</li>"
end
def quote(content)
change_state(@quote)
puts parse_line(content)
end
def paragraph(line)
change_state(@paragraph)
printf parse_line(line)
end
def footnote(tag, content)
change_state(@footnote)
printf "<li><a id='footnote_#{tag}'>[#{tag}]</a>:" << parse_line(content) << "</li>"
end
end
class State
def start
printf self.class::HEADER
end
def finish
if self.class::FOOTER.length > 0 then
printf "#{self.class::FOOTER}\n"
end
end
def block?
false
end
end
class BlockState < State
def block?
true
end
end
class PlantUML < BlockState
def initialize
@exec_str = ""
end
def start
initialize
end
def process(line)
@exec_str = @exec_str << line << "\n"
end
def finish
Open3.popen3("java -jar D:/app/plantuml.jar -pipe -svg") do |i, o, e, w|
i.write @exec_str
i.close
o.each do |l| puts l end
e.each do |l| printf("<!-- stderr: %s -->\n", l) end
printf("<!-- thread: %s -->\n", w.value)
end
end
end
class Code < BlockState
HEADER = '<pre style="overflow-x: scroll;padding: 1px 1px 1px 1px;border:1px solid black"><code>'
FOOTER = "</code></pre>"
def process(line)
line.gsub!('&', "&")
line.gsub!('<', "<")
line.gsub!('>', ">")
puts line
end
end
class Normal < State
HEADER = ""
FOOTER = ""
end
class List < State
HEADER = "<ul>"
FOOTER = "</ul>"
end
class Quote < State
HEADER = "<blockquote>"
FOOTER = "</blockquote>"
end
class Paragraph < State
HEADER = "<p>"
FOOTER = "</p>"
end
class Footnote < State
HEADER = "<ul>"
FOOTER = "</ul>"
end
mathjax = <<RUBY
<script>
MathJax = {
chtml: {
displayAlign: "left",
},
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>
RUBY
header1 = <<-RUBY
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script>
MathJax = {
chtml: {
displayAlign: "left",
},
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>
<title>
RUBY
header2 = <<RUBY
</title>
</head>
<body>
<h1>
RUBY
header3 = <<~RUBY
</h1>
RUBY
footer = <<RUBY
</body>
</html>
RUBY
opt = OptionParser.new
type = :html
opt.on('-b', '--blog', 'blog type output') do |v|
type = :blog
end
filename = ""
opt.on('-i VAL', '--input', 'input filename') do |v|
filename = v
end
opt.parse!(ARGV)
def parse_line(line)
line.gsub!(/`([^`]*)`/) {
s = $1
s.gsub!('&', "&")
s.gsub!('<', "<")
s.gsub!('>', ">")
"<code>#{s}</code>"
}
line.gsub!(/\[([^\]]*)\]\(([^\)]*)\)/) {
"<a href='#{$2}'>#{$1}</a>"
}
line.gsub!(/\[\^([^\]]*)\]/) {
"<a href='#footnote_#{$1}'><sup>[#{$1}]</sup></a>"
}
line
end
File.open(filename, "r") do |i|
line_num = 1
context = Context.new
i.each_line do |line|
line.chomp!
if line_num == 1 then
if type == :html then
puts header1.chomp + line + header2.chomp + line + header3
else
puts line
puts mathjax
end
elsif line.match(/^```exec plantuml/) then
context.plant_uml
elsif line.match(/^```/) then
if context.block? then
context.normal
else
context.code
end
elsif context.block? then
context.process(line)
elsif md = line.match(/^#####([^#]*)/) then
title = md[1].strip
printf("<h6>%s</h6>\n", title)
elsif md = line.match(/^####([^#]*)/) then
title = md[1].strip
printf("<h5>%s</h5>\n", title)
elsif md = line.match(/^###([^#]*)/) then
title = md[1].strip
printf("<h4>%s</h4>\n", title)
elsif md = line.match(/^##([^#]*)/) then
title = md[1].strip
printf("<h3>%s</h3>\n", title)
elsif md = line.match(/^#([^#]*)/) then
title = md[1].strip
printf("<h2>%s</h2>\n", title)
elsif md = line.match(/^[ \t]*$/) then
context.normal
elsif md = line.match(/^> (.*)/) then
context.quote(md[1])
elsif md = line.match(/^- (.*)/) then
context.list(md[1])
elsif md = line.match(/^\[\^([^\]]*)\]:(.*)/) then
context.footnote(md[1], md[2])
else
context.paragraph(line)
end
line_num = line_num.succ
end
end
if type == :html then
puts(footer)
end
変更前のソース
require 'optparse'
require 'open3'
mathjax = <<RUBY
<script>
MathJax = {
chtml: {
displayAlign: "left",
},
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>
RUBY
header1 = <<-RUBY
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script>
MathJax = {
chtml: {
displayAlign: "left",
},
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>
<title>
RUBY
header2 = <<RUBY
</title>
</head>
<body>
<h1>
RUBY
header3 = <<~RUBY
</h1>
RUBY
footer = <<RUBY
</body>
</html>
RUBY
code_header = <<~RUBY
<pre style="overflow-x: scroll;padding: 1px 1px 1px 1px;border:1px solid black"><code>
RUBY
code_footer = <<~RUBY
</code></pre>
RUBY
list_header = "<ul>"
list_footer = "</ul>"
quotation_header = "<blockquote>"
quotation_footer = "</blockquote>"
paragraph_start = "<p>"
paragraph_end = "</p>"
opt = OptionParser.new
type = :html
opt.on('-b', '--blog', 'blog type output') do |v|
type = :blog
end
filename = ""
opt.on('-i VAL', '--input', 'input filename') do |v|
filename = v
end
opt.parse!(ARGV)
def parse_line(line)
line.gsub!(/`([^`]*)`/) {
s = $1
s.gsub!('&', "&")
s.gsub!('<', "<")
s.gsub!('>', ">")
"<code>#{s}</code>"
}
line.gsub!(/\[([^\]]*)\]\(([^\)]*)\)/) {
"<a href='#{$2}'>#{$1}</a>"
}
line.gsub!(/\[\^([^\]]*)\]/) {
"<a href='#footnote_#{$1}'><sup>[#{$1}]</sup></a>"
}
return line
end
File.open(filename, "r") do |i|
line_num = 1
mode = :normal
exec_str = ""
i.each_line do |line|
line.chomp!
if line_num == 1 then
if type == :html then
puts header1.chomp + line + header2.chomp + line + header3
else
puts line
puts mathjax
end
elsif line.match(/^```exec rscript/) then
if mode == :normal then
mode = :exec_rscript
exec_str = "Rscript --vanilla -e '"
elsif mode == :exec_rscript
mode = :normal
exec_str = exec_str << "'"
#exec_str = "Rscript --vanilla -e '1 + 1'"
#exec_str = "R --vanilla -e '1+1'"
#exec_str = "Rscript --vanilla -e '1 + 1'"
#exec_str = "echo 'test'"
printf("<!-- exec_str: %s -->\n", exec_str)
Open3.popen3(exec_str) do |i, o, e, w|
i.write ""
i.close
o.each do |l| puts l end
e.each do |l| printf("<!-- stderr: %s -->\n", l) end
printf("<!-- thread: %s -->\n", w.value)
end
end
elsif line.match(/^```exec plantuml/) then
if mode == :normal then
mode = :exec_plantuml
exec_str = ""
elsif mode == :exec_plantuml
mode = :normal
Open3.popen3("java -jar D:/app/plantuml.jar -pipe -svg") do |i, o, e, w|
i.write exec_str
i.close
o.each do |l| puts l end
e.each do |l| printf("<!-- stderr: %s -->\n", l) end
printf("<!-- thread: %s -->\n", w.value)
end
end
elsif line.match(/^```/) then
if mode == :normal then
mode = :code
printf code_header.chomp
elsif mode == :paragraph
mode = :code
puts paragraph_end
printf code_header.chomp
elsif mode == :code
mode = :normal
puts code_footer
end
elsif mode == :exec_rscript
exec_str = exec_str << line
elsif mode == :exec_plantuml
exec_str = exec_str << line << "\n"
elsif mode == :code
line.gsub!('&', "&")
line.gsub!('<', "<")
line.gsub!('>', ">")
puts line
elsif md = line.match(/^#####([^#]*)/) then
title = md[1].strip
printf("<h6>%s</h6>\n", title)
elsif md = line.match(/^####([^#]*)/) then
title = md[1].strip
printf("<h5>%s</h5>\n", title)
elsif md = line.match(/^###([^#]*)/) then
title = md[1].strip
printf("<h4>%s</h4>\n", title)
elsif md = line.match(/^##([^#]*)/) then
title = md[1].strip
printf("<h3>%s</h3>\n", title)
elsif md = line.match(/^#([^#]*)/) then
title = md[1].strip
printf("<h2>%s</h2>\n", title)
elsif md = line.match(/^[ \t]*$/) then
if mode == :paragraph then
mode = :normal
puts paragraph_end
elsif mode == :quotation then
mode = :normal
puts quotation_footer
elsif mode == :list
mode = :normal
puts list_footer
elsif mode == :footnote
mode = :normal
puts list_footer
end
elsif md = line.match(/^> (.*)/) then
if mode == :normal then
mode = :quotation
puts quotation_header
line = parse_line(md[1])
puts line
elsif mode == :paragraph
mode = :quotation
puts paragraph_end
puts quotation_header
line = parse_line(md[1])
puts line
elsif mode == :list
mode = :quotation
puts list_end
puts quotation_header
line = parse_line(md[1])
puts line
elsif mode == :quotation
line = parse_line(md[1])
puts line
else
raise "不正な状態遷移です。"
end
elsif md = line.match(/^- (.*)/) then
if mode == :normal then
mode = :list
printf list_header.chomp
line = parse_line(md[1])
printf "<li>#{line}</li>"
elsif mode == :paragraph
mode = :list
puts paragraph_end
printf list_header.chomp
line = parse_line(md[1])
printf "<li>#{line}</li>"
elsif mode == :quotation
mode = :list
puts quotation_footer
printf list_header.chomp
line = parse_line(md[1])
printf "<li>#{line}</li>"
elsif mode == :footnote
mode = :list
puts list_footer
printf list_header.chomp
line = parse_line(md[1])
printf "<li>#{line}</li>"
elsif mode == :list
line = parse_line(md[1])
printf "<li>#{line}</li>"
else
raise "不正な状態遷移です。"
end
elsif md = line.match(/^\[\^([^\]]*)\]:(.*)/) then
if mode == :normal then
printf list_header.chomp
elsif mode == :paragraph
puts paragraph_end
printf list_header.chomp
elsif mode == :quotation
puts quotation_footer
printf list_header.chomp
elsif mode == :list
puts list_footer
printf list_header.chomp
elsif mode == :footnote
else
raise "不正な状態遷移です。"
end
mode = :footnote
line = "<li><a id='footnote_#{md[1]}'>[#{md[1]}]</a>:" << parse_line(md[2]) << "</li>"
printf line
else
line = parse_line(line)
if mode == :normal then
mode = :paragraph
printf paragraph_start.chomp
printf line
elsif mode == :paragraph
printf line
end
end
line_num = line_num.succ
end
end
if type == :html then
puts(footer)
end
コメント
コメントを投稿