jira2md
Version:
JIRA to MarkDown text format converter.
203 lines (193 loc) • 8.29 kB
JavaScript
const { marked } = require('marked');
marked.setOptions({ breaks: true, smartyPants: true });
class J2M {
/**
* Converts a Markdown string into HTML (just a wrapper to Marked's parse method).
*
* @static
* @param {string} str - String to convert from Markdown to HTML
* @returns {string} The HTML result
*/
static md_to_html(str) {
return marked.parse(str);
}
/**
* Converts a Jira Wiki string into HTML.
*
* @static
* @param {string} str - String to convert from Jira Wiki syntax to HTML
* @returns {string} The HTML result
*/
static jira_to_html(str) {
return marked.parse(J2M.to_markdown(str));
}
/**
* Converts a Jira Wiki string into Markdown.
*
* @static
* @param {string} str - Jira Wiki string to convert to Markdown
* @returns {string} The Markdown result
*/
static to_markdown(str) {
return (
str
// Un-Ordered Lists
.replace(/^[ \t]*(\*+)\s+/gm, (match, stars) => {
return `${Array(stars.length).join(' ')}* `;
})
// Ordered lists
.replace(/^[ \t]*(#+)\s+/gm, (match, nums) => {
return `${Array(nums.length).join(' ')}1. `;
})
// Headers 1-6
.replace(/^h([0-6])\.(.*)$/gm, (match, level, content) => {
return Array(parseInt(level, 10) + 1).join('#') + content;
})
// Bold
.replace(/\*(\S.*)\*/g, '**$1**')
// Italic
.replace(/_(\S.*)_/g, '*$1*')
// Monospaced text
.replace(/\{\{([^}]+)\}\}/g, '`$1`')
// Citations (buggy)
// .replace(/\?\?((?:.[^?]|[^?].)+)\?\?/g, '<cite>$1</cite>')
// Inserts
.replace(/\+([^+]*)\+/g, '<ins>$1</ins>')
// Superscript
.replace(/\^([^^]*)\^/g, '<sup>$1</sup>')
// Subscript
.replace(/~([^~]*)~/g, '<sub>$1</sub>')
// Strikethrough
.replace(/(\s+)-(\S+.*?\S)-(\s+)/g, '$1~~$2~~$3')
// Code Block
.replace(
/\{code(:([a-z]+))?([:|]?(title|borderStyle|borderColor|borderWidth|bgColor|titleBGColor)=.+?)*\}([^]*?)\n?\{code\}/gm,
'```$2$5\n```'
)
// Pre-formatted text
.replace(/{noformat}/g, '```')
// Un-named Links
.replace(/\[([^|]+?)\]/g, '<$1>')
// Images
.replace(/!(.+)!/g, '')
// Named Links
.replace(/\[(.+?)\|(.+?)\]/g, '[$1]($2)')
// Single Paragraph Blockquote
.replace(/^bq\.\s+/gm, '> ')
// Remove color: unsupported in md
.replace(/\{color:[^}]+\}([^]*?)\{color\}/gm, '$1')
// panel into table
.replace(/\{panel:title=([^}]*)\}\n?([^]*?)\n?\{panel\}/gm, '\n| $1 |\n| --- |\n| $2 |')
// table header
.replace(/^[ \t]*((?:\|\|.*?)+\|\|)[ \t]*$/gm, (match, headers) => {
const singleBarred = headers.replace(/\|\|/g, '|');
return `\n${singleBarred}\n${singleBarred.replace(/\|[^|]+/g, '| --- ')}`;
})
// remove leading-space of table headers and rows
.replace(/^[ \t]*\|/gm, '|')
);
// // remove unterminated inserts across table cells
// .replace(/\|([^<]*)<ins>(?![^|]*<\/ins>)([^|]*)\|/g, (_, preceding, following) => {
// return `|${preceding}+${following}|`;
// })
// // remove unopened inserts across table cells
// .replace(/\|(?<![^|]*<ins>)([^<]*)<\/ins>([^|]*)\|/g, (_, preceding, following) => {
// return `|${preceding}+${following}|`;
// });
}
/**
* Converts a Markdown string into Jira Wiki syntax.
*
* @static
* @param {string} str - Markdown string to convert to Jira Wiki syntax
* @returns {string} The Jira Wiki syntax result
*/
static to_jira(str) {
const map = {
// cite: '??',
del: '-',
ins: '+',
sup: '^',
sub: '~',
};
return (
str
// Tables
.replace(
/^\n((?:\|.*?)+\|)[ \t]*\n((?:\|\s*?-{3,}\s*?)+\|)[ \t]*\n((?:(?:\|.*?)+\|[ \t]*\n)*)$/gm,
(match, headerLine, separatorLine, rowstr) => {
const headers = headerLine.match(/[^|]+(?=\|)/g);
const separators = separatorLine.match(/[^|]+(?=\|)/g);
if (headers.length !== separators.length) return match;
const rows = rowstr.split('\n');
if (rows.length === 2 && headers.length === 1)
// Panel
return `{panel:title=${headers[0].trim()}}\n${rowstr
.replace(/^\|(.*)[ \t]*\|/, '$1')
.trim()}\n{panel}\n`;
return `||${headers.join('||')}||\n${rowstr}`;
}
)
// Bold, Italic, and Combined (bold+italic)
.replace(/([*_]+)(\S.*?)\1/g, (match, wrapper, content) => {
switch (wrapper.length) {
case 1:
return `_${content}_`;
case 2:
return `*${content}*`;
case 3:
return `_*${content}*_`;
default:
return wrapper + content + wrapper;
}
})
// All Headers (# format)
.replace(/^([#]+)(.*?)$/gm, (match, level, content) => {
return `h${level.length}.${content}`;
})
// Headers (H1 and H2 underlines)
.replace(/^(.*?)\n([=-]+)$/gm, (match, content, level) => {
return `h${level[0] === '=' ? 1 : 2}. ${content}`;
})
// Ordered lists
.replace(/^([ \t]*)\d+\.\s+/gm, (match, spaces) => {
return `${Array(Math.floor(spaces.length / 3) + 1)
.fill('#')
.join('')} `;
})
// Un-Ordered Lists
.replace(/^([ \t]*)\*\s+/gm, (match, spaces) => {
return `${Array(Math.floor(spaces.length / 2 + 1))
.fill('*')
.join('')} `;
})
// Headers (h1 or h2) (lines "underlined" by ---- or =====)
// Citations, Inserts, Subscripts, Superscripts, and Strikethroughs
.replace(new RegExp(`<(${Object.keys(map).join('|')})>(.*?)</\\1>`, 'g'), (match, from, content) => {
const to = map[from];
return to + content + to;
})
// Other kind of strikethrough
.replace(/(\s+)~~(.*?)~~(\s+)/g, '$1-$2-$3')
// Named/Un-Named Code Block
.replace(/```(.+\n)?((?:.|\n)*?)```/g, (match, synt, content) => {
let code = '{code}';
if (synt) {
code = `{code:${synt.replace(/\n/g, '')}}\n`;
}
return `${code}${content}{code}`;
})
// Inline-Preformatted Text
.replace(/`([^`]+)`/g, '{{$1}}')
// Images
.replace(/!\[[^\]]*\]\(([^)]+)\)/g, '!$1!')
// Named Link
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]')
// Un-Named Link
.replace(/<([^>]+)>/g, '[$1]')
// Single Paragraph Blockquote
.replace(/^>/gm, 'bq.')
);
}
}
module.exports = J2M;