fix local markdown attachment regressions
This commit is contained in:
@@ -6,13 +6,13 @@ use crate::page_aggregate::{
|
||||
PagePermissions, PageStats, PageTree,
|
||||
};
|
||||
use crate::routes::local_markdown_parser::{
|
||||
file_stem_title, parse_markdown_page, split_frontmatter,
|
||||
file_stem_title, parse_markdown_attachment_refs, parse_markdown_page, split_frontmatter,
|
||||
};
|
||||
use crate::routes::local_search_index;
|
||||
use crate::routes::snapshot_support::ProjectionSnapshot;
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Multipart, Path as AxumPath, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
|
||||
use axum::Json;
|
||||
use bridge_runtime::project_legacy_content_to_block_document;
|
||||
use control_plane::AppendAuditInput;
|
||||
use control_plane::{
|
||||
@@ -24,7 +24,7 @@ use core_protocol::{
|
||||
};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value, json};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
@@ -55,8 +55,8 @@ static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
|
||||
Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
fn local_page_tree_snapshot_cache()
|
||||
-> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
|
||||
fn local_page_tree_snapshot_cache(
|
||||
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||
}
|
||||
|
||||
@@ -2966,6 +2966,7 @@ pub fn resolve_local_markdown_page_aggregate(
|
||||
"本地 rootUri 必须指向目录",
|
||||
));
|
||||
}
|
||||
let root_source_uri = file_uri_for_path(&canonical_root);
|
||||
let metadata = load_local_folder_metadata(&canonical_root)?;
|
||||
let markdown_file = find_markdown_by_page_id(&canonical_root, &metadata, document_id)?
|
||||
.ok_or_else(|| {
|
||||
@@ -2995,6 +2996,20 @@ pub fn resolve_local_markdown_page_aggregate(
|
||||
&parsed.body,
|
||||
&attachment_paths,
|
||||
);
|
||||
let attachment_refs = parse_markdown_attachment_refs(
|
||||
&parsed.body,
|
||||
&markdown_file.path.display().to_string(),
|
||||
&root_source_uri,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|mut attachment_ref| {
|
||||
if let Some(path) = attachment_ref.resolved_absolute_path.as_deref() {
|
||||
let resolved_path = Path::new(path);
|
||||
attachment_ref.authorized = Some(resolved_path.starts_with(&canonical_root));
|
||||
}
|
||||
attachment_ref
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let block_count = content.as_array().map(|blocks| blocks.len()).unwrap_or(0) as u64;
|
||||
let page_subtree = markdown_page_subtree(document_id, &title, &content);
|
||||
let workspace_id = local_workspace_id(&canonical_root);
|
||||
@@ -3063,6 +3078,7 @@ pub fn resolve_local_markdown_page_aggregate(
|
||||
block_document,
|
||||
block_projection_version: 1,
|
||||
projection_source: "local_markdown.content".into(),
|
||||
attachment_refs: serde_json::to_value(attachment_refs).unwrap_or_else(|_| json!([])),
|
||||
},
|
||||
tree: PageTree { page_subtree },
|
||||
stats: PageStats {
|
||||
@@ -3927,14 +3943,7 @@ pub(crate) fn write_local_markdown_asset(
|
||||
));
|
||||
}
|
||||
|
||||
let page_resource_dir =
|
||||
markdown_page_resource_directory(&markdown_file.path).ok_or_else(|| {
|
||||
WebError::bad_request_code(
|
||||
"local_asset_upload_bad_markdown_path",
|
||||
"无法解析本地页面资源目录",
|
||||
)
|
||||
})?;
|
||||
let asset_dir = page_resource_dir;
|
||||
let asset_dir = markdown_dir.to_path_buf();
|
||||
if !asset_dir.starts_with(&canonical_root) {
|
||||
return Err(WebError::bad_request_code(
|
||||
"local_folder_root_escape",
|
||||
@@ -3965,6 +3974,24 @@ pub(crate) fn write_local_markdown_asset(
|
||||
|
||||
let root_relative_path = normalize_relative_path(&canonical_root, &target)?;
|
||||
let markdown_relative_path = normalize_markdown_relative_asset_path(markdown_dir, &target)?;
|
||||
let markdown_href = markdown_href_for_relative_path(&markdown_relative_path);
|
||||
let mut attachment_ref = parse_markdown_attachment_refs(
|
||||
&format!(
|
||||
"[{}]({})",
|
||||
target
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or(&sanitized_name),
|
||||
markdown_href
|
||||
),
|
||||
&markdown_file.path.display().to_string(),
|
||||
&file_uri_for_path(&canonical_root),
|
||||
)
|
||||
.into_iter()
|
||||
.next();
|
||||
if let Some(ref mut value) = attachment_ref {
|
||||
value.authorized = Some(true);
|
||||
}
|
||||
let asset_type = local_upload_asset_type(kind, &file.content_type);
|
||||
let mut uploaded_assets = metadata.uploaded_assets;
|
||||
uploaded_assets.insert(
|
||||
@@ -3997,6 +4024,8 @@ pub(crate) fn write_local_markdown_asset(
|
||||
"rootUri": file_uri_for_path(&canonical_root),
|
||||
"rootRelativePath": root_relative_path,
|
||||
"markdownRelativePath": markdown_relative_path,
|
||||
"markdownHref": markdown_href,
|
||||
"attachmentRef": attachment_ref,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -7063,7 +7092,11 @@ fn parent_key_for_relative_path(relative_path: &str) -> String {
|
||||
.map(|component| component.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
if value.is_empty() { None } else { Some(value) }
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
})
|
||||
.map(|value| normalize_file_order_parent_key(&value))
|
||||
.unwrap_or_else(|| ".".to_string())
|
||||
@@ -8130,6 +8163,15 @@ fn normalize_markdown_relative_asset_path(
|
||||
.join("/"))
|
||||
}
|
||||
|
||||
fn markdown_href_for_relative_path(relative_path: &str) -> String {
|
||||
let trimmed = relative_path.trim().replace('\\', "/");
|
||||
if trimmed.starts_with("./") || trimmed.starts_with("../") {
|
||||
trimmed
|
||||
} else {
|
||||
format!("./{trimmed}")
|
||||
}
|
||||
}
|
||||
|
||||
fn local_upload_asset_type(kind: &str, mime_type: &str) -> &'static str {
|
||||
let normalized_kind = kind.trim().to_ascii_lowercase();
|
||||
if normalized_kind == "image" || mime_type.trim().to_ascii_lowercase().starts_with("image/") {
|
||||
@@ -8384,6 +8426,7 @@ fn editor_blocks_to_markdown_with_rewrite(
|
||||
lines.push(text);
|
||||
} else {
|
||||
let src = rewrite_local_open_url_to_markdown_relative(src, local_file_context)
|
||||
.map(|value| markdown_href_for_relative_path(&value))
|
||||
.unwrap_or_else(|| src.to_string());
|
||||
lines.push(format!(
|
||||
"",
|
||||
@@ -8412,9 +8455,7 @@ fn editor_blocks_to_markdown_with_rewrite(
|
||||
if url.is_empty() {
|
||||
lines.push(text);
|
||||
} else {
|
||||
let url = rewrite_local_open_url_to_markdown_relative(url, local_file_context)
|
||||
.unwrap_or_else(|| url.to_string());
|
||||
let label = if name.is_empty() { url.as_str() } else { name };
|
||||
let label = if name.is_empty() { url } else { name };
|
||||
lines.push(format!("[{}]({})", label, markdown_link_target(&url)));
|
||||
}
|
||||
}
|
||||
@@ -9041,7 +9082,7 @@ fn inline_styles_from_object(object: &Map<String, Value>) -> Value {
|
||||
fn markdown_text_with_styles(
|
||||
text: &str,
|
||||
styles: &Value,
|
||||
local_file_context: Option<(&Path, &Path)>,
|
||||
_local_file_context: Option<(&Path, &Path)>,
|
||||
) -> String {
|
||||
let mut value = escape_markdown_inline_text(text);
|
||||
let link = styles
|
||||
@@ -9078,8 +9119,6 @@ fn markdown_text_with_styles(
|
||||
value = format!("~~{value}~~");
|
||||
}
|
||||
if let Some(href) = link {
|
||||
let href =
|
||||
rewrite_local_open_url_to_markdown_relative(&href, local_file_context).unwrap_or(href);
|
||||
value = format!("[{}]({href})", value.replace(']', r"\]"));
|
||||
}
|
||||
value
|
||||
@@ -9224,10 +9263,6 @@ fn markdown_page_subtree(document_id: &str, title: &str, content: &Value) -> Val
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
LocalAccessGrantRequest, LocalAccessValidateRootRequest, LocalFileOpenQuery,
|
||||
LocalResourceReadQuery, LocalResourceWriteRequest, LocalShareGrantRequest,
|
||||
LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery, SharedCacheRecordRequest,
|
||||
SyncConflictReportRequest, SyncPendingChangeRequest,
|
||||
add_sqlite_local_access_grant_for_context,
|
||||
create_default_local_workspace_for_actor_at_base, create_local_access_grant,
|
||||
create_share_grant, create_share_link, create_user_access_grant, create_user_share_grant,
|
||||
@@ -9246,15 +9281,18 @@ mod tests {
|
||||
resolve_local_markdown_page_aggregate, save_local_markdown_page,
|
||||
update_local_markdown_title, validate_local_access_root, write_local_markdown_asset,
|
||||
write_local_markdown_page_body, write_local_mindmap_data, write_local_resource,
|
||||
write_sync_conflict_report,
|
||||
write_sync_conflict_report, LocalAccessGrantRequest, LocalAccessValidateRootRequest,
|
||||
LocalFileOpenQuery, LocalResourceReadQuery, LocalResourceWriteRequest,
|
||||
LocalShareGrantRequest, LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery,
|
||||
SharedCacheRecordRequest, SyncConflictReportRequest, SyncPendingChangeRequest,
|
||||
};
|
||||
use crate::app::{AppConfig, AppState};
|
||||
use crate::context::RequestContext;
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Path as AxumPath, Query, State};
|
||||
use axum::http::{HeaderMap, Method, StatusCode};
|
||||
use axum::Json;
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
@@ -9526,25 +9564,28 @@ mod tests {
|
||||
"tableCell"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][0]["content"][0]["content"][0]["content"][0]["text"],
|
||||
table["props"]["tiptapTable"]["content"][0]["content"][0]["content"][0]["content"][0]
|
||||
["text"],
|
||||
"左"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]["text"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
|
||||
["text"],
|
||||
"A"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]["marks"]
|
||||
[0]["type"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
|
||||
["marks"][0]["type"],
|
||||
"code"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]["text"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
|
||||
["text"],
|
||||
"B"
|
||||
);
|
||||
assert_eq!(
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]["marks"]
|
||||
[0]["type"],
|
||||
table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
|
||||
["marks"][0]["type"],
|
||||
"bold"
|
||||
);
|
||||
}
|
||||
@@ -9647,12 +9688,10 @@ mod tests {
|
||||
let body = serde_json::to_value(&aggregate.body).expect("body json");
|
||||
|
||||
assert_eq!(body["fileVersion"], body["conflictDetectionKey"]);
|
||||
assert!(
|
||||
body["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:")
|
||||
);
|
||||
assert!(body["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
@@ -9674,12 +9713,10 @@ mod tests {
|
||||
.expect("save");
|
||||
|
||||
assert_eq!(result["fileVersion"], result["conflict_detection_key"]);
|
||||
assert!(
|
||||
result["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:")
|
||||
);
|
||||
assert!(result["fileVersion"]
|
||||
.as_str()
|
||||
.expect("file version")
|
||||
.starts_with("local-md:local-md:README.md:"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
@@ -9842,7 +9879,7 @@ fn main() {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_save_rewrites_uploaded_markdown_inline_link_as_media_path() {
|
||||
fn local_markdown_save_does_not_migrate_runtime_open_url_inline_link() {
|
||||
let root = temp_root("mnote-local-uploaded-md-inline-link");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("Page")).expect("create page dir");
|
||||
@@ -9888,28 +9925,18 @@ fn main() {}
|
||||
)
|
||||
.expect("save inline attachment link");
|
||||
|
||||
assert_eq!(asset["sourcePath"], "notes.md");
|
||||
let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
|
||||
assert!(saved.contains("[notes.md](notes.md)"));
|
||||
assert!(!saved.contains("/api/local-folder/files/open"));
|
||||
|
||||
let aggregate =
|
||||
resolve_local_markdown_page_aggregate(&root_uri, document_id).expect("aggregate");
|
||||
let media = aggregate
|
||||
.body
|
||||
.content
|
||||
.as_array()
|
||||
.expect("blocks")
|
||||
.iter()
|
||||
.find(|block| block["type"].as_str() == Some("media"))
|
||||
.expect("uploaded md inline link should reload as media block");
|
||||
assert_eq!(media["props"]["sourcePath"], "notes.md");
|
||||
assert_eq!(asset["sourcePath"], "notes.md");
|
||||
assert!(
|
||||
saved.contains("/api/local-folder/files/open"),
|
||||
"开发态不再把旧 runtime open URL 自动迁移为标准 href"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_save_rewrites_uploaded_markdown_relative_open_url_as_media_path() {
|
||||
fn local_markdown_save_does_not_migrate_relative_runtime_open_url() {
|
||||
let root = temp_root("mnote-local-uploaded-md-relative-open-link");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("Page")).expect("create page dir");
|
||||
@@ -9956,7 +9983,10 @@ fn main() {}
|
||||
.expect("save relative open url");
|
||||
|
||||
let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
|
||||
assert!(saved.contains("[notes.md](notes.md)"));
|
||||
assert!(
|
||||
saved.contains("/api/local-folder/files/open"),
|
||||
"开发态不再把旧 runtime open URL 自动迁移为标准 href"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
@@ -10800,12 +10830,10 @@ fn main() {}
|
||||
assert_eq!(created["grant"]["ownerUserId"], "user_owner");
|
||||
assert_eq!(created["grant"]["targetUserId"], "user_target");
|
||||
assert_eq!(created["grant"]["permission"], "write");
|
||||
assert!(
|
||||
created["grant"]["shareId"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.starts_with("folder_")
|
||||
);
|
||||
assert!(created["grant"]["shareId"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.starts_with("folder_"));
|
||||
|
||||
let outside_error = create_user_share_grant(
|
||||
Extension(request_context("user_owner", "user")),
|
||||
@@ -11075,13 +11103,11 @@ fn main() {}
|
||||
assert_eq!(stored.len(), 1);
|
||||
assert_eq!(stored[0].id, link_id);
|
||||
assert_ne!(stored[0].token_hash, "visible-token");
|
||||
assert!(
|
||||
state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve share link")
|
||||
.is_some()
|
||||
);
|
||||
assert!(state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve share link")
|
||||
.is_some());
|
||||
|
||||
let (_, Json(listed)) = get_share_links(
|
||||
State(state.clone()),
|
||||
@@ -11114,13 +11140,11 @@ fn main() {}
|
||||
.expect("share revoked broadcast delta");
|
||||
assert_eq!(revoked_delta["kind"], "control_plane_event");
|
||||
assert_eq!(revoked_delta["eventType"], "control.share.revoked");
|
||||
assert!(
|
||||
state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve revoked share link")
|
||||
.is_none()
|
||||
);
|
||||
assert!(state
|
||||
.control_plane()
|
||||
.resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
|
||||
.expect("resolve revoked share link")
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
state
|
||||
.control_plane()
|
||||
@@ -11324,13 +11348,11 @@ fn main() {}
|
||||
payload["workspace"]["manifest"]["ownerId"],
|
||||
"user@example.com"
|
||||
);
|
||||
assert!(
|
||||
payload["workspace"]["manifest"]["capabilities"]
|
||||
.as_array()
|
||||
.expect("capabilities")
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some("markdown_edit"))
|
||||
);
|
||||
assert!(payload["workspace"]["manifest"]["capabilities"]
|
||||
.as_array()
|
||||
.expect("capabilities")
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some("markdown_edit")));
|
||||
ensure_local_workspace_access_for_actor("user@example.com", "user", &root_uri)
|
||||
.expect("owner can access managed workspace");
|
||||
|
||||
@@ -11581,12 +11603,11 @@ fn main() {}
|
||||
execute_local_tree_command(&root_uri, "delete", "local-md:Renamed.md", None, None)
|
||||
.expect("delete loose markdown");
|
||||
assert!(!root.join("Renamed.md").exists());
|
||||
assert!(
|
||||
root.join(".mnote")
|
||||
.join("trash")
|
||||
.join("Renamed.md")
|
||||
.is_file()
|
||||
);
|
||||
assert!(root
|
||||
.join(".mnote")
|
||||
.join("trash")
|
||||
.join("Renamed.md")
|
||||
.is_file());
|
||||
assert_eq!(deleted["resourceKind"].as_str(), Some("markdown"));
|
||||
|
||||
let restored =
|
||||
@@ -13098,6 +13119,11 @@ fn main() {}
|
||||
assert_eq!(asset["uploadIntent"], "editor.markdown.attach");
|
||||
assert_eq!(asset["rootRelativePath"], "docs/README/photo-1.png");
|
||||
assert_eq!(asset["markdownRelativePath"], "photo-1.png");
|
||||
assert_eq!(asset["markdownHref"], "./photo-1.png");
|
||||
assert_eq!(asset["attachmentRef"]["rawHref"], "./photo-1.png");
|
||||
assert_eq!(asset["attachmentRef"]["kind"], "pageLocal");
|
||||
assert_eq!(asset["attachmentRef"]["openKind"], "image");
|
||||
assert_eq!(asset["attachmentRef"]["authorized"], true);
|
||||
assert_eq!(
|
||||
asset["ownerDocumentId"],
|
||||
"local-md:docs~2FREADME~2FREADME.md"
|
||||
@@ -13122,6 +13148,8 @@ fn main() {}
|
||||
assert_eq!(markdown_asset["uploadIntent"], "editor.markdown.attach");
|
||||
assert_eq!(markdown_asset["rootRelativePath"], "docs/README/notes.md");
|
||||
assert_eq!(markdown_asset["markdownRelativePath"], "notes.md");
|
||||
assert_eq!(markdown_asset["markdownHref"], "./notes.md");
|
||||
assert_eq!(markdown_asset["attachmentRef"]["openKind"], "text");
|
||||
assert_eq!(
|
||||
markdown_asset["ownerDocumentId"],
|
||||
"local-md:docs~2FREADME~2FREADME.md"
|
||||
@@ -13155,6 +13183,31 @@ fn main() {}
|
||||
.expect("uploaded asset index");
|
||||
assert!(uploaded_asset_index.contains("docs/README/notes.md"));
|
||||
|
||||
std::fs::write(root.join("docs").join("Loose.md"), "# Loose\n").expect("write loose md");
|
||||
let loose_asset = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
"local-md:docs~2FLoose.md",
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "loose.pdf".to_string(),
|
||||
content_type: "application/pdf".to_string(),
|
||||
bytes: b"%PDF-1.4\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload loose markdown asset");
|
||||
assert_eq!(loose_asset["sourcePath"], "loose.pdf");
|
||||
assert_eq!(loose_asset["rootRelativePath"], "docs/loose.pdf");
|
||||
assert_eq!(loose_asset["markdownRelativePath"], "loose.pdf");
|
||||
assert_eq!(loose_asset["markdownHref"], "./loose.pdf");
|
||||
assert!(
|
||||
root.join("docs").join("loose.pdf").is_file(),
|
||||
"非 bundle Markdown 上传应写入 md 同目录"
|
||||
);
|
||||
assert!(
|
||||
!root.join("docs").join("Loose").join("loose.pdf").exists(),
|
||||
"非 bundle Markdown 上传不应写入同名子目录"
|
||||
);
|
||||
|
||||
let snapshot = load_local_folder_file_tree_children_snapshot(&root_uri, "docs/README")
|
||||
.expect("file tree");
|
||||
let items = snapshot.projection["items"].as_array().expect("items");
|
||||
|
||||
Reference in New Issue
Block a user