@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
349 lines (348 loc) • 10.1 kB
JavaScript
;
/**
* Utility functions for converting Atlassian Document Format (ADF) to Markdown
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.adfToMarkdown = adfToMarkdown;
const logger_util_js_1 = require("./logger.util.js");
// Create a contextualized logger for this file
const adfLogger = logger_util_js_1.Logger.forContext('utils/adf.util.ts');
// Log ADF utility initialization
adfLogger.debug('ADF utility initialized');
/**
* Convert Atlassian Document Format (ADF) to Markdown
*
* @param adf - The ADF content to convert (can be string or object)
* @returns The converted Markdown content
*/
function adfToMarkdown(adf) {
const methodLogger = logger_util_js_1.Logger.forContext('utils/adf.util.ts', 'adfToMarkdown');
try {
// Handle empty or undefined input
if (!adf) {
return '';
}
// Parse ADF if it's a string
let adfDoc;
if (typeof adf === 'string') {
try {
adfDoc = JSON.parse(adf);
}
catch {
return adf; // Return as-is if not valid JSON
}
}
else if (typeof adf === 'object') {
adfDoc = adf;
}
else {
return String(adf);
}
// Check if it's a valid ADF document
if (!adfDoc.content || !Array.isArray(adfDoc.content)) {
return '';
}
// Process the document
const markdown = processAdfContent(adfDoc.content);
methodLogger.debug(`Converted ADF to Markdown, length: ${markdown.length}`);
return markdown;
}
catch (error) {
methodLogger.error('[src/utils/adf.util.ts@adfToMarkdown] Error converting ADF to Markdown:', error);
return '*Error converting description format*';
}
}
/**
* Process ADF content nodes
*/
function processAdfContent(content) {
if (!content || !Array.isArray(content)) {
return '';
}
return content.map((node) => processAdfNode(node)).join('\n\n');
}
/**
* Process mention node
*/
function processMention(node) {
if (!node.attrs) {
return '';
}
const text = node.attrs.text || node.attrs.displayName || '';
if (!text) {
return '';
}
// Format as @username to preserve the mention format
// Remove any existing @ symbol to avoid double @@ in the output
const cleanText = typeof text === 'string' && text.startsWith('@')
? text.substring(1)
: text;
return `@${cleanText}`;
}
/**
* Process a single ADF node
*/
function processAdfNode(node) {
if (!node || !node.type) {
return '';
}
switch (node.type) {
case 'paragraph':
return processParagraph(node);
case 'heading':
return processHeading(node);
case 'bulletList':
return processBulletList(node);
case 'orderedList':
return processOrderedList(node);
case 'listItem':
return processListItem(node);
case 'codeBlock':
return processCodeBlock(node);
case 'blockquote':
return processBlockquote(node);
case 'rule':
return '---';
case 'mediaGroup':
return processMediaGroup(node);
case 'table':
return processTable(node);
case 'text':
return processText(node);
case 'mention':
return processMention(node);
default:
// For unknown node types, try to process content if available
if (node.content) {
return processAdfContent(node.content);
}
return '';
}
}
/**
* Process paragraph node
*/
function processParagraph(node) {
if (!node.content) {
return '';
}
// Process each child node and join them with proper spacing
return node.content
.map((childNode, index) => {
// Add a space between text nodes if needed
const needsSpace = index > 0 &&
childNode.type === 'text' &&
node.content[index - 1].type === 'text' &&
!childNode.text?.startsWith(' ') &&
!node.content[index - 1].text?.endsWith(' ');
return (needsSpace ? ' ' : '') + processAdfNode(childNode);
})
.join('');
}
/**
* Process heading node
*/
function processHeading(node) {
if (!node.content || !node.attrs) {
return '';
}
const level = typeof node.attrs.level === 'number' ? node.attrs.level : 1;
const headingMarker = '#'.repeat(level);
const content = node.content
.map((childNode) => processAdfNode(childNode))
.join('');
return `${headingMarker} ${content}`;
}
/**
* Process bullet list node
*/
function processBulletList(node) {
if (!node.content) {
return '';
}
return node.content.map((item) => processAdfNode(item)).join('\n');
}
/**
* Process ordered list node
*/
function processOrderedList(node) {
if (!node.content) {
return '';
}
return node.content
.map((item, index) => {
const processedItem = processAdfNode(item);
// Replace the first "- " with "1. ", "2. ", etc.
return processedItem.replace(/^- /, `${index + 1}. `);
})
.join('\n');
}
/**
* Process list item node
*/
function processListItem(node) {
if (!node.content) {
return '';
}
const content = node.content
.map((childNode) => {
const processed = processAdfNode(childNode);
// For nested lists, add indentation
if (childNode.type === 'bulletList' ||
childNode.type === 'orderedList') {
return processed
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
}
return processed;
})
.join('\n');
return `- ${content}`;
}
/**
* Process code block node
*/
function processCodeBlock(node) {
if (!node.content) {
return '```\n```';
}
const language = node.attrs?.language || '';
const code = node.content
.map((childNode) => processAdfNode(childNode))
.join('');
return `\`\`\`${language}\n${code}\n\`\`\``;
}
/**
* Process blockquote node
*/
function processBlockquote(node) {
if (!node.content) {
return '';
}
const content = node.content
.map((childNode) => processAdfNode(childNode))
.join('\n\n');
// Add > to each line
return content
.split('\n')
.map((line) => `> ${line}`)
.join('\n');
}
/**
* Process media group node
*/
function processMediaGroup(node) {
if (!node.content) {
return '';
}
return node.content
.map((mediaNode) => {
if (mediaNode.type === 'media' && mediaNode.attrs) {
const { id, type } = mediaNode.attrs;
if (type === 'file') {
return `[Attachment: ${id}]`;
}
else if (type === 'link') {
return `[External Link]`;
}
}
return '';
})
.filter(Boolean)
.join('\n');
}
/**
* Process table node
*/
function processTable(node) {
if (!node.content) {
return '';
}
const rows = [];
// Process table rows
node.content.forEach((row) => {
if (row.type === 'tableRow' && row.content) {
const cells = [];
row.content.forEach((cell) => {
if ((cell.type === 'tableCell' ||
cell.type === 'tableHeader') &&
cell.content) {
const cellContent = cell.content
.map((cellNode) => processAdfNode(cellNode))
.join('');
cells.push(cellContent.trim());
}
});
if (cells.length > 0) {
rows.push(cells);
}
}
});
if (rows.length === 0) {
return '';
}
// Create markdown table
const columnCount = Math.max(...rows.map((row) => row.length));
// Ensure all rows have the same number of columns
const normalizedRows = rows.map((row) => {
while (row.length < columnCount) {
row.push('');
}
return row;
});
// Create header row
const headerRow = normalizedRows[0].map((cell) => cell || '');
// Create separator row
const separatorRow = headerRow.map(() => '---');
// Create content rows
const contentRows = normalizedRows.slice(1);
// Build the table
const tableRows = [
headerRow.join(' | '),
separatorRow.join(' | '),
...contentRows.map((row) => row.join(' | ')),
];
return tableRows.join('\n');
}
/**
* Process text node
*/
function processText(node) {
if (!node.text) {
return '';
}
let text = node.text;
// Apply marks if available
if (node.marks && node.marks.length > 0) {
// Process link marks last to avoid issues with other formatting
const linkMark = node.marks.find((mark) => mark.type === 'link');
const otherMarks = node.marks.filter((mark) => mark.type !== 'link');
// Apply non-link marks first
otherMarks.forEach((mark) => {
switch (mark.type) {
case 'strong':
text = `**${text}**`;
break;
case 'em':
text = `*${text}*`;
break;
case 'code':
text = `\`${text}\``;
break;
case 'strike':
text = `~~${text}~~`;
break;
case 'underline':
// Markdown doesn't support underline, use emphasis instead
text = `_${text}_`;
break;
}
});
// Apply link mark last
if (linkMark && linkMark.attrs && linkMark.attrs.href) {
text = `[${text}](${linkMark.attrs.href})`;
}
}
return text;
}