feat: add adfToMarkdown for bidirectional description sync

Implement adfToMarkdown, adfInlineToMarkdown, and adfBlockToMarkdown so
that pulling a Jira issue back into Obsidian converts the ADF description
to clean markdown rather than clobbering the note with raw HTML or wiki
markup. Export adfToMarkdown and register it in SAFE_GLOBALS / context so
field mapping expressions can call it directly.
This commit is contained in:
slips 2026-04-22 10:35:00 -04:00
parent 1a14ac37c7
commit 75f9277387
2 changed files with 59 additions and 1 deletions

View file

@ -5,7 +5,7 @@ import {debugLog} from "./debugLogging";
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
import {defaultIssue} from "../default/defaultIssue";
import {jiraToMarkdown, markdownToJira} from "./markdownHtml";
import {markdownToAdf} from "./markdownToAdf";
import {markdownToAdf, adfToMarkdown} from "./markdownToAdf";
// Constants for validation and error messages
const FORBIDDEN_PATTERNS = ["document", "window", "eval", "Function", "fetch", "setTimeout", "globalThis"];
@ -14,6 +14,7 @@ const SAFE_GLOBALS = {
jiraToMarkdown,
markdownToJira,
markdownToAdf,
adfToMarkdown,
JSON: {
parse: JSON.parse,
stringify: JSON.stringify
@ -178,6 +179,7 @@ export async function safeStringToFunction(
jiraToMarkdown,
markdownToJira,
markdownToAdf,
adfToMarkdown,
JSON,
Math,
Date,

View file

@ -197,3 +197,59 @@ export function markdownToAdf(markdown: string): AdfDoc | null {
return content.length > 0 ? { version: 1, type: 'doc', content } : null;
}
function adfInlineToMarkdown(nodes: any[]): string {
if (!nodes) return '';
return nodes.map(node => {
if (node.type === 'hardBreak') return '\n';
if (node.type !== 'text') return '';
const text = node.text || '';
const marks: string[] = (node.marks || []).map((m: any) => m.type);
let result = text;
if (marks.includes('code')) return `\`${result}\``;
if (marks.includes('strong')) result = `**${result}**`;
if (marks.includes('em')) result = `*${result}*`;
return result;
}).join('');
}
function adfBlockToMarkdown(node: any): string {
if (!node) return '';
switch (node.type) {
case 'heading': {
const level = node.attrs?.level || 1;
const text = adfInlineToMarkdown(node.content || []);
return `${'#'.repeat(level)} ${text}`;
}
case 'paragraph': {
const text = adfInlineToMarkdown(node.content || []);
return text;
}
case 'codeBlock': {
const lang = node.attrs?.language || '';
const code = (node.content || []).map((n: any) => n.text || '').join('');
return `\`\`\`${lang}\n${code}\n\`\`\``;
}
case 'bulletList': {
return (node.content || []).map((item: any) =>
`- ${(item.content || []).map((block: any) => adfBlockToMarkdown(block)).join('\n')}`
).join('\n');
}
case 'orderedList': {
return (node.content || []).map((item: any, i: number) =>
`${i + 1}. ${(item.content || []).map((block: any) => adfBlockToMarkdown(block)).join('\n')}`
).join('\n');
}
case 'rule':
return '---';
default:
return (node.content || []).map((n: any) => adfBlockToMarkdown(n)).join('\n');
}
}
export function adfToMarkdown(adf: any): string {
if (!adf || typeof adf !== 'object') return '';
const blocks: string[] = (adf.content || []).map((node: any) => adfBlockToMarkdown(node));
return blocks.filter(b => b !== '').join('\n\n');
}