Files
Avtex/lib/core/markdown_mindmap_parser.dart
T

195 lines
5.6 KiB
Dart

import 'package:uuid/uuid.dart';
import '../models/mind_map_model.dart';
/// Парсер Markdown-файлов в MindMapNode структуру
///
/// Ожидаемая структура markdown:
/// ```markdown
/// ---
/// title: Название карты
/// tags: [crm, project]
/// ---
///
/// # Корневой узел
/// Описание корневого узла
///
/// ## Дочерний узел 1
/// Описание
///
/// ### Внук 1
///
/// ## Дочерний узел 2
/// ```
class MarkdownMindmapParser {
static const Uuid _uuid = Uuid();
/// Парсит markdown строку в дерево MindMapNode
/// Возвращает корневой узел + все дочерние
static List<MindMapNode> parse(String markdown, String projectId) {
final lines = markdown.split('\n');
final nodes = <MindMapNode>[];
final now = DateTime.now();
String? currentParentId;
final Map<int, String> levelToParentId = {};
for (final line in lines) {
final trimmed = line.trim();
if (trimmed.isEmpty) continue;
// Пропускаем frontmatter
if (trimmed == '---') continue;
if (trimmed.startsWith('title:') ||
trimmed.startsWith('tags:') ||
trimmed.startsWith('date:')) {
continue;
}
// Определяем уровень заголовка
final headerMatch = _headerRegex.firstMatch(trimmed);
if (headerMatch != null) {
final hashes = headerMatch.group(1)!;
final level = hashes.length;
final title = trimmed.substring(hashes.length).trim();
final id = _uuid.v4();
// Определяем parentId на основе уровня
if (level == 1) {
// Корневой узел
currentParentId = null;
levelToParentId.clear();
} else {
// Ищем ближайший родитель с уровнем меньше текущего
String? parentId;
for (int l = level - 1; l >= 1; l--) {
if (levelToParentId.containsKey(l)) {
parentId = levelToParentId[l];
break;
}
}
currentParentId = parentId;
}
levelToParentId[level] = id;
nodes.add(MindMapNode(
id: id,
projectId: projectId,
parentId: currentParentId,
level: level,
title: title,
color: _levelColor(level),
createdAt: now,
updatedAt: now,
));
}
}
// Если нет ни одного заголовка — создаём один корневой узел с текстом
if (nodes.isEmpty && markdown.trim().isNotEmpty) {
final firstLine = markdown.trim().split('\n').first.trim();
nodes.add(MindMapNode(
id: _uuid.v4(),
projectId: projectId,
parentId: null,
level: 0,
title: firstLine.length > 50 ? '${firstLine.substring(0, 50)}...' : firstLine,
color: _levelColor(0),
createdAt: now,
updatedAt: now,
));
}
return nodes;
}
/// Конвертирует дерево MindMapNode обратно в Markdown
static String toMarkdown(List<MindMapNode> nodes, {String? title}) {
if (nodes.isEmpty) return '';
final buffer = StringBuffer();
// Frontmatter
buffer.writeln('---');
buffer.writeln('title: ${title ?? 'Mindmap'}');
buffer.writeln('date: ${DateTime.now().toIso8601String()}');
buffer.writeln('---');
buffer.writeln();
// Сортируем по уровню, затем по порядку
final sorted = List<MindMapNode>.from(nodes)
..sort((a, b) {
final levelCompare = a.level.compareTo(b.level);
if (levelCompare != 0) return levelCompare;
return a.createdAt.compareTo(b.createdAt);
});
for (final node in sorted) {
final hashes = '#' * node.level.clamp(1, 6);
buffer.writeln('$hashes ${node.title}');
if (node.description != null && node.description!.isNotEmpty) {
buffer.writeln(node.description);
}
buffer.writeln();
}
return buffer.toString().trim();
}
/// Извлекает plain text описание из markdown (без заголовков)
static String? extractDescription(String markdown) {
final lines = markdown.split('\n');
final buffer = StringBuffer();
bool inFrontmatter = false;
for (final line in lines) {
final trimmed = line.trim();
if (trimmed == '---') {
inFrontmatter = !inFrontmatter;
continue;
}
if (inFrontmatter) continue;
if (trimmed.startsWith('#')) continue;
if (trimmed.isNotEmpty) {
buffer.writeln(trimmed);
}
}
final result = buffer.toString().trim();
return result.isEmpty ? null : result;
}
/// Извлекает wiki-ссылки [[Link]] из текста
static List<String> extractWikiLinks(String text) {
final matches = _wikiLinkRegex.allMatches(text);
return matches.map((m) => m.group(1)!).toList();
}
// ==================== PRIVATE ====================
static final RegExp _headerRegex = RegExp(r'^(#{1,6})\s+(.+)$');
static final RegExp _wikiLinkRegex = RegExp(r'\[\[([^\]]+)\]\]');
static String _levelColor(int level) {
switch (level) {
case 0:
return '#E53935'; // red
case 1:
return '#1E88E5'; // blue
case 2:
return '#43A047'; // green
case 3:
return '#FB8C00'; // orange
case 4:
return '#8E24AA'; // purple
case 5:
return '#00ACC1'; // cyan
default:
return '#78909C'; // grey
}
}
}