UNPKG

marklassian

Version:

Converts markdown to the Atlassian Document Format (ADF)

287 lines 8.82 kB
import { marked } from "marked"; export function markdownToAdf(markdown) { const tokens = marked.lexer(markdown); return { version: 1, type: "doc", content: tokensToAdf(tokens), }; } function tokensToAdf(tokens) { if (!tokens) return []; return tokens .map((token) => { switch (token.type) { case "paragraph": return processParagraph(token.tokens); case "heading": return { type: "heading", attrs: { level: token.depth }, content: inlineToAdf(token.tokens), }; case "list": return { type: token.ordered ? "orderedList" : "bulletList", ...(token.ordered ? { attrs: { order: token.start || 1 } } : {}), content: token.items.map((item) => processListItem(item)), }; case "code": return { type: "codeBlock", attrs: { language: token.lang || "text" }, content: [ { type: "text", text: token.text, }, ], }; case "blockquote": return { type: "blockquote", content: tokensToAdf(token.tokens), }; case "hr": return { type: "rule" }; case "table": return processTable(token); default: return null; } }) .filter(Boolean) .flat(); } function createMediaNode(token) { return { type: "mediaSingle", attrs: { layout: "center", }, content: [ { type: "media", attrs: { type: "external", url: token.href, alt: token.text || "", }, }, ], }; } function processTable(token) { const headers = token.header.map((header) => ({ type: "tableHeader", content: processParagraph(header.tokens), })); const rows = token.rows.map((row) => ({ type: "tableRow", content: row.map((cell) => ({ type: "tableCell", content: processParagraph(cell.tokens), })), })); const content = []; if (headers.length) { content.push({ type: "tableRow", content: headers, }); } return { type: "table", content: content.concat(rows), }; } function processParagraph(tokens) { if (!tokens) return []; if (tokens.length === 1 && tokens[0]?.type === "image") { return [createMediaNode(tokens[0])]; } const outputNodes = []; let currentParagraphTokens = []; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token?.type === "image") { if (currentParagraphTokens.length) { outputNodes.push({ type: "paragraph", content: inlineToAdf(currentParagraphTokens), }); currentParagraphTokens = []; } outputNodes.push(createMediaNode(token)); } else { currentParagraphTokens.push(token); } } if (currentParagraphTokens.length) { outputNodes.push({ type: "paragraph", content: inlineToAdf(currentParagraphTokens), }); } return outputNodes; } function processListItem(item) { const itemContent = []; let currentParagraphTokens = []; (item.tokens || []).forEach((token) => { if (token.type === "text" || token.type === "em" || token.type === "strong" || token.type === "del" || token.type === "link" || token.type === "codespan") { currentParagraphTokens.push(token); } else { if (currentParagraphTokens.length) { itemContent.push({ type: "paragraph", content: inlineToAdf(currentParagraphTokens), }); currentParagraphTokens = []; } if (token.type === "list") { itemContent.push({ type: token.ordered ? "orderedList" : "bulletList", ...(token.ordered ? { attrs: { order: token.start || 1 } } : {}), content: token.items.map((nestedItem) => processListItem(nestedItem)), }); } else { const processed = tokensToAdf([token]); if (processed.length) { itemContent.push(...processed); } } } }); if (currentParagraphTokens.length) { itemContent.push({ type: "paragraph", content: inlineToAdf(currentParagraphTokens), }); } return { type: "listItem", content: itemContent, }; } function getSafeText(token) { if (token.tokens?.length === 1 && token.tokens[0] && "text" in token.tokens[0]) { return getSafeText(token.tokens[0]); } if ("text" in token) { return token.text .replace(/\n$/, "") .replace(/\n/g, " ") .replace(/\s+/g, " "); } return ""; } function getMarks(token, marks = {}) { if (token.type === "em" && !marks.em) { marks.em = { type: "em" }; } if (token.type === "strong" && !marks.strong) { marks.strong = { type: "strong" }; } if (token.type === "del" && !marks.strike) { marks.strike = { type: "strike" }; } if (token.type === "link") { marks.link = { type: "link", attrs: { href: token.href }, }; } if (token.type === "codespan" && !marks.code) { marks.code = { type: "code" }; } const nextToken = token.tokens?.[0]; const tokensLength = token.tokens?.length ?? 0; // Only continue recursion if there is only one nested token if (nextToken && tokensLength === 1) { return getMarks(nextToken, marks); } const resolvedMarks = Object.values(marks); if (marks.code) { // Code Inline mark only supports a link or annotation mark return resolvedMarks.filter((mark) => mark.type === "link" || mark.type === "code"); } return resolvedMarks; } function inlineToAdf(tokens) { if (!tokens) return []; return tokens .flatMap((token) => { switch (token.type) { case "text": if (token.tokens) { return inlineToAdf(token.tokens); } return [ { type: "text", text: getSafeText(token), ...(token.tokens ? { content: inlineToAdf(token.tokens) } : {}), }, ]; case "em": return (token.tokens ?? []).map((t) => ({ type: "text", text: getSafeText(t), marks: getMarks(t, { em: { type: "em" } }), })); case "strong": return (token.tokens ?? []).map((t) => ({ type: "text", text: getSafeText(t), marks: getMarks(t, { strong: { type: "strong" } }), })); case "del": return (token.tokens ?? []).map((t) => ({ type: "text", text: getSafeText(t), marks: getMarks(t, { strike: { type: "strike" } }), })); case "link": return [ { type: "text", text: getSafeText(token), marks: getMarks(token), }, ]; case "codespan": return [ { type: "text", text: getSafeText(token), marks: getMarks(token), }, ]; case "br": return [{ type: "hardBreak" }]; default: return []; } }) .filter((node) => { if (node.type === "text" && !node.text) { return false; } return true; }); } //# sourceMappingURL=index.js.map