Skip to content

Instantly share code, notes, and snippets.

@endam
Created April 10, 2020 11:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save endam/6cb383863fe934871435ab29acb9e36e to your computer and use it in GitHub Desktop.
Save endam/6cb383863fe934871435ab29acb9e36e to your computer and use it in GitHub Desktop.
Backlog wiki記法→Markdown記法 一括変換スクリプト
class Convert {
execute(val) {
const
codes = [];
const
quotes = [];
const
paragraphs = [];
const
textLevelSemanticsCheck = (content) => {
const patterns = [
// 要素を<tagName>表記しているパターンをエスケープ
{
pattern: /<(.*?)>/g,
replacement: (m, p1) => {
return `&lt;${p1}&gt;`;
}
},
// 抜け漏れしている<をエスケープ。>は他の記法で利用されているためエスケープしない
{
pattern: /</g,
replacement: '&lt;'
},
// strong
{
pattern: /''(.*?)''/g,
replacement: (m, p1) => {
return ` **${p1.trim()}** `;
}
},
// em
{
pattern: /'(.*?)'/g,
replacement: (m, p1) => {
return ` *${p1.trim()}* `;
}
},
// strike
{
pattern: /%%(.*?)%%/g,
replacement: (m, p1) => {
return ` ~~${p1.trim()}~~ `;
}
},
// a
{
pattern: /\[\[(.*?)[:>](.*?)\]\]/g,
replacement: (m, p1, p2) => {
return `[${p1.trim()}](${p2})`;
}
},
// 色指定(Markdown in Backlogでは無視される)
{
pattern: /&color\((.*?)\)(\s+)?{(.*?)}/ig,
replacement: (m, p1, p2, p3) => {
return `<span style="color: ${p1};">${p3}</span>`;
}
},
// その他埋め込み
{
pattern: /#image\((.*?)\)/,
replacement: (m, p1) => {
return `![${p1}]`;
}
},
{
pattern: /#thumbnail\((.*?)\)/,
replacement: (m, p1) => {
return `![${p1}]`;
}
},
{
pattern: /#attach\((.*?):(.*?)\)/,
replacement: (m, p1, p2) => {
return `[${p1}][${p2}]`;
}
},
// 余計な半角スペースの連続を削除
{
pattern: / {2}/g,
replacement: ' '
}
];
patterns.forEach(({pattern, replacement}) => {
content = content.replace(pattern, replacement);
});
return content;
};
const
patterns = [
// 改行コード
{
pattern: /\n\r/g,
replacement: '\n'
},
{
pattern: /\r/g,
replacement: '\n'
},
// 見出し
{
pattern: /^(\*+)(.*)$/gm,
replacement: (m, p1, p2) => {
return `\n${p1.replace(/\*/g, '#')} ${textLevelSemanticsCheck(p2.trim())}\n`;
}
},
// theadなしテーブル
{
pattern: /\n\|([\s|\S]*?)\|\n(?!\|)/g,
replacement: (m, p1) => {
return `\n|${p1}|\n\n`;
}
},
{
pattern: /\n\|([\s|\S]*?)\|\n\n/g,
replacement: (m, p1) => {
if (p1.indexOf('|h\n') === -1) {
let i = 0;
let max = p1.split('\n')[0].split('|').length;
let row = '';
for (i; i < max; i++) {
row += '|:--';
}
row += '|';
return `\n${row}\n|${p1}|\n`;
}
return `\n|${p1}|\n`;
}
},
// 行見出しテーブル
{
pattern: /^\|(.*)\|(\s?)$/gm,
replacement: (m, p1, p2) => {
p1 = p1.replace(/\|~/g, '|');
p1 = p1.replace(/^~/g, '');
p1 = p1.replace(/\|\|/g, '| |');
p1 = p1.replace(/^\|/g, ' |');
p1 = p1.replace(/^\|/g, ' |'); // 先頭が空
p1 = p1.replace(/\|$/g, '| '); // 最後が空
return `|${p1}|${p2}`;
}
},
// 列見出しテーブル
{
pattern: /^\|(.*)\|h\s?$/gm,
replacement: (m, p1) => {
let row = '';
let cell = p1.split('|');
let i = 0;
let max = cell.length;
for (i; i < max; i++) {
row += '|:--';
}
row += '|';
p1 = p1.replace(/\|~/g, '|');
p1 = p1.replace(/^~/g, '');
p1 = p1.replace(/\|\|/g, '| |');
p1 = p1.replace(/^\|/g, ' |'); // 先頭が空
p1 = p1.replace(/\|$/g, '| '); // 最後が空
return `\n|${p1}|\n${row}`;
}
},
// テーブルに改行
{
pattern: /\n\|([\s|\S]*?)\|\n([^|])/g,
replacement: (m, p1, p2) => {
return `\n|${textLevelSemanticsCheck(p1)}|\n\n${p2}`;
}
},
{
pattern: /\n\|([\s|\S]*?)\|\n(?!\|)/g,
replacement: (m, p1) => {
return `\n\n|${p1}|\n\n`;
}
},
// 順序リスト
{
pattern: /\n\+([\s|\S]*?)\n\n/g,
replacement: (m, p1) => {
return `\n+${p1}\n\n\n`;
}
},
{
pattern: /\n\+([\s|\S]*?)\n\n/g,
replacement: (m, p1) => {
let result = '';
p1 = '\n+' + p1.trim();
// スペースの整形
p1 = p1.replace(/^(\++)(.*)$/gm, (m2, p2, p3) => {
return `${p2} ${p3.trim()}`;
});
p1 = p1.trim();
{
const symbolCount = []; // index番号はインデントレベル。インデントが上がったら
let currentLevel = 0;
p1.split('\n').forEach((line) => {
const level = line.split(' ')[0].length;
if (level < currentLevel) {
symbolCount[currentLevel] = 0;
}
currentLevel = level;
symbolCount[currentLevel] = symbolCount[currentLevel] ? symbolCount[currentLevel] + 1 : 1;
result += textLevelSemanticsCheck(line.replace('+ ', symbolCount[currentLevel] + '. ')) + '\n';
});
}
// ネストがあるときに残っている + をスペースに変換
p1 = result.replace(/^(\++)(.*)/gm, (m2, p2, p3) => {
const max = p2.length;
let i = 0;
let indent = '';
for (i; i < max; i++) {
indent += ' ';
}
return indent + p3;
});
return `\n${p1}\n`;
}
},
// 非順序リスト
{
pattern: /^(-+)(.*)$/gm,
replacement: (m, p1, p2) => {
let i = 0;
let max = p1.length - 1;
let indent = '';
if (!p2) {
return p1; // hr要素
}
for (i; i < max; i++) {
indent += ' ';
}
indent += '-';
p2 = textLevelSemanticsCheck(p2).trim();
return `${indent} ${p2}`;
}
},
// 改行をbr要素に変換(Markdown in Backlogでは無視されます)
{
pattern: /&br;/g,
replacement: ' <br>'
},
{
pattern: /&/g,
replacement: '&amp;'
}
];
// 正規表現を助けるための改行を追加
val = '\n' + val + '\n\n';
// 目次
val = val.replace(/^#contents$/gm, '[toc]\n');
// コード範囲指定を隠す
val = val.replace(/\n{code}([\s|\S]*?){\/code}\n/g, (m, p1) => {
codes.push(p1);
return `\n{{CODE_REPACE_BACKLOG_TO_MARKDOWN-${codes.length - 1}}}\n`;
});
// 引用範囲指定を隠す
val = val.replace(/\n{quote}([\s|\S]*?){\/quote}\n/g, (m, p1) => {
quotes.push(p1);
return `\n{{QUOTE_REPACE_BACKLOG_TO_MARKDOWN-${quotes.length - 1}}}\n`;
});
// 通常テキスト(パラグラフ)を隠す
val = val.replace(/^.*$/gm, (() => {
const isP = /^(?![*\|\-\+\s>)`])(.*)$/;
return (p1) => {
if (
p1 &&
isP.test(p1) &&
!p1.startsWith('{{CODE_REPACE_BACKLOG_TO_MARKDOWN') &&
!p1.startsWith('{{QUOTE_REPACE_BACKLOG_TO_MARKDOWN')
) {
paragraphs.push(p1);
return `{{PARAGRAPHS_REPACE_BACKLOG_TO_MARKDOWN-${paragraphs.length - 1}}}`;
}
return p1;
};
})());
// パラグラフの塊は最後に空行を開けさせる
val = val.replace(/\n{{PARAGRAPHS_REPACE_BACKLOG_TO_MARKDOWN-.*?}}\n(?!{{)/g, (p1) => {
return `${p1}\n`;
});
// 範囲指定型以外のBacklog記法を置き換える
patterns
.forEach(
({
pattern
,
replacement
}
) => {
val = val.replace(pattern, replacement);
}
)
;
// 範囲指定系を埋めもどす前に無駄な改行を削除する
while (/\n\n\n/g.test(val)) {
val = val.replace('\n\n\n', '\n\n');
}
// コード範囲指定を戻す
val = val.replace(/{{CODE_REPACE_BACKLOG_TO_MARKDOWN-(.*?)}}/g, (m, p1) => {
let content = codes[Number(p1)].trim();
if (content) {
return '\n```\n' + codes[Number(p1)].trim() + '\n```\n';
}
return '\n```\n```\n';
});
// 引用範囲指定を戻す
val = val.replace(/{{QUOTE_REPACE_BACKLOG_TO_MARKDOWN-(.*?)}}/g, (m, p1) => {
let content = quotes[Number(p1)].trim();
content = content.split('\n').join('\n> ');
return '\n> ' + content + '\n';
});
// パラグラフを戻す
val = val.replace(/{{PARAGRAPHS_REPACE_BACKLOG_TO_MARKDOWN-(.*?)}}/g, (m, p1) => {
let content = paragraphs[Number(p1)].trim();
content = textLevelSemanticsCheck(content);
return content;
});
val = val.replace(/''(.*?)''/g, (m, p1) => {
return ` **${p1.trim()}** `;
})
return val.trim();
}
}
module.exports = Convert;
const Convert = require('./Convert.js');
const convert = new Convert();
const fl = require('node-filelist');
const files = [ "****" ]; //読み込みたいファイルディレクトリまたはパス(配列なので複数指定可)
const option = { "ext" : "md" }; //読み込みたいファイルの拡張子(指定がない場合は全てのファイルを読み込みます)
const fs = require("fs-extra");
fl.read(files, option , (results) => {
for(let i = 0; i < results.length; i++){
const filePath = results[i].path;
console.log(filePath);
if (/.+\/\.(.+?)([?#;].*)?$/.test(filePath)) {
continue;
}
const newFilePath = filePath.replace('KCT_KC', 'KCT_KC_1');
dir = newFilePath.substring(0, newFilePath.lastIndexOf('/')) + '/';
console.log(dir);
if (!fs.existsSync(dir)) {
fs.mkdirsSync(dir);
}
let text = fs.readFileSync(filePath, 'utf8');
text = convert.execute(text);
fs.writeFileSync(newFilePath, text);
}
});
{
"name": "converter",
"version": "1.0.0",
"description": "",
"main": "main.js",
"dependencies": {
"fs-extra": "^9.0.0",
"node-filelist": "^1.0.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment