@aashari/mcp-server-atlassian-bitbucket
Version:
Node.js/TypeScript MCP server for Atlassian Bitbucket. Enables AI systems (LLMs) to interact with workspaces, repositories, and pull requests via tools (list, get, comment, search). Connects AI directly to version control workflows through the standard MC
344 lines (343 loc) • 9.78 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 file-level logger for the module
const adfLogger = logger_util_js_1.Logger.forContext('utils/adf.util.ts');
/**
* 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) {
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
return processAdfContent(adfDoc.content);
}
catch (error) {
adfLogger.error('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;
}