---
title: PukiWiki形式のテキストをMarkdown形式のファイルに変換する
keywords: [Ruby, JRuby, Markdown, PukiWiki]
author: aterai
pubdate: 2012-09-27
description: PukiWiki形式のテキストをMarkdown形式のファイルに変換するRubyスクリプトを作成する
---
#contents
* Summary [#h1b34392]
このサイトで使用している`PukiWiki`形式のテキストを`Markdown`形式のファイルに変換するサンプルプログラムを紹介しています。
- 以下のソースコードは、[http://jp.rubyist.net/magazine/?0010-CodeReview Rubyist Magazine - あなたの Ruby コードを添削します 【第 1 回】 pukipa.rb] より引用、改変
- 注: このサイトの`Swing`ディレクトリを以下に存在する記事を変換することだけが目的なので、制限多数
> jruby -E UTF-8 p2m.rb .\wiki
* ソースコード [#i99db5ec]
- [https://ateraimemo.com/data/jruby/p2m.rb p2m.rb]
- [https://gist.github.com/aterai/5dedec2b950b8b317394 PukiWiki 2 Markdown forked from: http://magazine.rubyist.net/?0010-CodeReview]
#code{{
# -*- mode: ruby; encoding: utf-8 -*-
require 'uri'
require 'date'
require 'yaml'
module HTMLUtils
ESC = {
'&' => '&',
'"' => '"',
'<' => '<',
'>' => '>'
}
def escape(str)
table = ESC # optimize
str.gsub(/[&"<>]/u) {|s| table[s]}
end
CODE = {
'<' => '<',
'>' => '>',
'&' => '&'
}
def code_escape(str)
table = CODE
str.gsub(/[<>&]/u) {|s| table[s]}
end
URIENC = {
'(' => '%28',
')' => '%29',
' ' => '%20'
}
def uri_encode(str)
table = URIENC
str.gsub(/[\(\) ]/u) {|s| table[s]}
end
def urldecode(str)
str.gsub(/[A-F\d]{2}/u) {|x| [x.hex].pack('C*')}
end
end
class PukiWikiParser
include HTMLUtils
def initialize()
@h_start_level = 2
end
def filename(pw_name)
decoded_name = HTMLUtils.urldecode(pw_name).sub(/\:/, '_').downcase.split("/").last
name = decoded_name.sub(/\.txt\z/, '.md')
if @timestamp.nil? || @timestamp.size===0
return name
else
return "#{@timestamp}-#{name}"
end
end
def has_pubdate
@timestamp != ''
end
def to_md(src, page_names, page, base_uri = 'https://ateraimemo.com/', suffix= '/')
@page_names = page_names
@base_uri = base_uri
@page = page.sub(/\ASwing\/(.+)\.txt\z/) { $1 }
@pagelist_suffix = suffix
@inline_re = nil
@timestamp = ''
head = []
buf = []
@FRONT_MATTER_REGEX ||= %r<
\A---[\r\n](.*?)[\r\n]---[\r\n](.*)
>mx
if @FRONT_MATTER_REGEX =~ src.lstrip then
frontmatter = $1
body = $2
# yaml = YAML.load(frontmatter)
# yaml['layout'] = 'post'
# yaml['category'] = 'swing'
# yaml['folder'] = @page
# yaml['comments'] = true
# if yaml.key?('noindex') then
# return ''
# end
# @timestamp = yaml['pubdate'].strftime('%Y-%m-%d')
# head.push(yaml.to_yaml.rstrip)
head.push("---")
head.push("layout: post")
#head.push("category: swing")
#head.push("folder: #{@page}")
heads = frontmatter.rstrip.split(/\r?\n/).map {|line| line.chomp }
while heads.first
case heads.first
when /\Apubdate: /
pubdate = heads.shift
head.push pubdate
@timestamp = DateTime.parse(pubdate.sub(/\Apubdate: /, '')).strftime('%Y-%m-%d')
when /\Anoindex: /
@timestamp = ''
return ''
else
head.push heads.shift
end
end
head.push("comments: true")
head.push("---\n")
else
body = src
end
lines = body.rstrip.split(/\r?\n/).map {|line| line.chomp }
while lines.first
case lines.first
when ''
buf.push lines.shift
when /\A----/
lines.shift
buf.push '- - - -' #hr
when /\A\*/
buf.push parse_h(lines.shift)
when /\A\#code.*\{\{/
buf.concat parse_pre2(take_multi_block(lines))
when /\A\#.+/
bp = parse_block_plugin(lines.shift)
buf.push bp unless bp.nil?
when /\A\s/
buf.concat parse_pre(take_block(lines, /\A\s/))
when /\A\/\//
#buf.concat parse_comment(take_block(lines, /\A\/\//))
take_block(lines, /\A\/\//)
when /\A>/
buf.concat parse_quote(take_block(lines, /\A>/))
when /\A-/
buf.concat parse_list('ul', take_list_block(lines))
when /\A\+/
buf.concat parse_list('ol', take_block(lines, /\A\+/))
when /\A:/
buf.concat parse_dl(take_block(lines, /\A:/))
else
buf.concat parse_p(take_block(lines, /\A(?![*\s>:\-\+\#]|----|\z)/))
end
end
buf.join("\n")
head.join("\n").concat(buf.join("\n"))
end
private
def take_block(lines, marker)
buf = []
until lines.empty?
break unless marker =~ lines.first
if /\A\/\// =~ lines.first then
lines.shift
else
buf.push lines.shift.sub(marker, '')
end
end
buf
end
def take_multi_block(lines)
buf = []
until lines.empty?
l = lines.shift
break if /\A\}\}\z/ =~ l
next if /\A\#code.*\z/ =~ l
buf.push l
end
buf
end
def parse_h(line)
level = @h_start_level + (line.slice(/\A\*{1,4}/).length - 1)
h = "#"*level
# content = line.sub(/\A\*+/, '')
##content = line.gsub(/\A\*+(.+) \[#\w+\]$/) { $1 }
#"<h#{level}>#{parse_inline(content)}</h#{level}>"
content = line.gsub(/\A\*+(.+)$/) { $1.gsub(/ +\[#\w+\]$/, "") }
"#{h} #{parse_inline(content).strip}"
end
def take_list_block(lines)
marker = /\A-/
buf = []
codeblock = false
listblock = false
until lines.empty?
#break unless marker =~ lines.first
#while lines.first
case lines.first
when /\A\/\//
lines.shift
when /\A----/
if codeblock then
buf.push "<!-- comment line for breaking list -->"
end
break
when marker
l = lines.shift
#puts l
buf.push l #lines.shift #.sub(marker, '')
listblock = true
codeblock = false
#puts buf.last
# when /\A$/
# buf.push lines.shift
when /\A\s/
buf.push '#' + lines.shift
codeblock = true
listblock = false
when /\A\#code.*\{\{/
array = []
until lines.empty?
l = lines.shift
array.push l
break if /\A\}\}\z/ =~ l
end
buf.concat array
codeblock = true
listblock = false
else
if listblock then
buf.push "<!-- comment line for breaking list -->"
break
elsif codeblock then
buf.push lines.shift
else
break
end
end
end
buf
end
def parse_list(type, lines)
marker = ((type == 'ul') ? /\A-+/ : /\A\++/)
parse_list0(type, lines, marker)
end
def parse_list0(type, lines, marker)
buf = []
level = 0
blockflag = false
until lines.empty?
line = lines.shift.strip
aaa = line.slice(marker)
if aaa then
level = aaa.length - 1
line = line.sub(marker,'').strip
#else
# level = 0
end
h = " "*level
s = (type == 'ul') ? '-' : '1.'
if line.empty? then
#buf.push line
elsif line.start_with?('#code') then
hh = " "*(level+1)
array = take_multi_block(lines).map{|ll| hh + code_escape(ll)}
line = array.shift.strip
buf.concat [hh, %Q|#{hh}<pre class="prettyprint"><code>|.concat(line), array.join("\n"), "</code></pre>"]
blockflag = false
elsif line.start_with?('#') then
unless blockflag then
blockflag = true
buf.push h
end
x = "\t"*2
line = code_escape(line.sub(/\A\#\s/, ''))
buf.push "#{h}#{x}#{line}"
elsif line.start_with?('<!--') then
buf.concat ['', line]
break
else
blockflag = false
#puts "#{level}: #{line}"
buf.push "#{h}#{s} #{parse_inline(line)}"
end
end
buf
end
def parse_dl(lines)
buf = ["<dl>"]
lines.each do |line|
dt, dd = *line.split('|', 2)
buf.push "<dt>#{parse_inline(dt)}</dt>"
buf.push "<dd>#{parse_inline(dd)}</dd>" if dd
end
buf.push "</dl>"
buf
end
def parse_quote(lines)
["<blockquote><p>", lines.join("\n"), "</p></blockquote>"]
end
def parse_pre(lines)
#[%Q|#{lines.map {|line| "\t".concat(line) }.join("\n")}|, %Q|{:class="prettyprint"}|]
lines.map{|line| "\t".concat(line)} #.join("\n")
end
def parse_pre2(lines)
array = lines.map{|line| code_escape(line)}
array[0] = %Q|<pre class="prettyprint"><code>|.concat(array[0])
[array.join("\n"), "</code></pre>"]
end
def parse_pre3(lines)
["```java", lines.join("\n"), "```"]
end
def parse_comment(lines)
["<!-- #{lines.map {|line| escape(line) }.join("\n")}",
' -->']
end
def parse_p(lines)
lines.map {|line| parse_inline(line)}
end
def parse_inline(str)
str = str.gsub(/%%(?!%)((?:(?!%%).)*)%%/) { ['~~', $1, '~~'].join() } #nest: <del>[http://example.com/ example]</del>, <strike>
@inline_re ||= %r~
&(?<plugin>[0-9A-Za-z_]+)(?:\((?<parameter>[^\)]*)\))?(?:{(?<inline>[^}]+)})?; # inline plugin ex. &new(...){...};
| \[\[(?<bracket>[^>]+)>?(?<uri>[^\]]*)\]\] # bracket, URI
| \[(?<uri>https?://\S+)\s+(?<label>[^\]]+)\] # URI, label
| (?<uri>#{URI.regexp('http')}) # URI autolink
| KBD\{(?<kbd>[^\}]+)\} # <kbd>
| ``(?!`)(?<code>(?:(?!``).)*)`` # <code>
| \'\'(?!\')(?<strong>(?:(?!\'\').)*)\'\' # <strong>
#| %%(?!%)(?<del>(?:(?!%%).)*)%% # <del>, <strike>
~x
str.gsub(@inline_re) {
case
when $~[:plugin] then parse_inline_plugin($~)
when $~[:bracket] then a_href($~[:uri].strip, $~[:bracket], 'pagelink')
when $~[:label] then a_href($~[:uri].strip, $~[:label], '')
when $~[:uri] then a_href($~[:uri].strip, $~[:uri], '')
when $~[:kbd] then ['<kbd>', $~[:kbd], '</kbd>'].join()
when $~[:code] then ['<code>', $~[:code], '</code>'].join()
when $~[:strong] then ['**', $~[:strong], '**'].join()
#when $~[:del] then ['~~', $~[:del], '~~'].join()
else
raise 'must not happen'
end
}
end
def parse_inline_plugin(mtch) #plugin, para, inline)
plugin = mtch[:plugin].strip
parameter = mtch[:parameter]
inline = mtch[:inline]
case plugin
when 'jnlp'
%Q|{% jnlp %}|
when 'jar2'
%Q|{% jar %}|
when 'zip'
%Q|{% src %}\n- {% svn %}|
when 'ref'
parameter
when 'new'
inline.strip
when 'user'
%Q|*#{parameter}*|
else
plugin
end
end
def parse_block_plugin(line)
@plugin_re = %r<
\A\#([^\(]+)\(?([^\)]*)\)?
>x
args = []
line.gsub(@plugin_re) {
args.push $1
args.push $2 #.slice(",")
}
#buf = []
case args.first
when 'download'
%Q<{% download #{args[1]} %}>
when 'ref'
%Q<>
when 'comment'
nil
else
''
end
end
def a_href(uri, label, cssclass)
str = label.strip
if(cssclass.casecmp('pagelink')==0) then
if(uri.size===0) then
%Q<[#{str}](#{@base_uri}#{escape(str)}.html)>
else
%Q<[#{str}](#{@base_uri}#{escape(uri.strip)}.html)>
end
else
#%Q<[#{str}](#{URI.escape(uri.strip)})>
%Q<[#{str}](#{uri_encode(uri.strip)})>
end
end
def autolink_re
Regexp.union(* @page_names.reject {|name| name.size <= 3 })
end
def page_uri(page_name)
"#{@base_uri}#{urldecode(page_name)}#{@pagelist_suffix}"
end
end
def main
include HTMLUtils
srcpath = ARGV[0]
tgtpath = ARGV[1]
srcmask = ARGV[2] ? ARGV[2] : "*.txt"
if File.exist?(srcpath)
parser = PukiWikiParser.new()
Dir::glob("#{srcpath}/#{srcmask}").each {|f|
page_names = []
fname = File.basename(f)
tbody = File.read(f)
buf = parser.to_md(tbody, page_names, HTMLUtils.urldecode(fname))
tmp = parser.filename(fname)
if !tmp.include?("_") and parser.has_pubdate then
nname = [tgtpath, tmp].join('/')
puts tmp
outf = open(nname, "wb")
outf.puts(buf)
outf.close()
end
}
else
puts srcpath
puts "No such directory"
end
end
main
}}
- [[Jekyll>Jekyll]]で、以下のように変換結果の表示してテスト
-- `Ubuntu 13.04` `64bit`版
-- `Ruby 1.9.3`
-- https://ateraimemo.com/index.html
> jekyll server
> http://localhost:4000/swing/2011/09/26/swing-linesplittinglabel/
- 以下のような文字列で、「02 で発生し、1.7.0」がイタリックになってしまう
1.6.0_02 で発生し、1.7.0_05 で修正された
- リストがうまく変換されない場合がある
-- `Maruku: <a href="...">...</a>`のようなリンクだけで文字列がないリストを作成しようとすると空になる?
-- `Kramdown`: リストとブロック要素の間に空行が必要? `redcarpet`でも同様?
--- `Kramdown`で変換する場合、以下のような数値文字参照(`Numeric character reference`)をコードブロック(`<pre><code>`)に変換するときにエラーになる?
JEditorPane OK: ��
JEditorPane NG: 𦹀
-- 上記のようにリスト中にコードブロックがある場合、`- `で始まる行の後に、空行(もしくは空白文字のみのインデント)が必要だが、`p2m.rb`では生成できていない
--- コードブロックも、`(リスト階層+1)*4`スペース(もしくはタブ)のインデントが必要だが、`p2m.rb`では生成できていない
** Liquid [#cc063cb0]
- `PukiWiki`のプラグインを`Liquid`タグに移植
-- 例えば、`PukiWiki`で利用している`&src(swing/surrogatepair);`プラグイン(自作)に対応する`{% src swing/surrogatepair %}`を作成する
- `C:\jekyll-bootstrap\_plugins\src.rb`
#code{{
# -*- encoding: utf-8 -*-
class Src < Liquid::Tag
def initialize(tagName, id, tokens)
super
@id = id
end
def render(context)
#page_url = context.environments.first["page"]["url"]
url = "src.zip"
gaq = %Q|_gaq.push(['_trackEvent', 'Source', 'Download', '#{@id}']);location.href='#{url}'|
%Q|<a href="#{url}" onclick="#{gaq}">Source code(src.zip)</a>|
end
Liquid::Template.register_tag "src", self
end
}}
* Front Matter [#zdaefb02]
- [http://jekyllrb.com/docs/frontmatter/ Front Matter]を参考にして、ファイル先頭にページのメタ情報を`YAML`で記述しているので、これを本体の`Markdown`形式変換とは分けて処理する
#code{{
@FRONT_MATTER_REGEX ||= %r<
\A---[\r\n](.*?)[\r\n]---[\r\n](.*)
>mx
if @FRONT_MATTER_REGEX =~ src.lstrip then
frontmatter = $1
body = $2
#...
}}
* テスト [#id32e40e]
- 変換結果のテスト
# 変換したすべてのmdファイルのn行目(以下は3行目のタイトル)だけ一覧表示
$ cd _posts
$ find . -type f -name "*.md" -exec sed -n "3,3p" {} \;
* コメント [#w483296e]
* Comment [#w483296e]
#comment
#comment