mirror of
https://github.com/alamion/obsidian-jira-sync.git
synced 2026-07-22 05:43:04 +00:00
Feat:
- @slipstyle's ADF converter (tweaked) - More broad description of Field Mapping settings section - a few minor fixes of how lambda js functions convert into strings
This commit is contained in:
commit
6e35db6254
8 changed files with 505 additions and 31 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import { JiraIssue } from '../interfaces';
|
||||
import { jiraToMarkdown } from '../tools/markdownHtml';
|
||||
import { adfToMarkdown } from '../tools/markdownToAdf';
|
||||
|
||||
export interface FieldMapping {
|
||||
toJira: (value: any) => any;
|
||||
|
|
@ -12,7 +14,7 @@ export const obsidianJiraFieldMappings: Record<string, FieldMapping> = {
|
|||
},
|
||||
description: {
|
||||
toJira: () => null,
|
||||
fromJira: (issue) => issue.fields.description,
|
||||
fromJira: (issue) => jiraToMarkdown(issue.fields.description),
|
||||
},
|
||||
key: {
|
||||
toJira: () => null,
|
||||
|
|
@ -74,4 +76,23 @@ export const obsidianJiraFieldMappings: Record<string, FieldMapping> = {
|
|||
toJira: () => null,
|
||||
fromJira: (issue) => issue.fields.aggregateprogress.percent + '%',
|
||||
},
|
||||
comments: {
|
||||
toJira: () => null,
|
||||
fromJira: (issue) => {
|
||||
const comments = issue.fields.comment?.comments;
|
||||
if (!comments?.length) return '';
|
||||
return comments
|
||||
.map((c: any) => {
|
||||
const author = c.author?.displayName ?? 'Unknown';
|
||||
const date = c.created ? c.created.replace('T', ' ').substring(0, 19) : '';
|
||||
const body = adfToMarkdown(c.body) ?? '';
|
||||
const calloutBody = body
|
||||
.split('\n')
|
||||
.map((l: string) => (l === '' ? '>' : `> ${l}`))
|
||||
.join('\n');
|
||||
return `> [!note]+ ${author} — ${date}\n> \n${calloutBody}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
title: Field mappings
|
||||
desc: Configure how fields are mapped between Obsidian and Jira. Each field requires both a 'To Jira' and 'From Jira' transformation function.
|
||||
desc: |
|
||||
Configure how fields are mapped between Obsidian and Jira. Each field requires both a 'To Jira' and 'From Jira' transformation function.
|
||||
|
||||
To Jira:
|
||||
- value - the value of field in your .md file - can be a text string from file contents or a field value from frontmatter
|
||||
|
||||
From Jira:
|
||||
- issue - raw data from Jira (can be seen in lower `Fetch issue settings` section)
|
||||
- data_source - either parsed sections or frontmatter data in dictionary format, where keys are field names and values are field values
|
||||
fv:
|
||||
name: Enable field validation
|
||||
desc: Check fields before saving based on the standard API response (validation does not account for custom fields)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
title: Сопоставление полей
|
||||
desc: Настройте, как поля будут сопоставляться между Obsidian и Jira. Для каждого
|
||||
поля требуется функция преобразования 'В Jira' и 'Из Jira'.
|
||||
desc: |
|
||||
Настройте сопоставление полей между Obsidian и Jira. Для каждого поля необходимо указать функции преобразования "В Jira" и "Из Jira".
|
||||
|
||||
В Jira:
|
||||
- value — значение поля в .md файле — это может быть текст из содержимого файла или значение поля из frontmatter
|
||||
|
||||
Из Jira:
|
||||
- issue — необработанные данные из Jira (можно увидеть в нижнем разделе "Настройка получаемых полей")
|
||||
- data_source — либо спаршенные секции данных из файла, либо данные frontmatter. Показывается в формате словаря, где ключи — это названия полей, а значения — данные из файла
|
||||
fv:
|
||||
name: Включить проверку полей
|
||||
desc: Проверять поля перед сохранением базируясь на стандартном ответе API
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export class FieldMappingsComponent implements SettingsComponent {
|
|||
// Add explanation
|
||||
mappingSection.createEl('p', {
|
||||
text: t('desc'),
|
||||
cls: 'break-line',
|
||||
});
|
||||
|
||||
// Add validation toggle
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { debugLog } from './debugLogging';
|
|||
import { FieldMapping } from '../default/obsidianJiraFieldsMapping';
|
||||
import { defaultIssue } from '../default/defaultIssue';
|
||||
import { jiraToMarkdown, markdownToJira } from './markdownHtml';
|
||||
import { markdownToAdf, adfToMarkdown } from './markdownToAdf';
|
||||
|
||||
// Constants for validation and error messages
|
||||
const FORBIDDEN_PATTERNS = ['document', 'window', 'eval', 'Function', 'fetch', 'setTimeout', 'globalThis'];
|
||||
|
|
@ -12,6 +13,8 @@ const SYNTAX_KEYWORDS = ['return', 'if', 'else', 'for', 'while', 'switch', 'try'
|
|||
const SAFE_GLOBALS = {
|
||||
jiraToMarkdown,
|
||||
markdownToJira,
|
||||
markdownToAdf,
|
||||
adfToMarkdown,
|
||||
JSON: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
|
|
@ -200,6 +203,8 @@ export async function safeStringToFunction(
|
|||
const context = {
|
||||
jiraToMarkdown,
|
||||
markdownToJira,
|
||||
markdownToAdf,
|
||||
adfToMarkdown,
|
||||
JSON,
|
||||
Math,
|
||||
Date,
|
||||
|
|
@ -263,39 +268,34 @@ export function jiraFunctionToString(fn: (...args: any[]) => any, isFromJira: bo
|
|||
|
||||
if (!baseStr) return baseStr;
|
||||
|
||||
const fnStr = fn.toString().trim();
|
||||
|
||||
const paramMatch = fnStr.match(/^\s*(?:\(?([^)]*)\)?\s*=>|\s*function\s*\(([^)]*)\))/);
|
||||
if (paramMatch) {
|
||||
if (isFromJira) {
|
||||
const fnStr = fn.toString().trim();
|
||||
const params = (paramMatch[1] || paramMatch[2] || '').split(',').map((p) => p.trim());
|
||||
|
||||
const paramMatch = fnStr.match(/^\s*(?:\(?([^)]*)\)?\s*=>|\s*function\s*\(([^)]*)\))/);
|
||||
if (paramMatch) {
|
||||
const params = (paramMatch[1] || paramMatch[2] || '').split(',').map((p) => p.trim());
|
||||
let resultStr = baseStr;
|
||||
|
||||
let resultStr = baseStr;
|
||||
|
||||
if (params.length >= 1 && params[0]) {
|
||||
const regex1 = new RegExp(`\\b${params[0]}\\b`, 'g');
|
||||
resultStr = resultStr.replace(regex1, 'issue');
|
||||
}
|
||||
if (params.length >= 2 && params[1]) {
|
||||
const regex2 = new RegExp(`\\b${params[1]}\\b`, 'g');
|
||||
resultStr = resultStr.replace(regex2, 'data_source');
|
||||
}
|
||||
|
||||
return resultStr;
|
||||
if (params.length >= 1 && params[0]) {
|
||||
const regex1 = new RegExp(`\\b${params[0]}\\b`, 'g');
|
||||
resultStr = resultStr.replace(regex1, 'issue');
|
||||
}
|
||||
if (params.length >= 2 && params[1]) {
|
||||
const regex2 = new RegExp(`\\b${params[1]}\\b`, 'g');
|
||||
resultStr = resultStr.replace(regex2, 'data_source');
|
||||
}
|
||||
|
||||
return resultStr;
|
||||
} else {
|
||||
const fnStr = fn.toString().trim();
|
||||
const param = (paramMatch[1] || paramMatch[2] || '').trim();
|
||||
|
||||
const paramMatch = fnStr.match(/^\s*(?:\(?([^)]*)\)?\s*=>|\s*function\s*\(([^)]*)\))/);
|
||||
if (paramMatch) {
|
||||
const param = (paramMatch[1] || paramMatch[2] || '').trim();
|
||||
|
||||
if (param) {
|
||||
const regex = new RegExp(`\\b${param}\\b`, 'g');
|
||||
return baseStr.replace(regex, 'value');
|
||||
}
|
||||
if (param) {
|
||||
const regex = new RegExp(`\\b${param}\\b`, 'g');
|
||||
return baseStr.replace(regex, 'value');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return baseStr;
|
||||
}
|
||||
|
|
@ -303,6 +303,10 @@ export function jiraFunctionToString(fn: (...args: any[]) => any, isFromJira: bo
|
|||
export function functionToExpressionString(fn: (...args: any[]) => any): string {
|
||||
try {
|
||||
const fnStr = fn.toString().trim();
|
||||
if (fnStr.contains("\n")) { // return everything for multiline funcs
|
||||
return fnStr.split('\n').map((s: string) => s.trim()).join('\n');
|
||||
}
|
||||
|
||||
|
||||
// Handle arrow functions
|
||||
const arrowMatch = fnStr.match(/^\s*\(?([^)]*)\)?\s*=>\s*(.+)$/s);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { JiraIssue } from '../interfaces';
|
||||
import { jiraToMarkdown } from './markdownHtml';
|
||||
import { Notice, TFile } from 'obsidian';
|
||||
import JiraPlugin from '../main';
|
||||
import { extractAllJiraSyncValuesFromContent, updateJiraSyncContent } from './sectionTools';
|
||||
|
|
@ -67,7 +66,7 @@ export async function updateJiraToLocal(plugin: JiraPlugin, file: TFile, issue:
|
|||
let updatedContent = fileContent;
|
||||
let updatesDict: Record<string, string> = {};
|
||||
for (const [fieldName, fieldValue] of Object.entries(syncSections)) {
|
||||
updatesDict[fieldName] = jiraToMarkdown(fieldValue);
|
||||
updatesDict[fieldName] = fieldValue;
|
||||
}
|
||||
|
||||
debugLog(`Updating sync sections: ${JSON.stringify(updatesDict)}`);
|
||||
|
|
|
|||
430
src/tools/markdownToAdf.ts
Normal file
430
src/tools/markdownToAdf.ts
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
interface AdfTextMark {
|
||||
type: 'strong' | 'em' | 'code' | 'link';
|
||||
attrs?: { href: string };
|
||||
}
|
||||
|
||||
interface AdfTextNode {
|
||||
type: 'text';
|
||||
text: string;
|
||||
marks?: AdfTextMark[];
|
||||
}
|
||||
|
||||
interface AdfInlineNode {
|
||||
type: 'hardBreak';
|
||||
}
|
||||
|
||||
type AdfInlineContent = AdfTextNode | AdfInlineNode;
|
||||
|
||||
interface AdfParagraphNode {
|
||||
type: 'paragraph';
|
||||
content: AdfInlineContent[];
|
||||
}
|
||||
|
||||
interface AdfHeadingNode {
|
||||
type: 'heading';
|
||||
attrs: { level: number };
|
||||
content: AdfInlineContent[];
|
||||
}
|
||||
|
||||
interface AdfCodeBlockNode {
|
||||
type: 'codeBlock';
|
||||
attrs: { language: string };
|
||||
content: [{ type: 'text'; text: string }];
|
||||
}
|
||||
|
||||
interface AdfListItemNode {
|
||||
type: 'listItem';
|
||||
content: [AdfParagraphNode];
|
||||
}
|
||||
|
||||
interface AdfBulletListNode {
|
||||
type: 'bulletList';
|
||||
content: AdfListItemNode[];
|
||||
}
|
||||
|
||||
interface AdfOrderedListNode {
|
||||
type: 'orderedList';
|
||||
content: AdfListItemNode[];
|
||||
}
|
||||
|
||||
interface AdfTaskListNode {
|
||||
type: 'taskList';
|
||||
content: AdfListItemNode[];
|
||||
attrs?: { localId: string };
|
||||
}
|
||||
|
||||
interface AdfRuleNode {
|
||||
type: 'rule';
|
||||
}
|
||||
|
||||
type AdfBlockNode =
|
||||
| AdfParagraphNode
|
||||
| AdfHeadingNode
|
||||
| AdfCodeBlockNode
|
||||
| AdfBulletListNode
|
||||
| AdfOrderedListNode
|
||||
| AdfTaskListNode
|
||||
| AdfRuleNode;
|
||||
|
||||
interface AdfDoc {
|
||||
version: 1;
|
||||
type: 'doc';
|
||||
content: AdfBlockNode[];
|
||||
}
|
||||
|
||||
function parseInline(text: string): AdfInlineContent[] {
|
||||
const nodes: AdfInlineContent[] = [];
|
||||
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|\[\[[^\]]+\]\])/g;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
nodes.push({ type: 'text', text: text.slice(lastIndex, match.index) });
|
||||
}
|
||||
if (match[2] !== undefined) {
|
||||
// **bold**
|
||||
nodes.push({ type: 'text', text: match[2], marks: [{ type: 'strong' }] });
|
||||
} else if (match[3] !== undefined) {
|
||||
// *italic*
|
||||
nodes.push({ type: 'text', text: match[3], marks: [{ type: 'em' }] });
|
||||
} else if (match[4] !== undefined) {
|
||||
// `code`
|
||||
nodes.push({ type: 'text', text: match[4], marks: [{ type: 'code' }] });
|
||||
} else if (match[5] !== undefined) {
|
||||
// [text](url) — convert to ADF link mark
|
||||
nodes.push({ type: 'text', text: match[5], marks: [{ type: 'link', attrs: { href: match[6] } }] });
|
||||
} else {
|
||||
// [[wikilink]] — preserve as plain text so round-trip survives
|
||||
nodes.push({ type: 'text', text: match[0] });
|
||||
}
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push({ type: 'text', text: text.slice(lastIndex) });
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : [{ type: 'text', text }];
|
||||
}
|
||||
|
||||
export function markdownToAdf(markdown: string): AdfDoc | null {
|
||||
if (!markdown || !markdown.trim()) return null;
|
||||
|
||||
const lines = markdown.split('\n');
|
||||
const content: AdfBlockNode[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// Fenced code block
|
||||
if (line.startsWith('```')) {
|
||||
const lang = line.slice(3).trim();
|
||||
const codeLines: string[] = [];
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].startsWith('```')) {
|
||||
codeLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
content.push({
|
||||
type: 'codeBlock',
|
||||
attrs: { language: lang },
|
||||
content: [{ type: 'text', text: codeLines.join('\n') }],
|
||||
});
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Heading
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
|
||||
if (headingMatch) {
|
||||
content.push({
|
||||
type: 'heading',
|
||||
attrs: { level: headingMatch[1].length },
|
||||
content: parseInline(headingMatch[2]),
|
||||
});
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (line.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
|
||||
content.push({ type: 'rule' });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Task list (checkboxes) — must be checked before bullet list
|
||||
if (line.match(/^[-*+]\s+\[[ xX]\]\s*/)) {
|
||||
const items: any[] = [];
|
||||
let taskCounter = 0;
|
||||
while (i < lines.length && lines[i].match(/^[-*+]\s+\[[ xX]\]\s*/)) {
|
||||
const checkMatch = lines[i].match(/^[-*+]\s+\[([ xX])\]\s*(.*)/);
|
||||
if (checkMatch) {
|
||||
const state = checkMatch[1].toLowerCase() === 'x' ? 'DONE' : 'TODO';
|
||||
// taskItem only allows inline nodes — flatten sub-items as hardBreak + inline
|
||||
const inlineContent: AdfInlineContent[] = parseInline(checkMatch[2]);
|
||||
i++;
|
||||
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
|
||||
inlineContent.push({ type: 'hardBreak' });
|
||||
inlineContent.push(...parseInline('- ' + lines[i].replace(/^\s+[-*+]\s+/, '')));
|
||||
i++;
|
||||
}
|
||||
items.push({
|
||||
type: 'taskItem',
|
||||
attrs: { localId: `task-${Date.now()}-${taskCounter++}`, state },
|
||||
content: inlineContent,
|
||||
});
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
content.push({ type: 'taskList', attrs: { localId: `tasklist-${Date.now()}` }, content: items });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bullet list
|
||||
if (line.match(/^[-*+]\s+/)) {
|
||||
const items: AdfListItemNode[] = [];
|
||||
while (i < lines.length && lines[i].match(/^[-*+]\s+/)) {
|
||||
const itemContent: any[] = [
|
||||
{ type: 'paragraph', content: parseInline(lines[i].replace(/^[-*+]\s+/, '')) },
|
||||
];
|
||||
i++;
|
||||
const subItems: AdfListItemNode[] = [];
|
||||
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
|
||||
subItems.push({
|
||||
type: 'listItem',
|
||||
content: [{ type: 'paragraph', content: parseInline(lines[i].replace(/^\s+[-*+]\s+/, '')) }],
|
||||
});
|
||||
i++;
|
||||
}
|
||||
if (subItems.length > 0) {
|
||||
itemContent.push({ type: 'bulletList', content: subItems });
|
||||
}
|
||||
items.push({ type: 'listItem', content: itemContent as [AdfParagraphNode] });
|
||||
}
|
||||
content.push({ type: 'bulletList', content: items });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered list
|
||||
if (line.match(/^\d+\.\s+/)) {
|
||||
const items: AdfListItemNode[] = [];
|
||||
while (i < lines.length && lines[i].match(/^\d+\.\s+/)) {
|
||||
const itemContent: any[] = [
|
||||
{ type: 'paragraph', content: parseInline(lines[i].replace(/^\d+\.\s+/, '')) },
|
||||
];
|
||||
i++;
|
||||
const subItems: AdfListItemNode[] = [];
|
||||
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
|
||||
subItems.push({
|
||||
type: 'listItem',
|
||||
content: [{ type: 'paragraph', content: parseInline(lines[i].replace(/^\s+[-*+]\s+/, '')) }],
|
||||
});
|
||||
i++;
|
||||
}
|
||||
if (subItems.length > 0) {
|
||||
itemContent.push({ type: 'bulletList', content: subItems });
|
||||
}
|
||||
items.push({ type: 'listItem', content: itemContent as [AdfParagraphNode] });
|
||||
}
|
||||
content.push({ type: 'orderedList', content: items });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Empty line
|
||||
if (line.trim() === '') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Paragraph — collect until empty line or block-level element
|
||||
const paraLines: string[] = [];
|
||||
while (
|
||||
i < lines.length &&
|
||||
lines[i].trim() !== '' &&
|
||||
!lines[i].match(/^#{1,6}\s/) &&
|
||||
!lines[i].match(/^[-*+]\s+\[[ xX]\]\s*/) &&
|
||||
!lines[i].match(/^[-*+]\s/) &&
|
||||
!lines[i].match(/^\d+\.\s/) &&
|
||||
!lines[i].startsWith('```') &&
|
||||
!lines[i].match(/^(-{3,}|\*{3,}|_{3,})$/)
|
||||
) {
|
||||
paraLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (paraLines.length > 0) {
|
||||
content.push({
|
||||
type: 'paragraph',
|
||||
content: parseInline(paraLines.join('\n')),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 === 'mention') return node.attrs?.text || node.attrs?.displayName || '';
|
||||
if (node.type === 'emoji') return node.attrs?.text || node.attrs?.shortName || '';
|
||||
if (node.type === 'inlineCard') {
|
||||
const url = node.attrs?.url || '';
|
||||
return url ? `[${url}](${url})` : '';
|
||||
}
|
||||
if (node.type !== 'text') return '';
|
||||
const text = node.text || '';
|
||||
const marks: string[] = (node.marks || []).map((m: any) => m.type);
|
||||
const linkMark = (node.marks || []).find((m: any) => m.type === 'link');
|
||||
let result = text;
|
||||
if (marks.includes('code')) return `\`${result}\``;
|
||||
if (marks.includes('strike')) result = `~~${result}~~`;
|
||||
if (marks.includes('strong')) result = `**${result}**`;
|
||||
if (marks.includes('em')) result = `*${result}*`;
|
||||
if (linkMark) result = `[${result}](${linkMark.attrs?.href || ''})`;
|
||||
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) => {
|
||||
const blocks: any[] = item.content || [];
|
||||
const first = blocks[0];
|
||||
const rest = blocks.slice(1);
|
||||
const mainText = first ? adfBlockToMarkdown(first) : '';
|
||||
const nested = rest
|
||||
.map((b: any) => adfBlockToMarkdown(b))
|
||||
.filter((s: string) => s.trim() !== '')
|
||||
.map((s: string) =>
|
||||
s
|
||||
.split('\n')
|
||||
.map((l: string) => ' ' + l)
|
||||
.join('\n'),
|
||||
)
|
||||
.join('\n');
|
||||
return `- ${mainText}${nested ? '\n' + nested : ''}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
case 'orderedList': {
|
||||
return (node.content || [])
|
||||
.map((item: any, idx: number) => {
|
||||
const blocks: any[] = item.content || [];
|
||||
const first = blocks[0];
|
||||
const rest = blocks.slice(1);
|
||||
const mainText = first ? adfBlockToMarkdown(first) : '';
|
||||
const nested = rest
|
||||
.map((b: any) => adfBlockToMarkdown(b))
|
||||
.filter((s: string) => s.trim() !== '')
|
||||
.map((s: string) =>
|
||||
s
|
||||
.split('\n')
|
||||
.map((l: string) => ' ' + l)
|
||||
.join('\n'),
|
||||
)
|
||||
.join('\n');
|
||||
return `${idx + 1}. ${mainText}${nested ? '\n' + nested : ''}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
case 'taskList': {
|
||||
return (node.content || [])
|
||||
.map((item: any) => {
|
||||
const checked = item.attrs?.state === 'DONE';
|
||||
const blocks: any[] = item.content || [];
|
||||
const first = blocks[0];
|
||||
const rest = blocks.slice(1);
|
||||
const rawText =
|
||||
first?.type === 'paragraph'
|
||||
? adfInlineToMarkdown(first.content || [])
|
||||
: adfInlineToMarkdown(blocks);
|
||||
// Indent continuation lines (e.g. hardBreak + sub-item text)
|
||||
const textLines = rawText.split('\n');
|
||||
const fullText =
|
||||
textLines[0] +
|
||||
(textLines.slice(1).length
|
||||
? '\n' +
|
||||
textLines
|
||||
.slice(1)
|
||||
.map((l: string) => ' ' + l)
|
||||
.join('\n')
|
||||
: '');
|
||||
const nested = rest
|
||||
.map((b: any) => adfBlockToMarkdown(b))
|
||||
.filter((s: string) => s.trim() !== '')
|
||||
.map((s: string) =>
|
||||
s
|
||||
.split('\n')
|
||||
.map((l: string) => ' ' + l)
|
||||
.join('\n'),
|
||||
)
|
||||
.join('\n');
|
||||
return `- [${checked ? 'x' : ' '}] ${fullText}${nested ? '\n' + nested : ''}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
case 'rule':
|
||||
return '---';
|
||||
case 'blockquote': {
|
||||
const inner = (node.content || []).map((n: any) => adfBlockToMarkdown(n)).join('\n');
|
||||
return inner
|
||||
.split('\n')
|
||||
.map((line: string) => `> ${line}`)
|
||||
.join('\n');
|
||||
}
|
||||
case 'table': {
|
||||
const rows: any[] = node.content || [];
|
||||
const mdRows = rows.map((row: any) => {
|
||||
const cells = (row.content || []).map((cell: any) =>
|
||||
(cell.content || [])
|
||||
.map((n: any) => adfBlockToMarkdown(n))
|
||||
.join(' ')
|
||||
.replace(/\|/g, '\\|'),
|
||||
);
|
||||
return `| ${cells.join(' | ')} |`;
|
||||
});
|
||||
if (mdRows.length === 0) return '';
|
||||
const sep = `| ${rows[0].content.map(() => '---').join(' | ')} |`;
|
||||
return [mdRows[0], sep, ...mdRows.slice(1)].join('\n');
|
||||
}
|
||||
case 'panel':
|
||||
case 'expand':
|
||||
case 'layoutSection':
|
||||
case 'layoutColumn':
|
||||
return (node.content || []).map((n: any) => adfBlockToMarkdown(n)).join('\n\n');
|
||||
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');
|
||||
}
|
||||
|
|
@ -1470,3 +1470,7 @@ code.hljs {
|
|||
background: var(--interactive-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.break-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue