checkpoint before gfm ast parser design

This commit is contained in:
lix-2026
2026-05-08 00:41:03 +08:00
parent e8ba12e461
commit c620b9e40c
41 changed files with 12270 additions and 263 deletions
@@ -2,6 +2,9 @@ use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::command_support::execute_runtime_command_via_convex;
use crate::routes::local_folder_source::{
save_local_markdown_page, update_local_markdown_title, update_local_page_options,
};
use crate::routes::query_support::{
execute_runtime_query_via_convex, fetch_documents_meta_via_convex,
resolve_effective_workspace_id,
@@ -37,6 +40,8 @@ pub struct DocumentMetaQuery {
pub struct DocumentSaveRequest {
pub document_id: String,
pub workspace_id: Option<String>,
pub source_kind: Option<String>,
pub root_uri: Option<String>,
pub revision: Option<u64>,
pub conflict_detection_key: Option<String>,
pub editor_document: Option<Value>,
@@ -51,6 +56,8 @@ pub struct DocumentSaveRequest {
pub struct DocumentTitleRequest {
pub document_id: String,
pub workspace_id: Option<String>,
pub source_kind: Option<String>,
pub root_uri: Option<String>,
pub title: String,
pub command_name: Option<String>,
}
@@ -60,6 +67,8 @@ pub struct DocumentTitleRequest {
pub struct DocumentOptionsRequest {
pub document_id: String,
pub workspace_id: Option<String>,
pub source_kind: Option<String>,
pub root_uri: Option<String>,
#[serde(default)]
pub options: Value,
pub command_name: Option<String>,
@@ -501,6 +510,19 @@ pub async fn save(
.with_context(&context),
);
}
if body.source_kind.as_deref().map(str::trim) == Some("local_folder") {
let root_uri = body
.root_uri
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
.with_context(&context)
})?;
let result = save_local_markdown_page(root_uri, document_id, &body.content)?;
return Ok(ok_response(&context, result));
}
let effective_workspace_id =
resolve_effective_workspace_id(&context, body.workspace_id.as_deref(), false)?;
if should_proxy_via_next(&context) {
@@ -520,6 +542,10 @@ pub async fn save(
source: RuntimeSourceWire {
channel: context.source.channel.clone(),
client: context.source.client.clone(),
source_kind: None,
root_uri: None,
workspace_id: None,
capabilities: Vec::new(),
},
target: Some(RuntimeTargetWire {
workspace_id: effective_workspace_id.clone(),
@@ -582,6 +608,10 @@ pub async fn purge(
source: RuntimeSourceWire {
channel: context.source.channel.clone(),
client: context.source.client.clone(),
source_kind: None,
root_uri: None,
workspace_id: None,
capabilities: Vec::new(),
},
target: Some(RuntimeTargetWire {
workspace_id: None,
@@ -620,6 +650,19 @@ pub async fn title(
WebError::bad_request_code("title_required", "缺少有效页面标题").with_context(&context),
);
}
if body.source_kind.as_deref().map(str::trim) == Some("local_folder") {
let root_uri = body
.root_uri
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
.with_context(&context)
})?;
let result = update_local_markdown_title(root_uri, document_id, title)?;
return Ok(ok_response(&context, result));
}
let effective_workspace_id =
resolve_effective_workspace_id(&context, body.workspace_id.as_deref(), false)?;
let command = RuntimeCommandEnvelopeWire {
@@ -634,6 +677,10 @@ pub async fn title(
source: RuntimeSourceWire {
channel: context.source.channel.clone(),
client: context.source.client.clone(),
source_kind: None,
root_uri: None,
workspace_id: None,
capabilities: Vec::new(),
},
target: Some(RuntimeTargetWire {
workspace_id: effective_workspace_id.clone(),
@@ -691,6 +738,19 @@ pub async fn options(
.with_context(&context),
);
}
if body.source_kind.as_deref().map(str::trim) == Some("local_folder") {
let root_uri = body
.root_uri
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
.with_context(&context)
})?;
let result = update_local_page_options(root_uri, document_id, &body.options)?;
return Ok(ok_response(&context, result));
}
let effective_workspace_id =
resolve_effective_workspace_id(&context, body.workspace_id.as_deref(), false)?;
let command = RuntimeCommandEnvelopeWire {
@@ -705,6 +765,10 @@ pub async fn options(
source: RuntimeSourceWire {
channel: context.source.channel.clone(),
client: context.source.client.clone(),
source_kind: None,
root_uri: None,
workspace_id: None,
capabilities: Vec::new(),
},
target: Some(RuntimeTargetWire {
workspace_id: effective_workspace_id.clone(),
@@ -1028,4 +1092,114 @@ mod tests {
"page.layout.updateOptions"
);
}
#[tokio::test]
async fn local_folder_documents_save_title_and_options_write_to_disk() {
let root = std::env::temp_dir().join(format!(
"mnote-local-documents-write-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("create local root");
std::fs::write(
root.join("README.md"),
"---\nmnote_id: local-stable\ntitle: Old Title\n---\n# Old\n",
)
.expect("write md");
let root_uri = format!("file://{}", root.display());
let document_id = "local-mdid:local-stable";
let title_response = app()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/documents/title")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"documentId": document_id,
"sourceKind": "local_folder",
"rootUri": root_uri,
"title": "New Local Title"
})
.to_string(),
))
.expect("request"),
)
.await
.expect("title response");
assert_eq!(title_response.status(), StatusCode::OK);
let save_response = app()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/documents/save")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"documentId": document_id,
"sourceKind": "local_folder",
"rootUri": root_uri,
"content": [
{
"id": "heading_1",
"type": "heading",
"props": { "level": 2 },
"content": [{ "type": "text", "text": "Saved Heading" }]
},
{
"id": "paragraph_1",
"type": "paragraph",
"content": [{ "type": "text", "text": "Saved body" }]
}
],
"blockCount": 2
})
.to_string(),
))
.expect("request"),
)
.await
.expect("save response");
assert_eq!(save_response.status(), StatusCode::OK);
let options_response = app()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/documents/options")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"documentId": document_id,
"sourceKind": "local_folder",
"rootUri": root_uri,
"options": {
"wideLayout": true,
"showToc": false
}
})
.to_string(),
))
.expect("request"),
)
.await
.expect("options response");
assert_eq!(options_response.status(), StatusCode::OK);
let markdown = std::fs::read_to_string(root.join("README.md")).expect("read md");
assert!(markdown.contains("mnote_id: local-stable"));
assert!(markdown.contains("title: New Local Title"));
assert!(markdown.contains("## Saved Heading"));
assert!(markdown.contains("Saved body"));
let options = std::fs::read_to_string(root.join(".mnote").join("page-options.json"))
.expect("read page options");
let options_json: Value = serde_json::from_str(&options).expect("options json");
assert_eq!(options_json["pages"][document_id]["wideLayout"], true);
assert_eq!(options_json["pages"][document_id]["showToc"], false);
let _ = std::fs::remove_dir_all(&root);
}
}