UNPKG

@aashari/mcp-server-atlassian-jira

Version:

Node.js/TypeScript MCP server for Atlassian Jira. Equips AI systems (LLMs) with tools to list/get projects, search/get issues (using JQL/ID), and view dev info (commits, PRs). Connects AI capabilities directly into Jira project management and issue tracki

397 lines (396 loc) 14.6 kB
"use strict"; /** * Converts Markdown to Atlassian Document Format (ADF) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.markdownToAdf = markdownToAdf; const logger_util_js_1 = require("./logger.util.js"); const adf_from_text_util_js_1 = require("./adf-from-text.util.js"); const fromMarkdownLogger = logger_util_js_1.Logger.forContext('utils/adf/from-markdown.ts'); /** * Convert Markdown text to an Atlassian Document Format (ADF) document * Supports a wide range of Markdown formatting: * - Headings (# text) converted to proper ADF heading nodes with levels 1-6 * - Bold (**text**) * - Italic (*text*) * - Code (`text`) * - Strikethrough (~~text~~) * - Links ([text](url)) * - Unordered lists (- item or * item) * - Ordered lists (1. item) * - Blockquotes (> text) * - Code blocks (```language\ncode\n```) * - Horizontal rules (---, ***, ___) * - Tables (| Column | Column |) * * @param markdown - Markdown text to convert to ADF * @returns ADF document object */ function markdownToAdf(markdown) { const methodLogger = fromMarkdownLogger.forMethod('markdownToAdf'); try { // Replace literal '\n' string with actual newlines const processedMarkdown = markdown.replace(/\\n/g, '\n'); // Split text into paragraphs const paragraphs = processedMarkdown.split('\n'); // Create basic document structure const adfDoc = { version: 1, type: 'doc', content: [], }; // Process paragraphs in a loop to handle multi-line structures for (let i = 0; i < paragraphs.length; i++) { const paragraph = paragraphs[i].trim(); // Skip empty paragraphs if (paragraph === '') { continue; } // Handle headings (lines starting with # symbol) if (paragraph.startsWith('#')) { const headingMatch = paragraph.match(/^(#+)\s*(.+?)\s*$/); if (headingMatch) { // Get heading level (number of # symbols) const level = headingMatch[1].length; // Get heading text const headingText = headingMatch[2]; // Create proper ADF heading node with appropriate level adfDoc.content.push({ type: 'heading', attrs: { level }, content: [ { type: 'text', text: headingText, }, ], }); } continue; } // Handle code blocks (lines starting with ```) if (paragraph.startsWith('```')) { const language = paragraph.substring(3).trim(); const codeLines = []; let j = i + 1; // Collect code lines until closing ``` while (j < paragraphs.length && !paragraphs[j].trim().startsWith('```')) { codeLines.push(paragraphs[j]); j++; } // Skip the closing ``` if found if (j < paragraphs.length && paragraphs[j].trim().startsWith('```')) { j++; } // Update loop counter i = j - 1; // Create code block adfDoc.content.push({ type: 'codeBlock', attrs: language ? { language } : {}, content: [ { type: 'text', text: codeLines.join('\n'), }, ], }); continue; } // Handle horizontal rules if (/^(\*\*\*|---|_{3,})$/.test(paragraph)) { adfDoc.content.push({ type: 'rule', }); continue; } // Handle tables - detect a line that starts and ends with pipe if (paragraph.trim().startsWith('|') && paragraph.trim().endsWith('|')) { // Look ahead to see if there's a separator row (|---|---|) const nextLine = i + 1 < paragraphs.length ? paragraphs[i + 1].trim() : ''; const isSeparatorRow = nextLine.startsWith('|') && nextLine.endsWith('|') && /^\|[\s\-:|]+\|$/.test(nextLine.replace(/\|[^|]+/g, '|---')); if (isSeparatorRow) { // This is a table // Extract rows starting with header, then separator, then data rows const tableRows = []; let j = i; // Collect all table rows (rows starting and ending with |) while (j < paragraphs.length && paragraphs[j].trim().startsWith('|') && paragraphs[j].trim().endsWith('|')) { tableRows.push(paragraphs[j].trim()); j++; } // Update loop counter to skip processed rows i = j - 1; // Parse and create ADF table if (tableRows.length >= 2) { // Need at least header and separator const tableContent = parseMarkdownTable(tableRows); adfDoc.content.push(tableContent); } continue; } } // Handle blockquotes (lines starting with >) if (paragraph.startsWith('>')) { const quoteText = paragraph.substring(1).trim(); adfDoc.content.push({ type: 'blockquote', content: [ { type: 'paragraph', content: parseMarkdownText(quoteText), }, ], }); continue; } // Handle ordered lists (lines starting with number and period) if (/^\d+\.\s/.test(paragraph)) { // Extract all list items starting from this paragraph const listItems = []; let j = i; // Collect consecutive list items while (j < paragraphs.length && /^\d+\.\s/.test(paragraphs[j].trim())) { const itemText = paragraphs[j] .trim() .replace(/^\d+\.\s/, ''); listItems.push(itemText); j++; } // Update loop counter to skip processed items i = j - 1; // Create ordered list structure const orderedListContent = listItems.map((item) => ({ type: 'listItem', content: [ { type: 'paragraph', content: parseMarkdownText(item), }, ], })); adfDoc.content.push({ type: 'orderedList', content: orderedListContent, }); continue; } // Handle unordered lists (lines starting with - or *) if (paragraph.startsWith('- ') || paragraph.startsWith('* ')) { // Extract all list items starting from this paragraph const listItems = []; let j = i; // Collect consecutive list items while (j < paragraphs.length && (paragraphs[j].trim().startsWith('- ') || paragraphs[j].trim().startsWith('* '))) { const itemText = paragraphs[j].trim().substring(2); listItems.push(itemText); j++; } // Update loop counter to skip processed items i = j - 1; // Create bullet list structure const bulletListContent = listItems.map((item) => ({ type: 'listItem', content: [ { type: 'paragraph', content: parseMarkdownText(item), }, ], })); adfDoc.content.push({ type: 'bulletList', content: bulletListContent, }); continue; } // Handle regular paragraphs with inline formatting adfDoc.content.push({ type: 'paragraph', content: parseMarkdownText(paragraph), }); } methodLogger.debug(`Converted Markdown to ADF, length: ${JSON.stringify(adfDoc).length}`); return adfDoc; } catch (error) { methodLogger.error('Error converting Markdown to ADF:', error); // Fall back to plain text if parsing fails return (0, adf_from_text_util_js_1.textToAdf)(markdown); } } /** * Parse Markdown text into ADF nodes * Handles inline formatting: bold, italic, code, strikethrough, and links */ function parseMarkdownText(text) { const result = []; // Regex patterns for inline Markdown formatting const patterns = [ // Links: [text](url) { regex: /\[([^\]]+)\]\(([^)]+)\)/g, process: (match) => ({ type: 'text', text: match[1], marks: [ { type: 'link', attrs: { href: match[2], }, }, ], }), }, // Bold: **text** { regex: /\*\*(.*?)\*\*/g, process: (match) => ({ type: 'text', text: match[1], marks: [{ type: 'strong' }], }), }, // Italic: *text* { regex: /\*(.*?)\*/g, process: (match) => ({ type: 'text', text: match[1], marks: [{ type: 'em' }], }), }, // Code: `text` { regex: /`(.*?)`/g, process: (match) => ({ type: 'text', text: match[1], marks: [{ type: 'code' }], }), }, // Strikethrough: ~~text~~ { regex: /~~(.*?)~~/g, process: (match) => ({ type: 'text', text: match[1], marks: [{ type: 'strike' }], }), }, ]; // Process text with multiple patterns let remainingText = text; let nextStartIndex = Number.MAX_SAFE_INTEGER; let nextPattern = null; let nextMatch = null; // Find all pattern matches and their positions while (remainingText.length > 0) { nextStartIndex = Number.MAX_SAFE_INTEGER; nextPattern = null; nextMatch = null; // Find the next earliest match for (const pattern of patterns) { pattern.regex.lastIndex = 0; const match = pattern.regex.exec(remainingText); if (match && match.index < nextStartIndex) { nextStartIndex = match.index; nextPattern = pattern; nextMatch = match; } } // No more matches found if (!nextPattern || !nextMatch) { // Add remaining text as plain text if (remainingText.length > 0) { result.push({ type: 'text', text: remainingText, }); } break; } // Add text before the match as plain text if (nextStartIndex > 0) { result.push({ type: 'text', text: remainingText.substring(0, nextStartIndex), }); } // Add the formatted text result.push(nextPattern.process(nextMatch)); // Update the remaining text remainingText = remainingText.substring(nextStartIndex + nextMatch[0].length); } return result.length > 0 ? result : [{ type: 'text', text: text }]; } /** * Parse a markdown table into an ADF table structure * @param tableRows - Array of table row strings including header, separator, and data rows * @returns ADF table node */ function parseMarkdownTable(tableRows) { // Create the basic table structure const tableNode = { type: 'table', attrs: { isNumberColumnEnabled: false, layout: 'default', }, content: [], }; // Process each row tableRows.forEach((row, rowIndex) => { // Skip separator row (row index 1) if (rowIndex === 1) { return; } // Split the row into cells, removing first and last empty entries from split const cells = row .split('|') .slice(1, -1) .map((cell) => cell.trim()); // Create a row node const rowNode = { type: 'tableRow', content: [], }; // Process each cell cells.forEach((cellText) => { // Determine if this is a header cell (first row) const cellType = rowIndex === 0 ? 'tableHeader' : 'tableCell'; // Create cell node const cellNode = { type: cellType, content: [ { type: 'paragraph', content: parseMarkdownText(cellText), }, ], }; // Add cell to row if (rowNode.content) { rowNode.content.push(cellNode); } }); // Add row to table if (tableNode.content && rowNode.content && rowNode.content.length > 0) { tableNode.content.push(rowNode); } }); return tableNode; }