はてな記法で書かれたテキストファイルを Markdown に変換するPHPスクリプト
目的・背景
- 今まで WordPress の投稿本文で「はてな記法」を使っていたが、今後は「Markdown」で書いていくことにした
- それにあたり、既存の記事も「Markdown」に変換して中身を置き換えたい
- が、いい感じのパーサーが見つからず
- 手作業はイヤなので、自家製ゴリ押し置換スクリプトを作ることにした
前提条件
用意するもの
はてな記法で書かれた .txt ファイル
作成するもの
Markdown記法で書かれた .md ファイル
実行環境
VirtualBox 上に立てた仮想サーバー(本サイトのテスト環境)
- CentOS 7
- PHP8
免責事項
- 本スクリプトの作成にあたっては、以下のマインドで臨んでいます。
- 1回きりしか使わないので、とにかく変換できりゃいい
- セキュリティとか動作の負荷とか考えない
- PHP の書き方イマイチかもしれんけどエラー吐いてなきゃええやろ
- 今回初めて Class 宣言を書きました
- よって、本スクリプトについては一切の動作保証をしません。
- また、本スクリプトの使用に伴い不利益・不都合等が生じた場合も、一切の責任を負いません。
対応範囲
自動置換するもの
種類 | はてな記法 | Markdown |
---|---|---|
見出し | *見出し |
# 見出し |
箇条リスト | -リスト |
- リスト |
数字リスト | +リスト |
1. リスト |
複数行コード | >|| ~ ||< |
``` ~ ``` |
単一行コード | <code>あいうえお</code> |
|
引用 | >> ~ << |
> 1行分の文章 |
強調 | <strong>あいうえお</strong> |
**あいうえお** |
リンク | [https://~:title=あいうえお] |
[あいうえお](https://~) |
リンク(URLのみ) | [https://~] |
[https://~](https://~) |
続きを読む | ==== |
<!--more--> |
手作業で対応するもの
- 表記法
- コツコツ直したほうが早いと判断
- 脚注記法
- 対応する記法がないので、内容に応じて文中に落としこみ
- 改行の調整
- リストの直後に見出しなどが改行なしで続くと li 要素の中に入ってしまうなど、微妙に崩れる部分を適宜調整
成果物
ファイル一覧
md
ディレクトリ- 変換後の .md ファイルが保存される場所
txt
ディレクトリ- 変換前の .txt ファイルを配置する場所
index.php
- 実行ファイル本体
txt_import.php
- .txt ファイルを読み込む
hatena_md.php
- はてな記法からMarkdownへ変換する
md_export.php
- .md ファイルを書き出す
使い方
txt
ディレクトリに .txt ファイルを設置index.php
で配置したファイルの名前を配列に格納- ブラウザから
index.php
にアクセス md
ディレクトリに .md ファイルが出力される
デモ動画
Premiere Pro の習作がてらデモ動画作ってみました。約1分。
音声読み上げは「音読さん」を利用しています。
ソース
index.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2201-はてな記法をMarkdownに変換</title>
<style>
pre{
background-color: #EEF;
padding: 1em;
}
</style>
</head>
<body>
<h1>2201-はてな記法をMarkdownに変換</h1>
<?php
//各ファイルの呼び出し
require_once 'txt_import.php';
require_once 'hatena_md.php';
require_once 'md_export.php';
//インスタンスを生成
$ti = new txt_import();
$hm = new hatena_md();
$me = new md_export();
//入出力するファイルの名前を配列に格納(例:123.txt → 123.md)
$ids=array(123,456,789);
//1ファイルずつ実行
foreach ($ids as $id) {
//入力元ファイル
$ti->file_dir='txt/';//ディレクトリ
$ti->file_name=$id;//名前
$ti->file_ex='.txt';//拡張子
//出力先ファイル
$me->file_dir='md/';//ディレクトリ
$me->file_name=$id;//名前
$me->file_ex='.md';//拡張子
//読み込みを実行
$htn_obj=$ti->get_txtObj();
//はてな->Markdown変換を実行
$md_obj=$hm->get_mdObj($htn_obj);
//書き出しを実行
$me->data=$md_obj;
$me->save();
}
//確認用
echo '<pre>';
var_dump(array_map('htmlspecialchars', $md_obj));
echo '</pre>';
?>
</body>
</html>
txt_import.php
<?php
//テキストファイルを読み込んで配列に変換する
class txt_import{
//variable-----------------------------------------------------------//
//読み込むファイル
public $file='';
public $file_dir='';//ディレクトリ
public $file_name='';//名前
public $file_ex='';//拡張子
//出力する配列
private $txt_obj=array();
//public-----------------------------------------------------------//
//配列を返す
public function get_txtObj(){
//ファイル名を取得
$this->get_file();
//ファイルを読み込んで配列に変換
$this->import();
//結果を返す
return $this->txt_obj;
}
//private-----------------------------------------------------------//
/*
ファイルパスを作成
*/
private function get_file(){
$str=$this->file_dir.$this->file_name.$this->file_ex;
$this->file=$str;
return;
}
/*
テキストを配列に変換
*/
private function convert_txtToArr($text,$letter){
if($letter=='\n'){
$lines=explode("\n", $text);//行で分割
}else{
$lines=explode($letter, $text);//文字で分割
}
// $lines=array_map('trim', $lines); // 各行にtrim()をかける
// $lines=array_filter($lines, 'strlen'); // 文字数が0の行を取り除く
// $lines=array_values($lines); // これはキーを連番に振りなおしてるだけ
return $lines;
}
/*
ファイルからテキストを取得&配列に格納
*/
private function import(){
$text='';
if(file_exists($this->file)){
$text=file_get_contents($this->file);
}
$this->txt_obj=$this->convert_txtToArr($text,'\n');
return;
}
}
hatena_md.php
<?php
//配列に格納されたはてな記法のテキストをMarkdownに変換
class hatena_md{
//variable-----------------------------------------------------------//
//変換前データ
public $htn_obj=array();
//変換後データ
public $md_obj=array();
//マッチング用正規表現
private $reg_hl='/^(\*{1,3})([^\*]+.+)$/';//見出し
private $reg_ul='/^(\-{1,3})([^\-]+.+)$/';//箇条リスト
private $reg_ol='/^(\+{1,3})([^\+]+.+)$/';//数字リスト
private $reg_more='/^\={4,}$/';//もっと読む区切り
private $reg_preStart='/^>\|([a-z]+)?\|$/';//複数行コード開始
private $reg_preEnd='/^\|{2}<$/';//複数行コード終了
private $reg_quoteStart='/^>{2}$/';//引用ブロック開始
private $reg_quoteEnd='/^<{2}$/';//引用ブロック終了
//マッチング用正規表現(インライン)
private $reg_th='/[^\|]*(\|\*)[^\|]+\|/';//表見出しセル
private $reg_strong='/<\/?strong>/';//強調
private $reg_code='/<\/?code>/';//単一行コード
private $reg_link='/\[(https?:\/\/)?([^:]+)(:{1,2})title=([^\]]+)\]/';//テキストリンク
private $reg_link_url='/\[(https?:\/\/[^\]]+)\]$/';//テキストリンク(URLのみ)
//一時保存情報
private $tmp_text='';//変換するテキスト
private $tmp_matches='';//マッチした結果
private $tmp_flg=0;//正規表現にマッチするかどうか
private $tmp_tag='';//マッチしたタグの種類
private $tmp_isPre=0;//preの中身かどうか
private $tmp_isQuote=0;//blockquoteの中身かどうか
//public-----------------------------------------------------------//
//配列を返す
public function get_mdObj($obj){
$this->htn_obj=$obj;
$this->process();
return $this->md_obj;
}
//private-----------------------------------------------------------//
/*
はてな記法をMarkdownに変換
*/
private function process(){
$result=array();
foreach ($this->htn_obj as $key => $text) {
$this->tmp_text=$text;
$result[$key]=$this->convert($text);
}
$this->md_obj=$result;
return;
}
/*
正規表現パターンリストを作成
*/
private function set_patterns($num){
$patterns=array();
if($num===1){//パターン1
$patterns=array(
'hl' => $this->reg_hl,
'ul' => $this->reg_ul,
'ol' => $this->reg_ol,
'more' => $this->reg_more,
'preStart' => $this->reg_preStart,
'preEnd' => $this->reg_preEnd,
'quoteStart' => $this->reg_quoteStart,
'quoteEnd' => $this->reg_quoteEnd,
);
}elseif($num===2){//パターン2
$patterns=array(
'th' => $this->reg_th,
'strong' => $this->reg_strong,
'code' => $this->reg_code,
'link' => $this->reg_link,
'link_url' => $this->reg_link_url,
);
}
return $patterns;
}
/*
1行単位で変換実行
*/
private function convert(){
//パターン1でチェック→置換実行
$this->check_all(1);
$this->tmp_text=$this->run_replace();
//パターン2でチェック→置換実行
//パターン2に関しては1行に対して全部チェッククリアするまで繰り返し処理し続ける
$this->check_all(2);
if($this->tmp_flg){
while($this->tmp_flg==2){
$this->check_all(2);
$this->tmp_text=$this->run_replace();
}
}
if($this->tmp_isQuote && !empty($this->tmp_text)){
$this->tmp_text='> '. $this->tmp_text;
}
return $this->tmp_text;
}
/*
正規表現でチェック
*/
private function check($pattern){
preg_match($pattern, $this->tmp_text, $matches);
$this->tmp_matches=$matches;
return;
}
//複数パターンを一気にチェック
private function check_all($num){
$patterns=$this->set_patterns($num);
$this->tmp_flg=0;
foreach ($patterns as $key => $pattern) {
if(!$this->tmp_flg){
$this->check($pattern);
if($this->tmp_matches){
$this->tmp_flg=$num;
$this->tmp_tag=$key;
}
}
}
return;
}
/*
正規表現で置換
*/
//置換実行
private function run_replace(){
$str='';
if($this->tmp_flg){
$str=$this->replace($this->tmp_matches);
}else{
$str=$this->tmp_text;
}
return $str;
}
//対応パターン登録
private function replace($matches){
if($this->tmp_flg==1){
if($this->tmp_tag==='hl') return $this->replace_hl($matches[1],$matches[2]);
if($this->tmp_tag==='ul') return $this->replace_ul($matches[1],$matches[2]);
if($this->tmp_tag==='ol') return $this->replace_ol($matches[1],$matches[2]);
if($this->tmp_tag==='more') return $this->replace_more();
if($this->tmp_tag==='preStart') return $this->replace_preStart($matches);
if($this->tmp_tag==='preEnd') return $this->replace_preEnd();
if($this->tmp_tag==='quoteStart') return $this->replace_quoteStart($matches);
if($this->tmp_tag==='quoteEnd') return $this->replace_quoteEnd();
}elseif($this->tmp_flg==2){
if($this->tmp_tag==='th') return $this->replace_th();
if($this->tmp_tag==='strong') return $this->replace_strong();
if($this->tmp_tag==='code') return $this->replace_code();
if($this->tmp_tag==='link') return $this->replace_link($matches);
if($this->tmp_tag==='link_url') return $this->replace_link_url($matches);
}
}
//見出し
private function replace_hl($m1,$m2){
$str=str_replace('*','#',$m1).' '.$m2;
return $str;
}
//箇条リスト
private function replace_ul($m1,$m2){
$str='';
if($m1==='-'){
$str='- '.$m2;
}elseif($m1==='--'){
$str=' - '.$m2;
}elseif($m1==='---'){
$str=' - '.$m2;
}
return $str;
}
//数字リスト
private function replace_ol($m1,$m2){
$str='';
if($m1==='+'){
$str='1. '.$m2;
}elseif($m1==='++'){
$str=' 1. '.$m2;
}elseif($m1==='+++'){
$str=' 1. '.$m2;
}
return $str;
}
//箇条リスト
private function replace_more(){
$str='<!--more-->';
return $str;
}
//複数行コード開始
private function replace_preStart($m){
$str='';
if(!$this->tmp_isPre){
if(array_key_exists(1,$m)){
$str='```'.$m[1];
}else{
$str='```';
}
}else{
$str=$this->tmp_text;
}
$this->tmp_isPre=1;
return $str;
}
//複数行コード終了
private function replace_preEnd(){
$str='';
$this->tmp_isPre=0;
if(!$this->tmp_isPre){
$str='```';
}else{
$str=$this->tmp_text;
}
return $str;
}
//引用ブロック開始
private function replace_quoteStart($m){
$str='';
if(!$this->tmp_isQuote){
$str='';
}else{
$str=$this->tmp_text;
}
$this->tmp_isQuote=1;
return $str;
}
//引用ブロック終了
private function replace_quoteEnd(){
$str='';
$this->tmp_isQuote=0;
if(!$this->tmp_isQuote){
$str='';
}else{
$str=$this->tmp_text;
}
return $str;
}
//表見出しセル
private function replace_th(){
$str=$this->tmp_text;
$str=str_replace('|*','|',$str);
$str=str_replace('|',' | ',$str);
$str=preg_replace('/^\s\|/', '|', $str);
$str=preg_replace('/\|\s$/', '|', $str);
return $str;
}
//強調
private function replace_strong(){
$str=$this->tmp_text;
$str=str_replace('<strong>','**',$str);
$str=str_replace('</strong>','**',$str);
return $str;
}
//単一行コード
private function replace_code(){
$str=$this->tmp_text;
$str=str_replace('<code>','`',$str);
$str=str_replace('</code>','`',$str);
return $str;
}
//テキストリンク
private function replace_link($m){
$str=$this->tmp_text;
$str=preg_replace($this->reg_link, '[$4]($1$2)', $str);
return $str;
}
//テキストリンク(URLのみ)
private function replace_link_url($m){
$str=$this->tmp_text;
$str=preg_replace($this->reg_link_url, '[$1]($1)', $str);
return $str;
}
}
md_export.php
<?php
//配列を.mdファイルに書き出す
class md_export{
//variable-----------------------------------------------------------//
//読み込むデータ
public $data=array();
//出力先ファイル
public $file='';
public $file_dir='';//ディレクトリ
public $file_name='';//名前
public $file_ex='';//拡張子
//public-----------------------------------------------------------//
//はてな記法をMarkdown記法に変換する
public function save(){
$this->get_file();
$this->export();
}
//private-----------------------------------------------------------//
/*
ファイルパスを作成
*/
private function get_file(){
$str=$this->file_dir.$this->file_name.$this->file_ex;
$this->file=$str;
return;
}
/*
ファイルを書き出す
*/
private function export(){
file_put_contents($this->file,implode(PHP_EOL,$this->data));
echo '<p>'.$this->file.' を保存しました。</p>';
return;
}
}