151 lines
5.0 KiB
Rust
151 lines
5.0 KiB
Rust
use crate::context::RequestContext;
|
|||
|
|
use crate::error::WebError;
|
||
|
|
use crate::ssr::pages::mindmap::MindmapPage;
|
||
|
|
use axum::extract::{Extension, Path};
|
||
|
|
use axum::http::{HeaderMap, HeaderName, HeaderValue};
|
||
|
|
use axum::response::{Html, IntoResponse, Response};
|
||
|
|
use serde_json::json;
|
||
|
|
|
||
|
|
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||
|
|
const HEADER_MNOTE_WEB_SHELL: &str = "x-mnote-web-shell";
|
||
|
|
|
||
|
|
pub async fn mindmap_object_shell(
|
||
|
|
Extension(context): Extension<RequestContext>,
|
||
|
|
Path((doc_id, mindmap_id)): Path<(String, String)>,
|
||
|
|
) -> Result<Response, WebError> {
|
||
|
|
let contract = json!({
|
||
|
|
"schema": "mnote.mindmap_shell.v1",
|
||
|
|
"owner": "mnote-web",
|
||
|
|
"shell": "mindmap",
|
||
|
|
"documentId": doc_id,
|
||
|
|
"mindmapId": mindmap_id,
|
||
|
|
"projection": {
|
||
|
|
"schema": "mnote.mindmap_projection.v1",
|
||
|
|
"source": "rust-web-object-shell"
|
||
|
|
},
|
||
|
|
"island": {
|
||
|
|
"kind": "react_mindmap_runtime",
|
||
|
|
"mountId": "mnote-mindmap-island",
|
||
|
|
"legacyCompat": "next-app-router"
|
||
|
|
},
|
||
|
|
"requestId": context.trace.request_id,
|
||
|
|
"traceId": context.trace.trace_id
|
||
|
|
});
|
||
|
|
let contract_json = serde_json::to_string(&contract).unwrap_or_else(|_| "null".to_string());
|
||
|
|
let body_content = crate::ssr::render_view(leptos::view! {
|
||
|
|
<MindmapPage
|
||
|
|
document_id={doc_id.clone()}
|
||
|
|
mindmap_id={mindmap_id.clone()}
|
||
|
|
/>
|
||
|
|
});
|
||
|
|
let html = format!(
|
||
|
|
r#"<!doctype html>
|
||
|
|
<html lang="zh-CN">
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<title>思维导图</title>
|
||
|
|
<style>{}</style>
|
||
|
|
</head>
|
||
|
|
<body data-mnote-web-owner="mnote-web" data-mnote-shell="mindmap" data-document-id="{}" data-mindmap-id="{}">
|
||
|
|
{}
|
||
|
|
<script id="__MNOTE_MINDMAP_SHELL__" type="application/json">{}</script>
|
||
|
|
</body>
|
||
|
|
</html>"#,
|
||
|
|
crate::ssr::MNOTE_CSS,
|
||
|
|
escape_html(&doc_id),
|
||
|
|
escape_html(&mindmap_id),
|
||
|
|
body_content,
|
||
|
|
escape_script_json(&contract_json),
|
||
|
|
);
|
||
|
|
let mut response = Html(html).into_response();
|
||
|
|
stamp_shell_headers(response.headers_mut(), "mindmap");
|
||
|
|
Ok(response)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn stamp_shell_headers(headers: &mut HeaderMap, shell: &'static str) {
|
||
|
|
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
|
||
|
|
headers.insert(name, HeaderValue::from_static("mnote-web"));
|
||
|
|
}
|
||
|
|
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_SHELL.as_bytes()) {
|
||
|
|
headers.insert(name, HeaderValue::from_static(shell));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn escape_html(value: &str) -> String {
|
||
|
|
value
|
||
|
|
.replace('&', "&")
|
||
|
|
.replace('<', "<")
|
||
|
|
.replace('>', ">")
|
||
|
|
.replace('"', """)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn escape_script_json(value: &str) -> String {
|
||
|
|
value.replace("</script", "<\\/script")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use crate::app::{build_app, AppConfig, AppState};
|
||
|
|
use axum::body::{to_bytes, Body};
|
||
|
|
use axum::http::{Request, StatusCode};
|
||
|
|
use tower::util::ServiceExt;
|
||
|
|
|
||
|
|
fn app() -> axum::Router {
|
||
|
|
build_app(AppState::new(AppConfig {
|
||
|
|
service_name: "mnote-web".into(),
|
||
|
|
service_version: "0.1.0".into(),
|
||
|
|
bind_addr: "127.0.0.1:0".into(),
|
||
|
|
public_bind_addr: "127.0.0.1:3000".into(),
|
||
|
|
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||
|
|
enable_legacy_next_compat: true,
|
||
|
|
enable_debug_shell_routes: false,
|
||
|
|
hermes_base_path: "/api/hermes".into(),
|
||
|
|
compat_next_base_path: "/api/compat/next".into(),
|
||
|
|
convex_url: None,
|
||
|
|
convex_admin_key: None,
|
||
|
|
allow_dev_fixtures: true,
|
||
|
|
query_fixtures_json: None,
|
||
|
|
mutation_fixtures_json: None,
|
||
|
|
dev_user_id: "dev-user".into(),
|
||
|
|
dev_user_name: "开发用户".into(),
|
||
|
|
dev_user_email: "dev@mnote.local".into(),
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn mindmap_shell_returns_rust_object_shell_contract() {
|
||
|
|
let response = app()
|
||
|
|
.oneshot(
|
||
|
|
Request::builder()
|
||
|
|
.uri("/mindmap/doc_1/mind_1")
|
||
|
|
.body(Body::empty())
|
||
|
|
.expect("request"),
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.expect("response");
|
||
|
|
|
||
|
|
assert_eq!(response.status(), StatusCode::OK);
|
||
|
|
assert_eq!(
|
||
|
|
response
|
||
|
|
.headers()
|
||
|
|
.get("x-mnote-web-owner")
|
||
|
|
.and_then(|value| value.to_str().ok()),
|
||
|
|
Some("mnote-web")
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
response
|
||
|
|
.headers()
|
||
|
|
.get("x-mnote-web-shell")
|
||
|
|
.and_then(|value| value.to_str().ok()),
|
||
|
|
Some("mindmap")
|
||
|
|
);
|
||
|
|
let body = to_bytes(response.into_body(), usize::MAX)
|
||
|
|
.await
|
||
|
|
.expect("body");
|
||
|
|
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||
|
|
assert!(html.contains("mnote.mindmap_shell.v1"));
|
||
|
|
assert!(html.contains("data-react-island=\"mindmap_runtime\""));
|
||
|
|
assert!(html.contains("rust-web-object-shell"));
|
||
|
|
}
|
||
|
|
}
|