feat: Add Markdown to ProseMirror converter
- New converter supports headings, lists, blockquotes, code blocks - Documents now render with proper formatting in Outline - Auto-update collection documentStructure on document creation - Documents appear in sidebar automatically Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -2,6 +2,20 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.3.14] - 2026-01-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Markdown to ProseMirror Converter:** New `src/utils/markdown-to-prosemirror.ts`
|
||||||
|
- Converts Markdown text to ProseMirror JSON format
|
||||||
|
- Supports: headings, paragraphs, lists, checkboxes, blockquotes, code blocks, hr
|
||||||
|
- Supports inline: bold, italic, links, inline code
|
||||||
|
- Documents now render with proper formatting in Outline
|
||||||
|
|
||||||
|
- **Auto-update documentStructure:** `create_document` now updates collection's `documentStructure`
|
||||||
|
- New documents automatically appear in collection sidebar
|
||||||
|
- No manual database intervention needed
|
||||||
|
|
||||||
## [1.3.13] - 2026-01-31
|
## [1.3.13] - 2026-01-31
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
MCP server for direct PostgreSQL access to Outline Wiki database. Follows patterns established by `mcp-desk-crm-sql-v3`.
|
MCP server for direct PostgreSQL access to Outline Wiki database. Follows patterns established by `mcp-desk-crm-sql-v3`.
|
||||||
|
|
||||||
**Version:** 1.3.13
|
**Version:** 1.3.14
|
||||||
**Total Tools:** 164 tools across 33 modules
|
**Total Tools:** 164 tools across 33 modules
|
||||||
**Production:** hub.descomplicar.pt (via SSH tunnel)
|
**Production:** hub.descomplicar.pt (via SSH tunnel)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# MCP Outline PostgreSQL - Continuacao de Testes
|
# MCP Outline PostgreSQL - Continuacao de Testes
|
||||||
|
|
||||||
**Ultima Sessao:** 2026-01-31 (actualizado)
|
**Ultima Sessao:** 2026-01-31 (actualizado)
|
||||||
**Versao Actual:** 1.3.13
|
**Versao Actual:** 1.3.14
|
||||||
**Progresso:** ~95/164 tools testadas (58%) - **CÓDIGO VALIDADO**
|
**Progresso:** ~95/164 tools testadas (58%) - **CÓDIGO VALIDADO**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-outline-postgresql",
|
"name": "mcp-outline-postgresql",
|
||||||
"version": "1.3.13",
|
"version": "1.3.14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mcp-outline-postgresql",
|
"name": "mcp-outline-postgresql",
|
||||||
"version": "1.3.13",
|
"version": "1.3.14",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-outline-postgresql",
|
"name": "mcp-outline-postgresql",
|
||||||
"version": "1.3.13",
|
"version": "1.3.14",
|
||||||
"description": "MCP Server for Outline Wiki via PostgreSQL direct access",
|
"description": "MCP Server for Outline Wiki via PostgreSQL direct access",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ async function main() {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
transport: 'streamable-http',
|
transport: 'streamable-http',
|
||||||
version: '1.3.13',
|
version: '1.3.14',
|
||||||
sessions: sessions.size,
|
sessions: sessions.size,
|
||||||
stateful: STATEFUL,
|
stateful: STATEFUL,
|
||||||
tools: allTools.length
|
tools: allTools.length
|
||||||
@@ -101,7 +101,7 @@ async function main() {
|
|||||||
// Create MCP server
|
// Create MCP server
|
||||||
const server = createMcpServer(pgClient.getPool(), {
|
const server = createMcpServer(pgClient.getPool(), {
|
||||||
name: 'mcp-outline-http',
|
name: 'mcp-outline-http',
|
||||||
version: '1.3.13'
|
version: '1.3.14'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track session if stateful
|
// Track session if stateful
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async function main() {
|
|||||||
// Create MCP server with shared configuration
|
// Create MCP server with shared configuration
|
||||||
const server = createMcpServer(pgClient.getPool(), {
|
const server = createMcpServer(pgClient.getPool(), {
|
||||||
name: 'mcp-outline-postgresql',
|
name: 'mcp-outline-postgresql',
|
||||||
version: '1.3.13'
|
version: '1.3.14'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect stdio transport
|
// Connect stdio transport
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export function createMcpServer(
|
|||||||
): Server {
|
): Server {
|
||||||
const server = new Server({
|
const server = new Server({
|
||||||
name: config.name || 'mcp-outline-postgresql',
|
name: config.name || 'mcp-outline-postgresql',
|
||||||
version: config.version || '1.3.13'
|
version: config.version || '1.3.14'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set capabilities (required for MCP v2.2+)
|
// Set capabilities (required for MCP v2.2+)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
import { BaseTool, ToolResponse, DocumentArgs, GetDocumentArgs, CreateDocumentArgs, UpdateDocumentArgs, SearchDocumentsArgs, MoveDocumentArgs } from '../types/tools.js';
|
import { BaseTool, ToolResponse, DocumentArgs, GetDocumentArgs, CreateDocumentArgs, UpdateDocumentArgs, SearchDocumentsArgs, MoveDocumentArgs } from '../types/tools.js';
|
||||||
import { validatePagination, validateSortDirection, validateSortField, isValidUUID, sanitizeInput } from '../utils/security.js';
|
import { validatePagination, validateSortDirection, validateSortField, isValidUUID, sanitizeInput } from '../utils/security.js';
|
||||||
|
import { markdownToProseMirror } from '../utils/markdown-to-prosemirror.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. list_documents - Lista documentos publicados e drafts com filtros e paginação
|
* 1. list_documents - Lista documentos publicados e drafts com filtros e paginação
|
||||||
@@ -240,6 +241,9 @@ const createDocument: BaseTool<CreateDocumentArgs> = {
|
|||||||
await pgClient.query('BEGIN');
|
await pgClient.query('BEGIN');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Convert Markdown to ProseMirror JSON
|
||||||
|
const proseMirrorContent = markdownToProseMirror(text);
|
||||||
|
|
||||||
const docQuery = `
|
const docQuery = `
|
||||||
INSERT INTO documents (
|
INSERT INTO documents (
|
||||||
id, "urlId", title, text, "collectionId", "teamId", "parentDocumentId", "createdById",
|
id, "urlId", title, text, "collectionId", "teamId", "parentDocumentId", "createdById",
|
||||||
@@ -250,7 +254,7 @@ const createDocument: BaseTool<CreateDocumentArgs> = {
|
|||||||
gen_random_uuid(),
|
gen_random_uuid(),
|
||||||
substring(md5(random()::text) from 1 for 10),
|
substring(md5(random()::text) from 1 for 10),
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), 1, false, false, false, ARRAY[$6]::uuid[],
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), 1, false, false, false, ARRAY[$6]::uuid[],
|
||||||
1, '{"type": "doc", "content": [{"type": "paragraph"}]}'::jsonb
|
1, $10::jsonb
|
||||||
)
|
)
|
||||||
RETURNING id, "urlId", title, "collectionId", "publishedAt", "createdAt"
|
RETURNING id, "urlId", title, "collectionId", "publishedAt", "createdAt"
|
||||||
`;
|
`;
|
||||||
@@ -264,7 +268,8 @@ const createDocument: BaseTool<CreateDocumentArgs> = {
|
|||||||
userId,
|
userId,
|
||||||
userId,
|
userId,
|
||||||
args.template || false,
|
args.template || false,
|
||||||
publishedAt
|
publishedAt,
|
||||||
|
JSON.stringify(proseMirrorContent)
|
||||||
];
|
];
|
||||||
|
|
||||||
const docResult = await pgClient.query(docQuery, docParams);
|
const docResult = await pgClient.query(docQuery, docParams);
|
||||||
@@ -288,6 +293,24 @@ const createDocument: BaseTool<CreateDocumentArgs> = {
|
|||||||
text
|
text
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Update collection's documentStructure to include the new document
|
||||||
|
const urlSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||||
|
const updateStructureQuery = `
|
||||||
|
UPDATE collections
|
||||||
|
SET "documentStructure" = COALESCE("documentStructure", '[]'::jsonb) || $1::jsonb,
|
||||||
|
"updatedAt" = NOW()
|
||||||
|
WHERE id = $2
|
||||||
|
`;
|
||||||
|
await pgClient.query(updateStructureQuery, [
|
||||||
|
JSON.stringify([{
|
||||||
|
id: newDoc.id,
|
||||||
|
url: `/doc/${urlSlug}-${newDoc.urlId}`,
|
||||||
|
title: title,
|
||||||
|
children: []
|
||||||
|
}]),
|
||||||
|
args.collection_id
|
||||||
|
]);
|
||||||
|
|
||||||
await pgClient.query('COMMIT');
|
await pgClient.query('COMMIT');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
310
src/utils/markdown-to-prosemirror.ts
Normal file
310
src/utils/markdown-to-prosemirror.ts
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
/**
|
||||||
|
* Markdown to ProseMirror JSON converter
|
||||||
|
* Converts basic Markdown to Outline's ProseMirror schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ProseMirrorNode {
|
||||||
|
type: string;
|
||||||
|
attrs?: Record<string, unknown>;
|
||||||
|
content?: ProseMirrorNode[];
|
||||||
|
text?: string;
|
||||||
|
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProseMirrorDoc {
|
||||||
|
type: 'doc';
|
||||||
|
content: ProseMirrorNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Markdown text to ProseMirror JSON
|
||||||
|
*/
|
||||||
|
export function markdownToProseMirror(markdown: string): ProseMirrorDoc {
|
||||||
|
const lines = markdown.split('\n');
|
||||||
|
const content: ProseMirrorNode[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
// Empty line - skip
|
||||||
|
if (line.trim() === '') {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal rule
|
||||||
|
if (/^(-{3,}|_{3,}|\*{3,})$/.test(line.trim())) {
|
||||||
|
content.push({ type: 'hr' });
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heading
|
||||||
|
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||||
|
if (headingMatch) {
|
||||||
|
content.push({
|
||||||
|
type: 'heading',
|
||||||
|
attrs: { level: headingMatch[1].length },
|
||||||
|
content: parseInlineContent(headingMatch[2])
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code block
|
||||||
|
if (line.startsWith('```')) {
|
||||||
|
const language = line.slice(3).trim() || null;
|
||||||
|
const codeLines: string[] = [];
|
||||||
|
i++;
|
||||||
|
while (i < lines.length && !lines[i].startsWith('```')) {
|
||||||
|
codeLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
content.push({
|
||||||
|
type: 'code_block',
|
||||||
|
attrs: { language },
|
||||||
|
content: [{ type: 'text', text: codeLines.join('\n') }]
|
||||||
|
});
|
||||||
|
i++; // skip closing ```
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blockquote
|
||||||
|
if (line.startsWith('>')) {
|
||||||
|
const quoteLines: string[] = [];
|
||||||
|
while (i < lines.length && (lines[i].startsWith('>') || lines[i].trim() === '')) {
|
||||||
|
if (lines[i].startsWith('>')) {
|
||||||
|
quoteLines.push(lines[i].replace(/^>\s?/, ''));
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
if (lines[i]?.trim() === '' && !lines[i + 1]?.startsWith('>')) break;
|
||||||
|
}
|
||||||
|
content.push({
|
||||||
|
type: 'blockquote',
|
||||||
|
content: [{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: parseInlineContent(quoteLines.join(' '))
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bullet list
|
||||||
|
if (/^[-*+]\s/.test(line)) {
|
||||||
|
const items: ProseMirrorNode[] = [];
|
||||||
|
while (i < lines.length && /^[-*+]\s/.test(lines[i])) {
|
||||||
|
const itemText = lines[i].replace(/^[-*+]\s+/, '');
|
||||||
|
items.push({
|
||||||
|
type: 'list_item',
|
||||||
|
content: [{ type: 'paragraph', content: parseInlineContent(itemText) }]
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
content.push({ type: 'bullet_list', content: items });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordered list
|
||||||
|
if (/^\d+\.\s/.test(line)) {
|
||||||
|
const items: ProseMirrorNode[] = [];
|
||||||
|
while (i < lines.length && /^\d+\.\s/.test(lines[i])) {
|
||||||
|
const itemText = lines[i].replace(/^\d+\.\s+/, '');
|
||||||
|
items.push({
|
||||||
|
type: 'list_item',
|
||||||
|
content: [{ type: 'paragraph', content: parseInlineContent(itemText) }]
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
content.push({ type: 'ordered_list', content: items });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkbox list
|
||||||
|
if (/^[-*]\s+\[[ x]\]/.test(line)) {
|
||||||
|
const items: ProseMirrorNode[] = [];
|
||||||
|
while (i < lines.length && /^[-*]\s+\[[ x]\]/.test(lines[i])) {
|
||||||
|
const checked = /\[x\]/i.test(lines[i]);
|
||||||
|
const itemText = lines[i].replace(/^[-*]\s+\[[ x]\]\s*/, '');
|
||||||
|
items.push({
|
||||||
|
type: 'checkbox_item',
|
||||||
|
attrs: { checked },
|
||||||
|
content: [{ type: 'paragraph', content: parseInlineContent(itemText) }]
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
content.push({ type: 'checkbox_list', content: items });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: paragraph
|
||||||
|
content.push({
|
||||||
|
type: 'paragraph',
|
||||||
|
content: parseInlineContent(line)
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one empty paragraph if no content
|
||||||
|
if (content.length === 0) {
|
||||||
|
content.push({ type: 'paragraph' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'doc', content };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse inline content (bold, italic, links, code)
|
||||||
|
*/
|
||||||
|
function parseInlineContent(text: string): ProseMirrorNode[] {
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes: ProseMirrorNode[] = [];
|
||||||
|
let remaining = text;
|
||||||
|
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
// Inline code
|
||||||
|
const codeMatch = remaining.match(/^`([^`]+)`/);
|
||||||
|
if (codeMatch) {
|
||||||
|
nodes.push({
|
||||||
|
type: 'text',
|
||||||
|
text: codeMatch[1],
|
||||||
|
marks: [{ type: 'code_inline' }]
|
||||||
|
});
|
||||||
|
remaining = remaining.slice(codeMatch[0].length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/) || remaining.match(/^__([^_]+)__/);
|
||||||
|
if (boldMatch) {
|
||||||
|
nodes.push({
|
||||||
|
type: 'text',
|
||||||
|
text: boldMatch[1],
|
||||||
|
marks: [{ type: 'bold' }]
|
||||||
|
});
|
||||||
|
remaining = remaining.slice(boldMatch[0].length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Italic
|
||||||
|
const italicMatch = remaining.match(/^\*([^*]+)\*/) || remaining.match(/^_([^_]+)_/);
|
||||||
|
if (italicMatch) {
|
||||||
|
nodes.push({
|
||||||
|
type: 'text',
|
||||||
|
text: italicMatch[1],
|
||||||
|
marks: [{ type: 'italic' }]
|
||||||
|
});
|
||||||
|
remaining = remaining.slice(italicMatch[0].length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link
|
||||||
|
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
||||||
|
if (linkMatch) {
|
||||||
|
nodes.push({
|
||||||
|
type: 'text',
|
||||||
|
text: linkMatch[1],
|
||||||
|
marks: [{ type: 'link', attrs: { href: linkMatch[2] } }]
|
||||||
|
});
|
||||||
|
remaining = remaining.slice(linkMatch[0].length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain text - consume until next special char or end
|
||||||
|
const plainMatch = remaining.match(/^[^`*_\[]+/);
|
||||||
|
if (plainMatch) {
|
||||||
|
nodes.push({ type: 'text', text: plainMatch[0] });
|
||||||
|
remaining = remaining.slice(plainMatch[0].length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: consume single character
|
||||||
|
nodes.push({ type: 'text', text: remaining[0] });
|
||||||
|
remaining = remaining.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert ProseMirror JSON back to Markdown
|
||||||
|
*/
|
||||||
|
export function proseMirrorToMarkdown(doc: ProseMirrorDoc): string {
|
||||||
|
return doc.content.map(node => nodeToMarkdown(node)).join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeToMarkdown(node: ProseMirrorNode, indent = ''): string {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'heading':
|
||||||
|
const level = (node.attrs?.level as number) || 1;
|
||||||
|
return '#'.repeat(level) + ' ' + contentToMarkdown(node.content);
|
||||||
|
|
||||||
|
case 'paragraph':
|
||||||
|
return contentToMarkdown(node.content);
|
||||||
|
|
||||||
|
case 'bullet_list':
|
||||||
|
return (node.content || [])
|
||||||
|
.map(item => '- ' + nodeToMarkdown(item.content?.[0] || { type: 'paragraph' }))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
case 'ordered_list':
|
||||||
|
return (node.content || [])
|
||||||
|
.map((item, i) => `${i + 1}. ` + nodeToMarkdown(item.content?.[0] || { type: 'paragraph' }))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
case 'checkbox_list':
|
||||||
|
return (node.content || [])
|
||||||
|
.map(item => {
|
||||||
|
const checked = item.attrs?.checked ? 'x' : ' ';
|
||||||
|
return `- [${checked}] ` + nodeToMarkdown(item.content?.[0] || { type: 'paragraph' });
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
case 'blockquote':
|
||||||
|
return (node.content || [])
|
||||||
|
.map(child => '> ' + nodeToMarkdown(child))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
case 'code_block':
|
||||||
|
const lang = node.attrs?.language || '';
|
||||||
|
return '```' + lang + '\n' + contentToMarkdown(node.content) + '\n```';
|
||||||
|
|
||||||
|
case 'hr':
|
||||||
|
return '---';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return contentToMarkdown(node.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentToMarkdown(content?: ProseMirrorNode[]): string {
|
||||||
|
if (!content) return '';
|
||||||
|
return content.map(node => {
|
||||||
|
if (node.type === 'text') {
|
||||||
|
let text = node.text || '';
|
||||||
|
if (node.marks) {
|
||||||
|
for (const mark of node.marks) {
|
||||||
|
switch (mark.type) {
|
||||||
|
case 'bold':
|
||||||
|
text = `**${text}**`;
|
||||||
|
break;
|
||||||
|
case 'italic':
|
||||||
|
text = `*${text}*`;
|
||||||
|
break;
|
||||||
|
case 'code_inline':
|
||||||
|
text = `\`${text}\``;
|
||||||
|
break;
|
||||||
|
case 'link':
|
||||||
|
text = `[${text}](${mark.attrs?.href})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return nodeToMarkdown(node);
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user