chore: align local-first control plane and editor fixes
- wire SQLite control-plane access/session paths into Rust web local-folder routes - preserve local Markdown attachment semantics across upload, reload, and secondary-pane resource tabs - refresh design governance docs, Reasonix task templates, and bug records - retire root .mcp.json local MCP config
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use comrak::nodes::{AstNode, ListType, NodeValue, TableAlignment};
|
||||
use comrak::{parse_document, Arena, Options};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedLocalMarkdownPage {
|
||||
@@ -134,7 +135,14 @@ fn find_first_h1(body: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn markdown_to_blocks(markdown: &str) -> Value {
|
||||
markdown_ast_document_to_blocks(&parse_markdown_ast_document(markdown))
|
||||
markdown_to_blocks_with_attachment_paths(markdown, &BTreeSet::new())
|
||||
}
|
||||
|
||||
pub fn markdown_to_blocks_with_attachment_paths(
|
||||
markdown: &str,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Value {
|
||||
markdown_ast_document_to_blocks(&parse_markdown_ast_document(markdown, attachment_paths))
|
||||
}
|
||||
|
||||
fn markdown_options() -> Options<'static> {
|
||||
@@ -149,6 +157,13 @@ fn markdown_options() -> Options<'static> {
|
||||
}
|
||||
|
||||
pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)> {
|
||||
parse_markdown_attachment_link_with_paths(trimmed, &BTreeSet::new())
|
||||
}
|
||||
|
||||
fn parse_markdown_attachment_link_with_paths(
|
||||
trimmed: &str,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String)> {
|
||||
let value = trimmed
|
||||
.strip_prefix('!')
|
||||
.unwrap_or(trimmed)
|
||||
@@ -170,7 +185,9 @@ pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)>
|
||||
}
|
||||
let target_path = std::path::Path::new(target);
|
||||
let extension = target_path.extension().and_then(|value| value.to_str())?;
|
||||
if extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown") {
|
||||
if (extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown"))
|
||||
&& !attachment_paths.contains(target)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let fallback_name = target_path
|
||||
@@ -186,20 +203,27 @@ pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)>
|
||||
Some((name.to_string(), target.to_string()))
|
||||
}
|
||||
|
||||
fn parse_markdown_ast_document(markdown: &str) -> MarkdownAstDocument {
|
||||
fn parse_markdown_ast_document(
|
||||
markdown: &str,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> MarkdownAstDocument {
|
||||
let arena = Arena::new();
|
||||
let options = markdown_options();
|
||||
let root = parse_document(&arena, markdown, &options);
|
||||
let mut blocks = Vec::new();
|
||||
for node in root.children() {
|
||||
append_ast_block(node, &mut blocks);
|
||||
append_ast_block(node, &mut blocks, attachment_paths);
|
||||
}
|
||||
MarkdownAstDocument { blocks }
|
||||
}
|
||||
|
||||
fn append_ast_block<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>) {
|
||||
fn append_ast_block<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
blocks: &mut Vec<MarkdownBlock>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) {
|
||||
match node.data.borrow().value.clone() {
|
||||
NodeValue::Paragraph => append_ast_paragraph(node, blocks),
|
||||
NodeValue::Paragraph => append_ast_paragraph(node, blocks, attachment_paths),
|
||||
NodeValue::Heading(heading) => blocks.push(MarkdownBlock::Heading {
|
||||
level: heading.level,
|
||||
content: collect_inline_children(node),
|
||||
@@ -212,7 +236,12 @@ fn append_ast_block<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>)
|
||||
}),
|
||||
NodeValue::List(list) => {
|
||||
for item in node.children() {
|
||||
append_ast_list_item(item, list.list_type == ListType::Ordered, blocks);
|
||||
append_ast_list_item(
|
||||
item,
|
||||
list.list_type == ListType::Ordered,
|
||||
blocks,
|
||||
attachment_paths,
|
||||
);
|
||||
}
|
||||
}
|
||||
NodeValue::Table(table) => blocks.push(ast_table_to_ir(node, table.alignments)),
|
||||
@@ -233,7 +262,11 @@ fn append_ast_block<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>)
|
||||
}
|
||||
}
|
||||
|
||||
fn append_ast_paragraph<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>) {
|
||||
fn append_ast_paragraph<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
blocks: &mut Vec<MarkdownBlock>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) {
|
||||
if let Some((alt, source_path)) = paragraph_image(node) {
|
||||
blocks.push(MarkdownBlock::Image { alt, source_path });
|
||||
return;
|
||||
@@ -242,11 +275,13 @@ fn append_ast_paragraph<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBloc
|
||||
blocks.push(MarkdownBlock::Mindmap { name, source_path });
|
||||
return;
|
||||
}
|
||||
if let Some((name, source_path)) = paragraph_attachment_media(node) {
|
||||
if let Some((name, source_path)) = paragraph_attachment_media(node, attachment_paths) {
|
||||
blocks.push(MarkdownBlock::Media { name, source_path });
|
||||
return;
|
||||
}
|
||||
if let Some((name, source_path, remaining)) = paragraph_leading_attachment_media(node) {
|
||||
if let Some((name, source_path, remaining)) =
|
||||
paragraph_leading_attachment_media(node, attachment_paths)
|
||||
{
|
||||
blocks.push(MarkdownBlock::Media { name, source_path });
|
||||
let content = merge_adjacent_inline_nodes(remaining);
|
||||
if !content.is_empty() {
|
||||
@@ -257,17 +292,22 @@ fn append_ast_paragraph<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBloc
|
||||
blocks.push(MarkdownBlock::Paragraph(collect_inline_children(node)));
|
||||
}
|
||||
|
||||
fn append_ast_list_item<'a>(node: &'a AstNode<'a>, ordered: bool, blocks: &mut Vec<MarkdownBlock>) {
|
||||
fn append_ast_list_item<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
ordered: bool,
|
||||
blocks: &mut Vec<MarkdownBlock>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) {
|
||||
let (is_task, checked) = match node.data.borrow().value.clone() {
|
||||
NodeValue::TaskItem(task_item) => (true, task_item.symbol.is_some()),
|
||||
NodeValue::Item(_) => (false, false),
|
||||
_ => return append_ast_block(node, blocks),
|
||||
_ => return append_ast_block(node, blocks, attachment_paths),
|
||||
};
|
||||
let mut content = Vec::new();
|
||||
for child in node.children() {
|
||||
match child.data.borrow().value.clone() {
|
||||
NodeValue::Paragraph => content.extend(collect_inline_children(child)),
|
||||
_ => append_ast_block(child, blocks),
|
||||
_ => append_ast_block(child, blocks, attachment_paths),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,13 +444,16 @@ fn merge_adjacent_inline_nodes(nodes: Vec<MarkdownInline>) -> Vec<MarkdownInline
|
||||
merged
|
||||
}
|
||||
|
||||
fn paragraph_attachment_media<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
fn paragraph_attachment_media<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String)> {
|
||||
let mut children = node.children();
|
||||
let first = children.next()?;
|
||||
if children.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
link_attachment_media(first)
|
||||
link_attachment_media(first, attachment_paths)
|
||||
}
|
||||
|
||||
fn paragraph_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
@@ -437,10 +480,11 @@ fn paragraph_image<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
|
||||
fn paragraph_leading_attachment_media<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String, Vec<MarkdownInline>)> {
|
||||
let mut children = node.children();
|
||||
let first = children.next()?;
|
||||
let (name, source_path) = link_attachment_media(first)?;
|
||||
let (name, source_path) = link_attachment_media(first, attachment_paths)?;
|
||||
let second = children.next()?;
|
||||
if !matches!(
|
||||
second.data.borrow().value,
|
||||
@@ -455,11 +499,17 @@ fn paragraph_leading_attachment_media<'a>(
|
||||
Some((name, source_path, remaining))
|
||||
}
|
||||
|
||||
fn link_attachment_media<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
fn link_attachment_media<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String)> {
|
||||
let NodeValue::Link(link) = &node.data.borrow().value else {
|
||||
return None;
|
||||
};
|
||||
parse_markdown_attachment_link(&format!("[{}]({})", collect_plain_text(node), link.url))
|
||||
parse_markdown_attachment_link_with_paths(
|
||||
&format!("[{}]({})", collect_plain_text(node), link.url),
|
||||
attachment_paths,
|
||||
)
|
||||
}
|
||||
|
||||
fn link_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
|
||||
Reference in New Issue
Block a user