UNPKG

@kmatthews/jira2md

Version:

JIRA to MarkDown text format converter.

219 lines (200 loc) 7.23 kB
const marked = require('marked'); marked.setOptions({ breaks: true, smartyPants: true }); class J2M { constructor(str) { this.str = str; } md_to_html(str) { return marked(str); }; jira_to_html(str) { return marked(this.to_markdown(str)); }; to_jira(str) { let hash = splitOutCodeblocks(str, 'toJira'); return transformHash(hash, 'toJira') }; to_markdown(str) { let hash = splitOutCodeblocks(str, 'toMarkdown'); return transformHash(hash, 'toMarkdown') }; }; const transformHash = function (hash, direction) { let string = '' if (direction == 'toMarkdown') { Object.keys(hash).forEach((key) => { if (hash[key]['code']) { string += codeblockToMarkdown(hash[key]['string']); } else { string += toMarkdownFormatting(hash[key]['string']); }; }); } else if (direction == 'toJira'){ Object.keys(hash).forEach((key) => { if (hash[key]['code']) { string += codeblockToJira(hash[key]['string']); } else { string += toJiraFormatting(hash[key]['string']); }; }); } else { throw 'Direction is invalid.' } return string }; const splitOutCodeblocks = function (str, direction) { let hash = {}; let array = []; // This block returns an array where each element is either a codeblock or is not if (direction == 'toMarkdown') { array = str.split(/(\{code[^]*?\{code\}|\{noformat[^]*?\{noformat\})/) } else if (direction == 'toJira') { array = str.split(/(```[^]*?```)/) } else { return [str] } array.map((string, index) => { hash[index] = { string: string, code: string.includes('```') || string.includes('{code}') || string.includes('{noformat}') } }); return hash; }; const codeblockToMarkdown = function (str) { return str .replace( /\{code(:([a-z]+))?([:|]?(title|borderStyle|borderColor|borderWidth|bgColor|titleBGColor)=.+?)*\}([^]*?)\{code\}/gm, '```$2$5```' ) // Pre-formatted text .replace(/{noformat}/g, '```') }; const codeblockToJira = function (str) { return str .replace(/`{3,}(\w+)?((?:\n|[^`])+)`{3,}/g, function (_match, synt, content) { let code = '{code'; if (synt) code += ':' + synt; return code + '}' + content + '{code}'; }) }; const toMarkdownFormatting = function (str) { return str // Ordered Lists .replace(/^[ \t]*(\*+)\s+/gm, function (_match, stars) { return Array(stars.length).join(" ") + '* '; }) // Un-ordered lists .replace(/^[ \t]*(#+)\s+/gm, function (_match, nums) { return Array(nums.length).join(" ") + '1. '; }) // Headers 1-6 .replace(/^h([0-6])\.(.*)$/gm, function (_match, level, content) { return Array(parseInt(level) + 1).join('#') + content; }) // Bold .replace(/(\s|^|\_)\*(\S.*?)\*($|[~`!@#$%^&*(){}\[\];:"'<,.>?\/\\|_+=-]|\s)/g, '$1**$2**$3') // Italic .replace(/(\s|^|\*)\_(\S.*?)\_($|[~`!@#$%^&*(){}\[\];:"'<,.>?\/\\|_+=-]|\s)/g, '$1*$2*$3') // 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)-+/g, '$1~~$2~~') // Un-named Links .replace(/\[([^|]+)\]/g, '<$1>') // 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, function (_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, '|'); }; const toJiraFormatting = function (str) { const map = { //cite: '??', del: '-', ins: '+', sup: '^', sub: '~' }; return str // Bold, Italic, and Combined (bold+italic) .replace(/(\s?|^)([*_]+)(\S.*?)\2(\s|[~`!@#$%^&()\{\}\[\];:"'<,\.>?\/\\|+=-]|$)/gm, function (_match, opening_chars, wrapper, content, closing_chars) { switch (wrapper.length) { case 1: return opening_chars + '_' + content + '_' + closing_chars; case 2: return opening_chars + "*" + content + "*" + closing_chars; case 3: return opening_chars + "_*" + content + "*_" + closing_chars; default: return opening_chars + wrapper + content * wrapper + closing_chars; } }) // All Headers (# format) .replace(/^([#]+)(.*?)$/gm, function (_match, level, content) { return 'h' + level.length + '.' + content; }) // Headers (H1 and H2 underlines) .replace(/^(.*?)\n([=-]+)$/gm, function (_match, content, level) { return 'h' + (level[0] === '=' ? 1 : 2) + '. ' + content; }) // Ordered lists .replace(/^([ \t]*)\d+\.\s+/gm, function (_match, spaces) { return Array(Math.floor(spaces.length / 2 + 1)).join("#") + '# '; }) // Un-Ordered Lists .replace(/^([ \t]*)\*\s+/gm, function (_match, spaces) { return Array(Math.floor(spaces.length / 2 + 1)).join("*") + '* '; }) // Headers (h1 or h2) (lines "underlined" by ---- or =====) // Citations, Inserts, Subscripts, Superscripts, and Strikethroughs .replace(new RegExp('<(' + Object.keys(map).join('|') + ')>(.*?)<\/\\1>', 'g'), function (_match, from, content) { const to = map[from]; return to + content + to; }) // Other kind of strikethrough .replace(/(\s|^)+\~~(.*?)\~~+/g, '$1-$2-') // Inline-Preformatted Text .replace(/`([^`]+)`/g, '{{$1}}') // Named Link .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]') // Un-Named Link .replace(/<([^>]+)>/g, '[$1]') // Single Paragraph Blockquote .replace(/^>/gm, 'bq.') // tables .replace(/^\n((?:\|.*?)+\|)[ \t]*\n((?:\|\s*?\-{3,}\s*?)+\|)[ \t]*\n((?:(?:\|.*?)+\|[ \t]*\n)*)$/gm, function (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 === 1 + 1 && headers.length === 1) { // panel return '{panel:title=' + headers[0].trim() + '}\n' + rowstr.replace(/^\|(.*)[ \t]*\|/, '$1').trim() + '\n{panel}\n'; } else { return '||' + headers.join('||') + '||\n' + rowstr; } }); }; module.exports = new J2M();