おれおれマークダウンの整理

おれおれマークダウンは深く考えずにとりあえず作る方針でぐちゃぐちゃになっていたので整理しました。マークダウンは上から読んでいって、今はコードブロックの中とかの状態に依存するから、Stateパターンがぴったりの案件ですよね。GoF本を引っ張り出してきて、めんどくさいのでほとんど読まずに実装しましたが、けっこうスッキリ書けたと思います。まあ、変更前がひどかったのもありますが。

クラス図にするとこんな感じ。State<|--Listとスペースを開けないで書くとistクラスがStateの横に書かれました。PlantUMLの書式を覚えていないからな。

State <|-- BlockState
BlockState <|-- PlantUML
BlockState <|-- Code
State <|-- Normal
State <|-- List
State <|-- Quote
State <|-- Paragraph
State <|-- Footnote
StateBlockStatePlantUMLCodeNormalListQuoteParagraphFootnote

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!('&', "&amp;")
    line.gsub!('<', "&lt;")
    line.gsub!('>', "&gt;")
    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!('&', "&amp;")
    s.gsub!('<', "&lt;")
    s.gsub!('>', "&gt;")
    "<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!('&', "&amp;")
    s.gsub!('<', "&lt;")
    s.gsub!('>', "&gt;")
    "<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!('&', "&amp;")
      line.gsub!('<', "&lt;")
      line.gsub!('>', "&gt;")
      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

コメント

このブログの人気の投稿

五十音配列付き新下駄配列

WSLでの親指シフトはどうやらMozcで実現可能と気がつくまで

親指シフト新下駄配列の可能性