收口 Rust Web 入口与 AI 写入链

- 将 3000 主入口继续收口到 mnote-web,补齐 /favicon.ico、/api/auth、session alias、AI run 等 Rust Web 路由边界。

- 更新登录页与 Convex Auth 代理,支持测试账号快速登录写入真实 Convex Auth cookie。

- 推进页面设置、Wolai 对齐、Phase 7 AI kernel/CLI-first 设计文档与相关 smoke 脚本。

- 更新 leptos-tiptap 生成资产、mnote-cli/bridge-runtime、前端依赖和 dev/prod 启动脚本。
This commit is contained in:
lix-2026
2026-05-06 21:44:20 +08:00
parent 98b6360595
commit e8ba12e461
86 changed files with 8872 additions and 1716 deletions
+29 -1
View File
@@ -1865,12 +1865,14 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams 0.4.2",
"web-sys",
"webpki-roots",
]
@@ -2116,7 +2118,7 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"wasm-streams 0.5.0",
"web-sys",
"xxhash-rust",
]
@@ -2489,6 +2491,19 @@ dependencies = [
"tungstenite",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
@@ -2887,6 +2902,19 @@ dependencies = [
"wasmparser",
]
[[package]]
name = "wasm-streams"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "wasm-streams"
version = "0.5.0"
+41 -29
View File
@@ -583,7 +583,6 @@ struct DocumentSaveCommandPayload {
#[serde(rename_all = "camelCase")]
struct DocumentOptionsCommandPayload {
document_id: String,
workspace_id: Option<String>,
options: DocumentOptionsPatchPayload,
}
@@ -7993,6 +7992,46 @@ fn execute_command(
_ => "documents.options.update",
};
let options = payload.options;
let mut options_json = serde_json::Map::new();
if let Some(value) = options.wide_layout {
options_json.insert("wideLayout".into(), json!(value));
}
if let Some(value) = options.small_text {
options_json.insert("smallText".into(), json!(value));
}
if let Some(value) = options.show_heading_numbers {
options_json.insert("showHeadingNumbers".into(), json!(value));
}
if let Some(value) = options.show_toc {
options_json.insert("showToc".into(), json!(value));
}
if let Some(value) = options.show_structure {
options_json.insert("showStructure".into(), json!(value));
}
if let Some(value) = options.protect_editing {
options_json.insert("protectEditing".into(), json!(value));
}
if let Some(value) = options.show_word_count {
options_json.insert("showWordCount".into(), json!(value));
}
if let Some(value) = options.collapse_backlinks {
options_json.insert("collapseBacklinks".into(), json!(value));
}
if let Some(value) = options.page_font.clone() {
options_json.insert("pageFont".into(), json!(value));
}
if let Some(value) = options.layout_density.clone() {
options_json.insert("layoutDensity".into(), json!(value));
}
if let Some(value) = options.hide_child_pages {
options_json.insert("hideChildPages".into(), json!(value));
}
if let Some(value) = options.show_block_ref_count {
options_json.insert("showBlockRefCount".into(), json!(value));
}
if let Some(value) = options.embed_default_block_id.clone() {
options_json.insert("embedDefaultBlockId".into(), json!(value));
}
let command = CommandEnvelope {
name: command_name.into(),
command_id: command_wire.command_id.clone(),
@@ -8034,22 +8073,7 @@ fn execute_command(
payload_json: request.payload_json,
args_json: json!({
"id": payload.document_id,
"workspaceId": payload.workspace_id,
"options": {
"wideLayout": options.wide_layout,
"smallText": options.small_text,
"showHeadingNumbers": options.show_heading_numbers,
"showToc": options.show_toc,
"showStructure": options.show_structure,
"protectEditing": options.protect_editing,
"showWordCount": options.show_word_count,
"collapseBacklinks": options.collapse_backlinks,
"pageFont": options.page_font,
"layoutDensity": options.layout_density,
"hideChildPages": options.hide_child_pages,
"showBlockRefCount": options.show_block_ref_count,
"embedDefaultBlockId": options.embed_default_block_id,
},
"options": Value::Object(options_json),
}),
}))
}
@@ -13395,21 +13419,9 @@ mod tests {
plan.args_json,
json!({
"id": "doc_1",
"workspaceId": "ws_1",
"options": {
"wideLayout": Value::Null,
"smallText": Value::Null,
"showHeadingNumbers": Value::Null,
"showToc": true,
"showStructure": Value::Null,
"protectEditing": Value::Null,
"showWordCount": Value::Null,
"collapseBacklinks": Value::Null,
"pageFont": Value::Null,
"layoutDensity": "compact",
"hideChildPages": Value::Null,
"showBlockRefCount": Value::Null,
"embedDefaultBlockId": Value::Null,
},
})
);
+86 -35
View File
@@ -90,6 +90,7 @@ pub struct CliOutputContext {
pub actor_id: String,
pub actor_type: String,
pub session_id: Option<String>,
pub reason: Option<String>,
pub workspace_id: Option<String>,
pub idempotency_key: Option<String>,
pub validate_only: bool,
@@ -566,6 +567,7 @@ pub fn plan_page_get(
"page",
"get",
&bridge,
ctx,
query.name,
json!({
"pageId": page_id,
@@ -614,6 +616,7 @@ pub fn plan_page_title(
"page",
"title",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -671,6 +674,7 @@ pub fn plan_page_save(
"page",
"save",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -724,6 +728,7 @@ pub fn plan_page_create(ctx: &CliContext, args: &PageCreateArgs<'_>) -> CliResul
"page",
"create",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -778,6 +783,7 @@ pub fn plan_page_move(ctx: &CliContext, args: &PageMoveArgs<'_>) -> CliResult<Cl
"page",
"move",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -825,6 +831,7 @@ pub fn plan_page_delete(ctx: &CliContext, args: &PageDeleteArgs<'_>) -> CliResul
"page",
"delete",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -868,6 +875,7 @@ pub fn plan_page_restore(ctx: &CliContext, args: &PageRestoreArgs<'_>) -> CliRes
"page",
"restore",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -923,6 +931,7 @@ pub fn plan_block_insert(
"block",
"insert",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -988,6 +997,7 @@ pub fn plan_block_patch(
"block",
"patch",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -1042,6 +1052,7 @@ pub fn plan_block_move(ctx: &CliContext, args: &BlockMoveArgs<'_>) -> CliResult<
"block",
"move",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -1091,6 +1102,7 @@ pub fn plan_block_embed(ctx: &CliContext, args: &BlockEmbedArgs<'_>) -> CliResul
"block",
"embed",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -1147,6 +1159,7 @@ pub fn plan_search_documents(
"search",
"documents",
&bridge,
ctx,
envelope.name,
json!({
"query": query,
@@ -1209,6 +1222,7 @@ pub fn plan_search_blocks(
"search",
"blocks",
&bridge,
ctx,
envelope.name,
json!({
"query": query,
@@ -1245,6 +1259,7 @@ pub fn plan_sidebar_dataset(ctx: &CliContext, workspace_id: &str) -> CliResult<C
"sidebar",
"dataset",
&bridge,
ctx,
envelope.name,
json!({
"workspaceId": workspace_id,
@@ -1286,6 +1301,7 @@ pub fn plan_mindmap_get(
"mindmap",
"get",
&bridge,
ctx,
envelope.name,
json!({
"workspaceId": workspace_id,
@@ -1351,6 +1367,7 @@ pub fn plan_mindmap_put(
"mindmap",
"put",
&bridge,
ctx,
command.name,
command.command_id,
json!({
@@ -2148,7 +2165,7 @@ fn execute_convex_query_raw(
args_json: Value,
cli_ctx: &CliContext,
) -> CliResult<Value> {
let env = ConvexCliEnv::load()?;
let env = ConvexCliEnv::load(cli_ctx)?;
let payload = json!({
"path": function_name,
"format": "convex_encoded_json",
@@ -2171,7 +2188,7 @@ fn execute_convex_mutation_raw(
args_json: Value,
cli_ctx: &CliContext,
) -> CliResult<Value> {
let env = ConvexCliEnv::load()?;
let env = ConvexCliEnv::load(cli_ctx)?;
let payload = json!({
"path": function_name,
"format": "convex_encoded_json",
@@ -2225,7 +2242,7 @@ struct ConvexCliEnv {
}
impl ConvexCliEnv {
fn load() -> CliResult<Self> {
fn load(cli_ctx: &CliContext) -> CliResult<Self> {
let url = read_env_or_dotenv("CONVEX_SELF_HOSTED_URL")?
.or_else(|| env::var("NEXT_PUBLIC_CONVEX_URL").ok())
.ok_or_else(|| {
@@ -2233,18 +2250,14 @@ impl ConvexCliEnv {
})?;
let admin_key = read_env_or_dotenv("CONVEX_SELF_HOSTED_ADMIN_KEY")?
.ok_or_else(|| CliError::validation("缺少 CONVEX_SELF_HOSTED_ADMIN_KEY"))?;
let dev_user_id = read_env_or_dotenv("DEV_USER_ID")?.unwrap_or_else(|| "dev-user".into());
let dev_user_id = normalize_actor_for_convex_identity(&cli_ctx.actor_id)
.or(read_env_or_dotenv("DEV_USER_ID")?)
.unwrap_or_else(|| "dev-user".into());
let dev_user_name =
read_env_or_dotenv("DEV_USER_NAME")?.unwrap_or_else(|| "开发用户".into());
let dev_user_email =
read_env_or_dotenv("DEV_USER_EMAIL")?.unwrap_or_else(|| "dev@mnote.local".into());
let identity = json!({
"subject": dev_user_id,
"issuer": "https://mnote.local/dev-auth",
"tokenIdentifier": format!("dev-user|{}", dev_user_id),
"name": dev_user_name,
"email": dev_user_email,
});
let identity = build_convex_dev_identity(&dev_user_id, &dev_user_name, &dev_user_email);
let encoded = base64::engine::general_purpose::STANDARD
.encode(serde_json::to_string(&identity).map_err(|error| {
CliError::transport(format!("开发用户身份序列化失败: {error}"))
@@ -2262,6 +2275,25 @@ impl ConvexCliEnv {
}
}
fn normalize_actor_for_convex_identity(actor_id: &str) -> Option<String> {
let trimmed = actor_id.trim();
if trimmed.is_empty() || trimmed == "anonymous" || trimmed == "cli_user" {
None
} else {
Some(trimmed.to_string())
}
}
fn build_convex_dev_identity(user_id: &str, name: &str, email: &str) -> Value {
json!({
"subject": user_id,
"issuer": "https://mnote.local/dev-auth",
"tokenIdentifier": format!("dev-user|{}", user_id),
"name": name,
"email": email,
})
}
fn read_env_or_dotenv(key: &str) -> CliResult<Option<String>> {
if let Ok(value) = env::var(key) {
let trimmed = value.trim().to_string();
@@ -2457,6 +2489,7 @@ fn build_query_output(
domain: &str,
action: &str,
bridge: &BridgeContext,
ctx: &CliContext,
name: String,
normalized_input: Value,
transport: CliTransportPlan,
@@ -2466,18 +2499,7 @@ fn build_query_output(
entrypoint: "mnote-cli",
domain: domain.into(),
action: action.into(),
context: build_output_context(
bridge,
&CliContext {
actor_id: bridge.actor_id.clone(),
actor_type: bridge.actor_type.clone(),
session_id: bridge.session_id.clone(),
reason: None,
idempotency_key: bridge.idempotency_key.clone(),
validate_only: bridge.validate_only,
dry_run: bridge.dry_run,
},
),
context: build_output_context(bridge, ctx),
operation: CliOperationOutput::Query {
name,
normalized_input,
@@ -2491,6 +2513,7 @@ fn build_command_output(
domain: &str,
action: &str,
bridge: &BridgeContext,
ctx: &CliContext,
name: String,
command_id: String,
normalized_input: Value,
@@ -2501,18 +2524,7 @@ fn build_command_output(
entrypoint: "mnote-cli",
domain: domain.into(),
action: action.into(),
context: build_output_context(
bridge,
&CliContext {
actor_id: bridge.actor_id.clone(),
actor_type: bridge.actor_type.clone(),
session_id: bridge.session_id.clone(),
reason: None,
idempotency_key: bridge.idempotency_key.clone(),
validate_only: bridge.validate_only,
dry_run: bridge.dry_run,
},
),
context: build_output_context(bridge, ctx),
operation: CliOperationOutput::Command {
name,
command_id,
@@ -2530,6 +2542,7 @@ fn build_output_context(bridge: &BridgeContext, ctx: &CliContext) -> CliOutputCo
actor_id: bridge.actor_id.clone(),
actor_type: bridge.actor_type.clone(),
session_id: bridge.session_id.clone(),
reason: ctx.reason.clone(),
workspace_id: bridge.workspace_id.clone(),
idempotency_key: ctx.idempotency_key.clone(),
validate_only: bridge.validate_only,
@@ -2786,6 +2799,18 @@ mod tests {
}
}
#[test]
fn output_context_keeps_reason_for_audit_trace() {
let ctx = CliContext {
reason: Some("phase7-cli-first".into()),
..CliContext::default()
};
let output = plan_page_save(&ctx, "page_1", Some("ws_1"), None, "[]", None)
.expect("page save plan should build");
assert_eq!(output.context.reason.as_deref(), Some("phase7-cli-first"));
}
#[test]
fn page_create_json_contract_uses_documents_create() {
let output = plan_page_create(
@@ -2817,6 +2842,32 @@ mod tests {
}
}
#[test]
fn convex_dev_identity_prefers_explicit_cli_actor() {
let ctx = CliContext {
actor_id: "nn7bhmt782sykdrecah0rbe2nx867ks1".into(),
..CliContext::default()
};
let user_id = normalize_actor_for_convex_identity(&ctx.actor_id)
.expect("显式 CLI actor 应成为 Convex 写入身份");
let identity = build_convex_dev_identity(&user_id, "开发用户", "dev@mnote.local");
assert_eq!(
identity["subject"],
json!("nn7bhmt782sykdrecah0rbe2nx867ks1")
);
assert_eq!(
identity["tokenIdentifier"],
json!("dev-user|nn7bhmt782sykdrecah0rbe2nx867ks1")
);
}
#[test]
fn convex_dev_identity_ignores_legacy_default_cli_actor() {
assert_eq!(normalize_actor_for_convex_identity("cli_user"), None);
assert_eq!(normalize_actor_for_convex_identity(" anonymous "), None);
}
#[test]
fn page_move_json_contract_uses_documents_move() {
let output = plan_page_move(
+1 -1
View File
@@ -12,7 +12,7 @@ core-protocol = { path = "../core-protocol" }
futures-util = "0.3"
leptos = { version = "0.8.14", default-features = false, features = ["ssr"] }
mnote-editor-core = { path = "../mnote-editor-core" }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "stream"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
storage-convex-bridge = { path = "../storage-convex-bridge" }
+668 -26
View File
@@ -3,26 +3,21 @@ use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::query_support::resolve_effective_workspace_id;
use crate::routes::snapshot_support::{load_projection_snapshot, ProjectionSnapshotSpec};
use axum::body::{Body, Bytes};
use axum::extract::Query;
use axum::extract::{Extension, State};
use axum::http::StatusCode;
use axum::http::{header, HeaderMap, HeaderName, HeaderValue, Request, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use core_protocol::KernelProjectionKind;
use futures_util::{StreamExt, TryStreamExt};
use serde::Deserialize;
use serde::Serialize;
use serde_json::{json, Value};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompatBoundaryResponse {
pub ok: bool,
pub boundary: &'static str,
pub compatibility: &'static str,
pub request_id: String,
pub trace_id: String,
pub target: String,
pub notes: Vec<&'static str>,
}
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -33,19 +28,548 @@ pub struct CompatSidebarQuery {
pub async fn next_ai_agent_run(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
) -> Json<CompatBoundaryResponse> {
Json(CompatBoundaryResponse {
ok: true,
boundary: "next_route_compat",
compatibility: "placeholder",
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
target: format!("{}/bridge", state.config().hermes_base_path),
notes: vec![
"/api/ai-agent/run 是 legacy compat endpointcanonical route 是 /api/hermes/bridge。",
"结构化写入必须通过 Hermes/Rust bridge 再落到 page/tree/edge command。",
],
request: Request<Body>,
) -> Result<Response, WebError> {
let (parts, body) = request.into_parts();
let body = axum::body::to_bytes(body, 10 * 1024 * 1024)
.await
.map_err(|error| WebError::internal(format!("读取 AI 请求体失败: {error}")))?;
let payload: Value = serde_json::from_slice(&body).map_err(|error| {
WebError::bad_request_code(
"ai_agent_bad_request",
format!("AI 请求体不是合法 JSON: {error}"),
)
.with_context(&context)
.with_header("x-mnote-web-owner", "mnote-web")
})?;
if state.config().enable_legacy_next_compat {
if let Some(base_url) = state.config().legacy_next_base_url.as_deref() {
if let Ok(response) =
proxy_ai_agent_run_to_next(base_url, &context, &parts.headers, body.clone()).await
{
return Ok(response);
}
}
}
if let Ok(response) = run_local_mnote_cli_ai_host(&state, &context, &payload).await {
return Ok(response);
}
let Some(backend_url) = resolve_ai_orchestrator_backend_url() else {
return Err(WebError::bad_gateway_code(
"ai_orchestrator_unavailable",
"未配置 BACKEND_URL,无法连接 document AI orchestrator。",
)
.with_context(&context)
.with_header("x-mnote-web-owner", "mnote-web"));
};
let upstream_url = reqwest::Url::parse(&format!(
"{}/api/v1/ai-agent/document/run",
backend_url.trim_end_matches('/')
))
.map_err(|error| WebError::internal(format!("AI orchestrator URL 非法: {error}")))?;
let forward_payload = build_document_ai_orchestrator_payload(&state, &context, &payload);
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|error| {
WebError::internal(format!("AI orchestrator HTTP 客户端创建失败: {error}"))
})?;
let mut upstream_request = client.post(upstream_url).json(&forward_payload);
upstream_request = apply_ai_forward_headers(upstream_request, &parts.headers, &context);
if let Some(api_key) = read_env_or_dotenv("MNOTE_AI_ORCHESTRATOR_API_KEY") {
upstream_request = upstream_request.header("x-mnote-ai-key", api_key);
}
let upstream_response = upstream_request.send().await.map_err(|error| {
WebError::bad_gateway_code(
"ai_orchestrator_proxy_error",
format!("document AI orchestrator 请求失败: {error}"),
)
.with_context(&context)
.with_header("x-mnote-web-owner", "mnote-web")
.with_header("x-upstream-service", "document-ai-orchestrator")
})?;
if !upstream_response.status().is_success() {
let status = upstream_response.status();
let body = upstream_response
.text()
.await
.unwrap_or_else(|error| format!("读取 upstream 错误响应失败: {error}"));
return Err(WebError::bad_gateway_code(
"ai_orchestrator_upstream_error",
format!(
"document AI orchestrator 返回 HTTP {}: {}",
status.as_u16(),
body
),
)
.with_context(&context)
.with_header("x-mnote-web-owner", "mnote-web")
.with_header("x-upstream-service", "document-ai-orchestrator"));
}
build_ai_sse_proxy_response(upstream_response, &context)
}
async fn proxy_ai_agent_run_to_next(
base_url: &str,
context: &RequestContext,
headers: &HeaderMap,
body_bytes: Bytes,
) -> Result<Response, WebError> {
let upstream_url = reqwest::Url::parse(&format!(
"{}/api/ai-agent/run",
base_url.trim_end_matches('/')
))
.map_err(|error| WebError::internal(format!("Next AI route URL 非法: {error}")))?;
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|error| WebError::internal(format!("Next AI HTTP 客户端创建失败: {error}")))?;
let mut upstream_request = client.post(upstream_url).body(body_bytes.clone());
for (name, value) in headers.iter() {
if is_hop_by_hop_header(name.as_str())
|| name == header::HOST
|| name == header::CONTENT_LENGTH
|| name == header::COOKIE
|| name == header::AUTHORIZATION
{
continue;
}
upstream_request = upstream_request.header(name.as_str(), value.as_bytes());
}
if let Some(cookie) = context.auth.cookie_header.as_deref() {
upstream_request = upstream_request.header(header::COOKIE, cookie);
}
if let Some(authorization) = context.auth.authorization.as_deref() {
upstream_request = upstream_request.header(header::AUTHORIZATION, authorization);
}
upstream_request = upstream_request.header("x-request-id", context.trace.request_id.as_str());
upstream_request = upstream_request.header("x-trace-id", context.trace.trace_id.as_str());
upstream_request = upstream_request.header("x-mnote-source-channel", "rust_web_route");
upstream_request = upstream_request.header("x-mnote-source-client", "mnote-web");
upstream_request = upstream_request.header("x-mnote-actor-id", context.auth.actor_id.as_str());
upstream_request =
upstream_request.header("x-mnote-actor-type", context.auth.actor_type.as_str());
if let Some(workspace_id) = context.workspace.workspace_id.as_deref() {
upstream_request = upstream_request.header("x-mnote-workspace-id", workspace_id);
}
if let Some(session_id) = context.auth.session_id.as_deref() {
upstream_request = upstream_request.header("x-mnote-session-id", session_id);
}
let upstream_response = upstream_request.send().await.map_err(|error| {
WebError::bad_gateway_code(
"next_ai_proxy_error",
format!("Next /api/ai-agent/run 请求失败: {error}"),
)
.with_context(context)
.with_header("x-mnote-web-owner", "mnote-web")
.with_header("x-upstream-service", "next-ai-route")
})?;
if !upstream_response.status().is_success() {
let status = upstream_response.status();
let body = upstream_response
.text()
.await
.unwrap_or_else(|error| format!("读取 Next AI 错误响应失败: {error}"));
return Err(WebError::bad_gateway_code(
"next_ai_upstream_error",
format!(
"Next /api/ai-agent/run 返回 HTTP {}: {}",
status.as_u16(),
body
),
)
.with_context(context)
.with_header("x-mnote-web-owner", "mnote-web")
.with_header("x-upstream-service", "next-ai-route"));
}
build_ai_sse_proxy_response(upstream_response, context)
}
fn resolve_repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..")
}
fn build_mnote_cli_args(context: &RequestContext, payload: &Value) -> Vec<String> {
let ai = payload
.get("options")
.and_then(|value| value.get("ai"))
.cloned()
.unwrap_or(Value::Null);
let runtime_context = payload.get("context").cloned().unwrap_or(Value::Null);
let document_id = runtime_context
.get("documentId")
.and_then(Value::as_str)
.unwrap_or("current");
let workspace_id = runtime_context
.get("workspaceId")
.and_then(Value::as_str)
.or(context.workspace.workspace_id.as_deref());
let session_id = ai
.get("sessionId")
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
.map(str::to_string)
.unwrap_or_else(|| format!("ai-{}", context.trace.request_id));
let args_json = json!({
"pageId": document_id,
"documentId": document_id,
"workspaceId": workspace_id,
"provider": ai.get("provider").cloned().unwrap_or(Value::Null),
"modelKey": ai.get("modelKey").cloned().unwrap_or(Value::Null),
"profileId": ai.get("profileId").cloned().unwrap_or(Value::Null),
"selectedUids": runtime_context.get("selectedUids").cloned().unwrap_or(Value::Null),
"pageOptions": runtime_context.get("pageOptions").cloned().unwrap_or(Value::Null),
})
.to_string();
vec![
"run".into(),
"--quiet".into(),
"--manifest-path".into(),
resolve_repo_root()
.join("rust")
.join("Cargo.toml")
.to_string_lossy()
.to_string(),
"-p".into(),
"mnote-cli".into(),
"--".into(),
"--json".into(),
"--validate-only".into(),
"--dry-run".into(),
"--actor-id".into(),
context.auth.actor_id.clone(),
"--actor-type".into(),
context.auth.actor_type.clone(),
"--session-id".into(),
session_id,
"--reason".into(),
"ai-agent-run:mnote-web-rust-host".into(),
"tool".into(),
"run".into(),
"--tool-name".into(),
"doc_get".into(),
"--kind".into(),
"query".into(),
"--mode".into(),
"explain-plan".into(),
"--args-json".into(),
args_json,
]
}
async fn run_local_mnote_cli_ai_host(
state: &AppState,
context: &RequestContext,
payload: &Value,
) -> Result<Response, WebError> {
let stream = payload
.get("stream")
.and_then(Value::as_bool)
.unwrap_or(true);
let args = build_mnote_cli_args(context, payload);
let repo_root = resolve_repo_root();
let actor_id =
if context.auth.actor_id.trim().is_empty() || context.auth.actor_id == "anonymous" {
state.config().dev_user_id.clone()
} else {
context.auth.actor_id.clone()
};
let actor_type =
if context.auth.actor_type.trim().is_empty() || context.auth.actor_type == "anonymous" {
"user".to_string()
} else {
context.auth.actor_type.clone()
};
let dev_email = state.config().dev_user_email.clone();
let dev_name = state.config().dev_user_name.clone();
let output = tokio::task::spawn_blocking(move || {
Command::new("cargo")
.args(args)
.current_dir(repo_root)
.env("CARGO_TERM_COLOR", "never")
.env(
"RUSTUP_TOOLCHAIN",
env::var("RUSTUP_TOOLCHAIN")
.ok()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "1.89.0".into()),
)
.env("DEV_USER_ID", actor_id)
.env("DEV_USER_EMAIL", dev_email)
.env("DEV_USER_NAME", dev_name)
.env("MNOTE_CLI_ALLOW_CREATE_PAGE", "1")
.env("MNOTE_CLI_ALLOW_EDIT", "1")
.env("MNOTE_ACTOR_TYPE", actor_type)
.output()
})
.await
.map_err(|error| {
WebError::bad_gateway_code(
"mnote_cli_host_join_error",
format!("mnote-cli host join 失败: {error}"),
)
.with_context(context)
.with_header("x-mnote-web-owner", "mnote-web")
})?
.map_err(|error| {
WebError::bad_gateway_code(
"mnote_cli_host_spawn_error",
format!("mnote-cli host 启动失败: {error}"),
)
.with_context(context)
.with_header("x-mnote-web-owner", "mnote-web")
})?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if !stream {
if !output.status.success() {
return Err(WebError::bad_gateway_code(
"mnote_cli_host_failed",
if stderr.is_empty() {
"mnote-cli 执行失败".into()
} else {
stderr
},
)
.with_context(context)
.with_header("x-mnote-web-owner", "mnote-web"));
}
let mut response = Json(json!({
"ok": true,
"bridgeOwner": "mnote-cli",
"text": if stdout.is_empty() { "mnote-cli 无输出" } else { &stdout },
}))
.into_response();
stamp_owner_header(response.headers_mut());
response.headers_mut().insert(
HeaderName::from_static("x-mnote-ai-execution-owner"),
HeaderValue::from_static("mnote-cli"),
);
return Ok(response);
}
let body = if output.status.success() {
format!(
"event: ready\ndata: {}\n\nevent: assistant_message\ndata: {}\n\nevent: completion\ndata: {}\n\n",
json!({"ok": true, "bridgeOwner": "mnote-cli"}).to_string(),
json!({"text": if stdout.is_empty() { "mnote-cli 无输出" } else { &stdout }}).to_string(),
json!({"ok": true, "text": if stdout.is_empty() { "mnote-cli 无输出" } else { &stdout }, "steps": 1}).to_string(),
)
} else {
format!(
"event: ready\ndata: {}\n\nevent: error\ndata: {}\n\n",
json!({"ok": true, "bridgeOwner": "mnote-cli"}).to_string(),
json!({"ok": false, "message": if stderr.is_empty() { "mnote-cli 执行失败" } else { &stderr }}).to_string(),
)
};
let mut response = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/event-stream; charset=utf-8")
.header(header::CACHE_CONTROL, "no-cache, no-transform")
.header(header::CONNECTION, "keep-alive")
.header("x-accel-buffering", "no")
.header("x-mnote-ai-execution-owner", "mnote-cli")
.body(Body::from(body))
.map_err(|error| WebError::internal(format!("mnote-cli SSE 响应构造失败: {error}")))?;
stamp_owner_header(response.headers_mut());
Ok(response)
}
fn build_document_ai_orchestrator_payload(
state: &AppState,
context: &RequestContext,
payload: &Value,
) -> Value {
let ai_options = payload.get("options").and_then(|value| value.get("ai"));
let user_id = if context.auth.actor_id.trim().is_empty() || context.auth.actor_id == "anonymous"
{
state.config().dev_user_id.clone()
} else {
context.auth.actor_id.clone()
};
json!({
"userId": user_id,
"sessionId": ai_options
.and_then(|value| value.get("sessionId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty()),
"model": ai_options
.and_then(|value| value.get("model"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty()),
"modelKey": ai_options
.and_then(|value| value.get("modelKey"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty()),
"profileId": ai_options
.and_then(|value| value.get("profileId"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty()),
"maxSteps": payload.get("maxSteps").filter(|value| !value.is_null()).cloned().unwrap_or_else(|| json!(10)),
"messages": payload.get("messages").cloned().unwrap_or_else(|| json!([])),
"context": build_document_ai_context(payload.get("context")),
})
}
fn build_document_ai_context(context: Option<&Value>) -> Value {
let get = |key: &str| {
context
.and_then(|value| value.get(key))
.cloned()
.unwrap_or(Value::Null)
};
json!({
"source": get("source"),
"action": get("action"),
"documentId": get("documentId"),
"workspaceId": get("workspaceId"),
"selectedBlockId": get("selectedBlockId"),
"selectedBlockIndex": get("selectedBlockIndex"),
"selectedUids": get("selectedUids"),
"selectedText": get("selectedText"),
"selection": get("selection"),
"tiptapDocument": get("tiptapDocument"),
"documentBlocks": get("documentBlocks"),
"node": get("node"),
"subtree": get("subtree"),
"outline": get("outline"),
"evidence": get("evidence"),
"pageOptions": get("pageOptions"),
})
}
fn apply_ai_forward_headers(
mut request: reqwest::RequestBuilder,
headers: &HeaderMap,
context: &RequestContext,
) -> reqwest::RequestBuilder {
for (name, value) in headers.iter() {
if is_hop_by_hop_header(name.as_str())
|| name == header::HOST
|| name == header::CONTENT_LENGTH
|| name == header::CONTENT_TYPE
{
continue;
}
request = request.header(name.as_str(), value.as_bytes());
}
request = request.header(header::CONTENT_TYPE.as_str(), "application/json");
request = request.header("x-request-id", context.trace.request_id.as_str());
request = request.header("x-trace-id", context.trace.trace_id.as_str());
request = request.header("x-mnote-source-channel", "rust_web_route");
request = request.header("x-mnote-source-client", "mnote-web");
request = request.header("x-mnote-actor-id", context.auth.actor_id.as_str());
request.header("x-mnote-actor-type", context.auth.actor_type.as_str())
}
fn build_ai_sse_proxy_response(
upstream_response: reqwest::Response,
context: &RequestContext,
) -> Result<Response, WebError> {
let status =
StatusCode::from_u16(upstream_response.status().as_u16()).unwrap_or(StatusCode::OK);
let ready = Bytes::from(format!(
"event: ready\ndata: {}\n\n",
json!({"ok": true, "requestId": context.trace.request_id}).to_string()
));
let upstream_stream = upstream_response
.bytes_stream()
.map_err(std::io::Error::other);
let body_stream = futures_util::stream::once(async move { Ok::<Bytes, std::io::Error>(ready) })
.chain(upstream_stream);
let mut response = Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "text/event-stream; charset=utf-8")
.header(header::CACHE_CONTROL, "no-cache, no-transform")
.header(header::CONNECTION, "keep-alive")
.header("x-accel-buffering", "no")
.body(Body::from_stream(body_stream))
.map_err(|error| WebError::internal(format!("AI SSE 响应构造失败: {error}")))?;
stamp_owner_header(response.headers_mut());
Ok(response)
}
fn stamp_owner_header(headers: &mut HeaderMap) {
if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-web-owner") {
headers.insert(name, HeaderValue::from_static("mnote-web"));
}
}
fn resolve_ai_orchestrator_backend_url() -> Option<String> {
read_env_or_dotenv("BACKEND_INTERNAL_URL")
.or_else(|| read_env_or_dotenv("BACKEND_URL"))
.map(|value| value.trim().trim_end_matches('/').to_string())
.filter(|value| !value.is_empty())
}
fn read_env_or_dotenv(key: &str) -> Option<String> {
if let Ok(value) = env::var(key) {
let trimmed = value.trim().trim_matches('"').to_string();
if !trimmed.is_empty() {
return Some(trimmed);
}
}
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.join(".env.all");
let content = fs::read_to_string(root).ok()?;
for line in content.lines() {
let line = line.trim_end_matches('\r');
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
let Some((k, v)) = line.split_once('=') else {
continue;
};
if k.trim() != key {
continue;
}
let trimmed = v.trim().trim_matches('"').to_string();
if !trimmed.is_empty() {
return Some(trimmed);
}
}
None
}
fn is_hop_by_hop_header(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"connection"
| "keep-alive"
| "proxy-authenticate"
| "proxy-authorization"
| "te"
| "trailers"
| "transfer-encoding"
| "upgrade"
)
}
pub async fn next_sidebar(
@@ -97,6 +621,8 @@ mod tests {
use crate::app::{build_app, AppConfig, AppState};
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::response::IntoResponse;
use tokio::net::TcpListener;
use tower::util::ServiceExt;
fn app() -> axum::Router {
@@ -135,4 +661,120 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn direct_ai_agent_run_is_owned_by_rust_web_when_next_compat_disabled() {
let response = 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: false,
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(),
}))
.oneshot(
Request::builder()
.method("POST")
.uri("/api/ai-agent/run")
.header("content-type", "application/json")
.body(Body::from(r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"online"}}}"#))
.expect("request"),
)
.await
.expect("response");
assert_ne!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
response
.headers()
.get("x-mnote-web-owner")
.and_then(|value| value.to_str().ok()),
Some("mnote-web")
);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let text = String::from_utf8(body.to_vec()).expect("utf8");
assert!(!text.contains("legacy_next_compat_disabled"));
}
#[tokio::test]
async fn ai_agent_run_proxies_to_next_ai_route_when_legacy_next_compat_enabled() {
let next_app = axum::Router::new().route(
"/api/ai-agent/run",
axum::routing::post(|| async move {
(
[(
axum::http::header::CONTENT_TYPE,
"text/event-stream; charset=utf-8",
)],
"event: assistant_message\ndata: {\"text\":\"hello from next ai\"}\n\n",
)
.into_response()
}),
);
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind next");
let addr = listener.local_addr().expect("local addr");
let server = tokio::spawn(async move {
axum::serve(listener, next_app).await.expect("serve next");
});
let response = 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(format!("http://{}", addr)),
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(),
}))
.oneshot(
Request::builder()
.method("POST")
.uri("/api/ai-agent/run")
.header("content-type", "application/json")
.body(Body::from(r#"{"stream":true,"scope":"document","messages":[{"role":"user","content":"ping"}],"context":{"documentId":"doc-1"},"options":{"ai":{"provider":"hermes"}}}"#))
.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")
);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let text = String::from_utf8(body.to_vec()).expect("utf8");
assert!(text.contains("assistant_message"));
assert!(text.contains("hello from next ai"));
server.abort();
}
}
@@ -65,6 +65,12 @@ pub struct DocumentOptionsRequest {
pub command_name: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentPurgeRequest {
pub document_id: String,
}
const NEXT_DOCUMENTS_BASE_URL_ENV: &str = "MNOTE_NEXT_BASE_URL";
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_DOCUMENTS_TRANSPORT: &str = "x-mnote-documents-transport";
@@ -552,6 +558,50 @@ pub async fn save(
Ok(ok_response(&context, result))
}
pub async fn purge(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
Json(body): Json<DocumentPurgeRequest>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
let document_id = body.document_id.trim();
if document_id.is_empty() {
return Err(
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
.with_context(&context),
);
}
let command = RuntimeCommandEnvelopeWire {
name: "documents.purge".into(),
command_id: format!("document_purge_{}", context.trace.request_id),
idempotency_key: context.source.idempotency_key.clone(),
actor: RuntimeActorWire {
actor_type: context.auth.actor_type.clone(),
actor_id: context.auth.actor_id.clone(),
session_id: context.auth.session_id.clone(),
},
source: RuntimeSourceWire {
channel: context.source.channel.clone(),
client: context.source.client.clone(),
},
target: Some(RuntimeTargetWire {
workspace_id: None,
page_id: Some(document_id.to_string()),
block_id: None,
}),
payload: json!({
"documentId": document_id,
}),
preflight_data: None,
reason: Some("mnote-web documents purge compat".into()),
refs: vec!["mnote-web-documents-compat".into()],
dry_run: false,
validate_only: false,
};
let result =
execute_runtime_command_via_convex(state.config(), &context, None, command).await?;
Ok(ok_response(&context, result))
}
pub async fn title(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
+428 -18
View File
@@ -19,6 +19,10 @@ use std::time::Duration;
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_MNOTE_LEGACY_UPSTREAM: &str = "x-mnote-legacy-upstream";
const COOKIE_RECENT_PAGE_ID: &str = "mnote_recent_page_id";
const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT";
const COOKIE_CONVEX_AUTH_REFRESH_TOKEN: &str = "__convexAuthRefreshToken";
const COOKIE_MNOTE_WEB_CONVEX_TOKEN: &str = "mnote_web_convex_token";
const COOKIE_MNOTE_WEB_DEV_SESSION: &str = "mnote_web_dev_session";
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -55,7 +59,16 @@ pub async fn gateway_health(State(state): State<AppState>) -> Response {
response
}
pub async fn auth_entry(
pub async fn favicon() -> Response {
let mut response = Response::builder()
.status(StatusCode::NO_CONTENT)
.body(Body::empty())
.unwrap_or_else(|_| StatusCode::NO_CONTENT.into_response());
stamp_gateway_headers(response.headers_mut(), false);
response
}
pub async fn auth_api(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
request: Request<Body>,
@@ -64,6 +77,49 @@ pub async fn auth_entry(
return legacy_next_proxy(State(state), Extension(context), request).await;
}
let body = axum::body::to_bytes(request.into_body(), 256 * 1024)
.await
.map_err(|error| WebError::bad_request(format!("读取登录请求失败: {error}")))?;
let payload: serde_json::Value = serde_json::from_slice(&body).map_err(|error| {
WebError::bad_request_code(
"auth_bad_request",
format!("登录请求不是合法 JSON: {error}"),
)
.with_context(&context)
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
})?;
let action = payload
.get("action")
.and_then(|value| value.as_str())
.unwrap_or_default();
if action != "auth:signIn" && action != "auth:signOut" {
return Err(WebError::bad_request_code(
"auth_action_unsupported",
"Rust gateway 当前仅支持 Convex Auth 登录与登出动作。",
)
.with_context(&context)
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
}
let convex_response = run_convex_auth_action(&state, &context, &payload).await?;
Ok(build_auth_proxy_response(&convex_response, &context))
}
pub async fn auth_entry(
State(_state): State<AppState>,
Extension(context): Extension<RequestContext>,
_request: Request<Body>,
) -> Result<Response, WebError> {
if has_real_auth_context(&context) {
let mut response = Response::builder()
.status(StatusCode::SEE_OTHER)
.header(header::LOCATION, "/")
.body(Body::empty())
.map_err(|error| WebError::internal(format!("认证跳转响应构造失败: {error}")))?;
stamp_gateway_headers(response.headers_mut(), false);
return Ok(response);
}
let content = crate::ssr::render_view(crate::ssr::pages::auth::AuthPage());
let mut response = Html(format!(
r#"<!doctype html>
@@ -90,6 +146,16 @@ pub async fn root_entry(
Extension(context): Extension<RequestContext>,
Query(query): Query<RootEntryQuery>,
) -> Result<Response, WebError> {
if !has_real_auth_context(&context) {
let mut response = Response::builder()
.status(StatusCode::SEE_OTHER)
.header(header::LOCATION, "/auth")
.body(Body::empty())
.map_err(|error| WebError::internal(format!("认证入口跳转响应构造失败: {error}")))?;
stamp_gateway_headers(response.headers_mut(), false);
return Ok(response);
}
let workspace_id =
resolve_root_workspace_id(&state, &context, query.workspace_id.as_deref()).await?;
let requested_page_id = normalize_optional_id(query.page_id.as_deref());
@@ -365,6 +431,195 @@ fn normalize_optional_id(value: Option<&str>) -> Option<&str> {
value.map(str::trim).filter(|value| !value.is_empty())
}
fn has_real_auth_context(context: &RequestContext) -> bool {
let actor_id = context.auth.actor_id.trim();
if !actor_id.is_empty() && actor_id != "anonymous" {
return true;
}
extract_cookie_value(context, COOKIE_CONVEX_AUTH_JWT).is_some()
|| extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN).is_some()
}
async fn run_convex_auth_action(
state: &AppState,
context: &RequestContext,
payload: &serde_json::Value,
) -> Result<serde_json::Value, WebError> {
let convex_url = state
.config()
.convex_url
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
WebError::service_unavailable_code(
"convex_config_missing",
"缺少 CONVEX_SELF_HOSTED_URL / NEXT_PUBLIC_CONVEX_URL,无法执行 Convex Auth。",
)
.with_context(context)
.with_header("x-error-phase", "convex_auth_url")
.with_header("x-upstream-service", "convex")
})?;
let action = payload
.get("action")
.and_then(|value| value.as_str())
.unwrap_or_default();
let mut args = payload.get("args").cloned().unwrap_or_else(|| json!({}));
if action == "auth:signIn"
&& args
.get("refreshToken")
.map(|value| !value.is_null())
.unwrap_or(false)
{
if let Some(refresh_token) = extract_cookie_value(context, COOKIE_CONVEX_AUTH_REFRESH_TOKEN)
{
args["refreshToken"] = serde_json::Value::String(refresh_token);
}
}
let request_body = json!({
"path": action,
"format": "convex_encoded_json",
"args": [args],
});
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(20))
.build()
.map_err(|error| {
WebError::internal(format!("Convex Auth HTTP 客户端创建失败: {error}"))
.with_context(context)
.with_header("x-error-phase", "convex_auth_client")
.with_header("x-upstream-service", "convex")
})?;
let mut request = client
.post(format!("{}/api/action", convex_url.trim_end_matches('/')))
.header("Content-Type", "application/json")
.header("Convex-Client", "mnote-web")
.json(&request_body);
if let Some(token) = extract_cookie_value(context, COOKIE_CONVEX_AUTH_JWT) {
request = request.header(header::AUTHORIZATION, format!("Bearer {token}"));
}
let response = request.send().await.map_err(|error| {
WebError::bad_gateway_code(
"convex_auth_proxy_error",
format!("Convex Auth 请求失败: {error}"),
)
.with_context(context)
.with_header("x-error-phase", "convex_auth_action")
.with_header("x-upstream-service", "convex")
})?;
let status = response.status();
let value: serde_json::Value = response.json().await.map_err(|error| {
WebError::bad_gateway_code(
"convex_auth_response_invalid",
format!("Convex Auth 响应不是合法 JSON: {error}"),
)
.with_context(context)
.with_header("x-error-phase", "convex_auth_decode")
.with_header("x-upstream-service", "convex")
})?;
if !status.is_success() && status.as_u16() != 560 {
return Err(WebError::bad_gateway_code(
"convex_auth_upstream_error",
format!("Convex Auth 返回 HTTP {}: {}", status.as_u16(), value),
)
.with_context(context)
.with_header("x-error-phase", "convex_auth_status")
.with_header("x-upstream-service", "convex"));
}
Ok(value)
}
fn build_auth_proxy_response(
convex_response: &serde_json::Value,
context: &RequestContext,
) -> Response {
if convex_response
.get("status")
.and_then(|value| value.as_str())
!= Some("success")
{
let message = convex_response
.get("errorMessage")
.and_then(|value| value.as_str())
.unwrap_or("Convex Auth 登录失败。");
let mut response = axum::Json(json!({ "error": message })).into_response();
*response.status_mut() = StatusCode::BAD_REQUEST;
clear_auth_cookies(response.headers_mut());
stamp_gateway_headers(response.headers_mut(), false);
context.apply_response_headers(response.headers_mut());
return response;
}
let value = convex_response
.get("value")
.cloned()
.unwrap_or_else(|| json!({}));
let tokens = value.get("tokens");
let mut response_body = value.clone();
if let Some(tokens) = tokens {
if tokens.is_null() {
response_body["tokens"] = serde_json::Value::Null;
} else if let Some(token) = tokens.get("token").and_then(|value| value.as_str()) {
response_body["tokens"] = json!({
"token": token,
"refreshToken": "dummy",
});
}
}
let mut response = axum::Json(response_body).into_response();
if let Some(tokens) = tokens {
if tokens.is_null() {
clear_auth_cookies(response.headers_mut());
} else {
set_auth_cookie_from_value(
response.headers_mut(),
COOKIE_CONVEX_AUTH_JWT,
tokens.get("token"),
);
set_auth_cookie_from_value(
response.headers_mut(),
COOKIE_CONVEX_AUTH_REFRESH_TOKEN,
tokens.get("refreshToken"),
);
expire_cookie(response.headers_mut(), COOKIE_MNOTE_WEB_DEV_SESSION);
}
}
stamp_gateway_headers(response.headers_mut(), false);
context.apply_response_headers(response.headers_mut());
response
}
fn set_auth_cookie_from_value(
headers: &mut axum::http::HeaderMap,
name: &'static str,
value: Option<&serde_json::Value>,
) {
let Some(value) = value.and_then(|value| value.as_str()) else {
return;
};
let cookie = format!("{name}={value}; Path=/; HttpOnly; SameSite=Lax");
if let Ok(value) = HeaderValue::from_str(&cookie) {
headers.append(header::SET_COOKIE, value);
}
}
fn clear_auth_cookies(headers: &mut axum::http::HeaderMap) {
expire_cookie(headers, COOKIE_CONVEX_AUTH_JWT);
expire_cookie(headers, COOKIE_CONVEX_AUTH_REFRESH_TOKEN);
expire_cookie(headers, COOKIE_MNOTE_WEB_DEV_SESSION);
}
fn expire_cookie(headers: &mut axum::http::HeaderMap, name: &'static str) {
let cookie = format!("{name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
if let Ok(value) = HeaderValue::from_str(&cookie) {
headers.append(header::SET_COOKIE, value);
}
}
fn choose_root_entry_active_page_id(
requested_page_id: Option<&str>,
recent_page_id: Option<&str>,
@@ -466,6 +721,14 @@ mod tests {
fn app_with_config(
legacy_next_base_url: String,
enable_legacy_next_compat: bool,
) -> axum::Router {
app_with_config_and_convex_url(legacy_next_base_url, enable_legacy_next_compat, None)
}
fn app_with_config_and_convex_url(
legacy_next_base_url: String,
enable_legacy_next_compat: bool,
convex_url: Option<String>,
) -> axum::Router {
build_app(AppState::new(AppConfig {
service_name: "mnote-web".into(),
@@ -477,7 +740,7 @@ mod tests {
enable_debug_shell_routes: false,
hermes_base_path: "/api/hermes".into(),
compat_next_base_path: "/api/compat/next".into(),
convex_url: None,
convex_url,
convex_admin_key: None,
allow_dev_fixtures: true,
query_fixtures_json: None,
@@ -488,6 +751,33 @@ mod tests {
}))
}
async fn spawn_convex_auth_upstream() -> String {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("convex auth listener");
let addr = listener.local_addr().expect("convex auth addr");
let app = axum::Router::new().route(
"/api/action",
post(|| async {
axum::Json(serde_json::json!({
"status": "success",
"value": {
"tokens": {
"token": "jwt-demo",
"refreshToken": "refresh-demo"
}
}
}))
}),
);
tokio::spawn(async move {
axum::serve(listener, app)
.await
.expect("convex auth server");
});
format!("http://{addr}")
}
async fn spawn_legacy_auth_upstream() -> String {
let listener = TcpListener::bind("127.0.0.1:0")
.await
@@ -598,6 +888,8 @@ mod tests {
.oneshot(
Request::builder()
.uri("/")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
@@ -674,6 +966,8 @@ mod tests {
.uri("/")
.header("cookie", "mnote_recent_page_id=page_child")
.header("x-mnote-workspace-id", "ws_demo")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
@@ -691,6 +985,57 @@ mod tests {
assert!(html.contains(r#"data-root-active-page-id="page_child""#));
}
#[tokio::test]
async fn root_entry_redirects_anonymous_viewer_to_auth() {
let response = app_with_config("http://127.0.0.1:3100".into(), false)
.oneshot(
Request::builder()
.uri("/")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::SEE_OTHER);
assert_eq!(
response
.headers()
.get(header::LOCATION)
.and_then(|value| value.to_str().ok()),
Some("/auth")
);
assert_eq!(
response
.headers()
.get("x-mnote-web-owner")
.and_then(|value| value.to_str().ok()),
Some("mnote-web")
);
}
#[tokio::test]
async fn root_entry_allows_forwarded_actor_to_enter_workspace() {
let response = app_with_config("http://127.0.0.1:3100".into(), false)
.oneshot(
Request::builder()
.uri("/")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
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(r#"data-mnote-shell="workspace""#));
}
#[tokio::test]
async fn auth_entry_returns_gateway_fallback_shell_when_compat_disabled() {
let response = app_with_config("http://127.0.0.1:3100".into(), false)
@@ -720,7 +1065,77 @@ mod tests {
}
#[tokio::test]
async fn auth_entry_uses_legacy_next_login_ui_when_compat_enabled() {
async fn favicon_is_handled_by_gateway_when_compat_disabled() {
let response = app_with_config("http://127.0.0.1:3100".into(), false)
.oneshot(
Request::builder()
.uri("/favicon.ico")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::NO_CONTENT);
assert_eq!(
response
.headers()
.get("x-mnote-web-owner")
.and_then(|value| value.to_str().ok()),
Some("mnote-web")
);
}
#[tokio::test]
async fn auth_api_sets_convex_auth_cookies_when_compat_disabled() {
let convex_url = spawn_convex_auth_upstream().await;
let response = app_with_config_and_convex_url(
"http://127.0.0.1:3100".into(),
false,
Some(convex_url),
)
.oneshot(
Request::builder()
.method("POST")
.uri("/api/auth")
.header("content-type", "application/json")
.body(Body::from(
r#"{"action":"auth:signIn","args":{"provider":"password","params":{"email":"mnote.e2e@example.com","password":"MnoteE2E123!","flow":"signIn"}}}"#,
))
.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")
);
let cookies = response.headers().get_all(header::SET_COOKIE);
let values = cookies
.iter()
.map(|value| value.to_str().unwrap_or_default())
.collect::<Vec<_>>();
assert!(values
.iter()
.any(|value| value.contains("__convexAuthJWT=jwt-demo")));
assert!(values
.iter()
.any(|value| value.contains("__convexAuthRefreshToken=refresh-demo")));
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["tokens"]["token"], "jwt-demo");
assert_eq!(payload["tokens"]["refreshToken"], "dummy");
}
#[tokio::test]
async fn auth_entry_uses_mnote_web_login_ui_when_compat_enabled() {
let legacy_base_url = spawn_legacy_auth_upstream().await;
let response = app_with_legacy_next_base_url(legacy_base_url)
.oneshot(
@@ -738,44 +1153,39 @@ mod tests {
.headers()
.get("x-mnote-legacy-upstream")
.and_then(|value| value.to_str().ok()),
Some("next-app-router")
None
);
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(r#"data-mnote-shell="auth""#));
assert!(html.contains("邮箱登录"));
assert!(html.contains("测试账号快速登录"));
}
#[tokio::test]
async fn auth_entry_proxies_post_to_legacy_next_when_compat_enabled() {
let legacy_base_url = spawn_legacy_auth_upstream().await;
let response = app_with_legacy_next_base_url(legacy_base_url)
async fn auth_entry_redirects_authenticated_viewer_to_root() {
let response = app_with_config("http://127.0.0.1:3100".into(), false)
.oneshot(
Request::builder()
.method("POST")
.uri("/auth")
.header("origin", "http://127.0.0.1:3000")
.header("referer", "http://127.0.0.1:3000/auth")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.status(), StatusCode::SEE_OTHER);
assert_eq!(
response
.headers()
.get("x-mnote-legacy-upstream")
.get(header::LOCATION)
.and_then(|value| value.to_str().ok()),
Some("next-app-router")
Some("/")
);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let text = String::from_utf8(body.to_vec()).expect("utf8");
assert_eq!(text, "auth-post-ok");
}
#[tokio::test]
+7
View File
@@ -30,6 +30,7 @@ pub fn build_router(state: AppState) -> Router {
let mut router = Router::new()
.route("/health", get(health::health))
.route("/", get(gateway::root_entry))
.route("/favicon.ico", get(gateway::favicon))
.route("/auth", get(gateway::auth_entry).post(gateway::auth_entry))
.route("/search", get(search::shell))
.route(
@@ -59,10 +60,16 @@ pub fn build_router(state: AppState) -> Router {
.route("/api/search/documents", post(search::documents))
.route("/api/gateway/health", get(gateway::gateway_health))
.route("/api/runtime/config", get(session::runtime_config))
.route("/api/auth", post(gateway::auth_api))
.route("/api/auth/session", get(session::session))
.route("/api/auth/whoami", get(session::session))
.route("/api/auth/mnote-web-token", get(session::session))
.route("/api/auth/session/refresh", post(session::refresh_session))
.route("/api/ai-agent/run", post(compat::next_ai_agent_run))
.route("/api/documents/meta", get(documents::meta))
.route("/api/documents/content", get(documents::content))
.route("/api/documents/page", get(web_shell::documents_page_compat))
.route("/api/documents/purge", post(documents::purge))
.route("/api/documents/title", post(documents::title))
.route("/api/documents/options", post(documents::options))
.route("/api/documents/save", post(documents::save))
+138 -20
View File
@@ -28,6 +28,7 @@ pub struct SessionResponse {
pub email: String,
pub name: String,
pub actor_type: String,
pub auth_mode: &'static str,
pub request_id: String,
pub trace_id: String,
}
@@ -47,36 +48,57 @@ pub async fn session(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
) -> Response {
owner_json(Json(SessionResponse {
ok: true,
owner: "mnote-web",
user_id: state.config().dev_user_id.clone(),
email: state.config().dev_user_email.clone(),
name: state.config().dev_user_name.clone(),
actor_type: context.auth.actor_type,
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
}))
owner_json(Json(build_session_response(&state, context)))
}
pub async fn refresh_session(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
) -> Response {
let mut response = owner_json(Json(SessionResponse {
ok: true,
owner: "mnote-web",
user_id: state.config().dev_user_id.clone(),
email: state.config().dev_user_email.clone(),
name: state.config().dev_user_name.clone(),
actor_type: context.auth.actor_type,
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
}));
let mut response = owner_json(Json(build_session_response(&state, context)));
*response.status_mut() = StatusCode::OK;
response
}
fn build_session_response(state: &AppState, context: RequestContext) -> SessionResponse {
let actor_id = context.auth.actor_id.trim();
let has_forwarded_actor = !actor_id.is_empty() && actor_id != "anonymous";
let user_id = if has_forwarded_actor {
actor_id.to_string()
} else {
state.config().dev_user_id.clone()
};
let actor_type = if has_forwarded_actor {
context.auth.actor_type
} else {
"devFallback".to_string()
};
SessionResponse {
ok: true,
owner: "mnote-web",
user_id,
email: if has_forwarded_actor {
String::new()
} else {
state.config().dev_user_email.clone()
},
name: if has_forwarded_actor {
actor_id.to_string()
} else {
state.config().dev_user_name.clone()
},
actor_type,
auth_mode: if has_forwarded_actor {
"forwardedActor"
} else {
"devFallback"
},
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
}
}
fn owner_json<T>(payload: Json<T>) -> Response
where
T: Serialize,
@@ -172,4 +194,100 @@ mod tests {
assert_eq!(payload["userId"], "dev-user");
assert!(payload.get("convexAdminKey").is_none());
}
#[tokio::test]
async fn session_prefers_forwarded_actor_identity_over_dev_fallback() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/session")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "user_real");
assert_eq!(payload["actorType"], "user");
assert_eq!(payload["authMode"], "forwardedActor");
assert!(payload.get("convexAdminKey").is_none());
}
#[tokio::test]
async fn legacy_whoami_alias_prefers_forwarded_actor_identity() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "user_real");
assert_eq!(payload["actorType"], "user");
assert_eq!(payload["authMode"], "forwardedActor");
assert_ne!(payload["code"], "legacy_next_compat_disabled");
}
#[tokio::test]
async fn legacy_whoami_alias_returns_dev_identity() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/whoami")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "dev-user");
assert_ne!(payload["code"], "legacy_next_compat_disabled");
}
#[tokio::test]
async fn legacy_mnote_web_token_alias_returns_dev_identity() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/auth/mnote-web-token")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["owner"], "mnote-web");
assert_eq!(payload["userId"], "dev-user");
assert_ne!(payload["code"], "legacy_next_compat_disabled");
}
}
+11 -13
View File
@@ -295,11 +295,7 @@ pub(crate) fn collect_filetree_render_rows(
let selected_ids = active_document_id
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|document_id| {
[format!("doc:{document_id}"), format!("index:{document_id}")]
.into_iter()
.collect::<BTreeSet<_>>()
})
.map(|document_id| BTreeSet::from([format!("index:{document_id}")]))
.unwrap_or_default();
projection
@@ -497,10 +493,7 @@ fn build_tree_shell_renderer_input(
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|document_id| {
FileTreeSelectionState::from_selected(&[
format!("doc:{document_id}"),
format!("index:{document_id}"),
])
FileTreeSelectionState::from_selected(&[format!("index:{document_id}")])
})
.unwrap_or_default();
TreeShellRendererInput::filetree(FileTreeRendererInput {
@@ -1512,10 +1505,10 @@ fn build_tree_shell_html(
let selectedFileTreeRowIds = new Set(
rendererSelectedFileTreeRowIds.length > 0
? rendererSelectedFileTreeRowIds
: currentActiveDocumentId ? [`doc:${currentActiveDocumentId}`, `index:${currentActiveDocumentId}`] : []
: currentActiveDocumentId ? [`index:${currentActiveDocumentId}`] : []
);
let fileTreeAnchorRowId = rendererAnchorRowId || (currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null);
let fileTreeFocusedRowId = rendererFocusedRowId || (currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null);
let fileTreeAnchorRowId = rendererAnchorRowId || (currentActiveDocumentId ? `index:${currentActiveDocumentId}` : null);
let fileTreeFocusedRowId = rendererFocusedRowId || (currentActiveDocumentId ? `index:${currentActiveDocumentId}` : null);
let visibleFileTreeRowIds = [];
let draggingPageNodeId = "";
let activePageDropNodeId = null;
@@ -4964,7 +4957,12 @@ mod tests {
assert!(filetree_html.contains("\"rendererInput\""));
assert!(filetree_html.contains("\"mode\":\"fileTree\""));
assert!(filetree_html.contains("\"filetreeSelection\""));
assert!(filetree_html.contains("\"selectedRowIds\""));
assert!(filetree_html.contains("\"selectedRowIds\":[\"index:page_root\"]"));
assert!(!filetree_html.contains("\"selectedRowIds\":[\"doc:page_root\""));
assert!(filetree_html.contains("\"focusedRowId\":\"index:page_root\""));
assert!(filetree_html.contains("data-row-id=\"doc:page_root\""));
assert!(filetree_html.contains("data-row-id=\"index:page_root\""));
assert!(filetree_html.contains("data-row-id=\"index:page_root\" data-row-kind=\"index\""));
assert!(filetree_html.contains("\"commandDispatcher\""));
assert!(filetree_html.contains("\"runtimeArtifact\""));
assert!(filetree_html.contains("\"contractName\":\"rust_tree_shell_runtime_artifact_v1\""));
+189 -17
View File
@@ -41,6 +41,13 @@ pub struct DocumentShellQuery {
pub workspace_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentPageCompatQuery {
pub document_id: String,
pub workspace_id: Option<String>,
}
pub async fn document_page_shell(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
@@ -82,6 +89,8 @@ pub async fn document_page_shell(
let workspace_name = workspace_projection.workspace_name.clone();
let page_subtree_json =
serde_json::to_string(&aggregate.tree.page_subtree).unwrap_or_else(|_| "null".to_string());
let page_options_json = serde_json::to_string(&aggregate.layout.page_options)
.unwrap_or_else(|_| "null".to_string());
let snapshot_json = serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string());
let bootstrap_json = build_editor_bootstrap_json(&aggregate, &context);
let body_content = crate::ssr::render_view(leptos::view! {
@@ -93,6 +102,7 @@ pub async fn document_page_shell(
workspace_name={workspace_name}
workspace_sidebar_html={workspace_sidebar_html}
page_subtree_json={page_subtree_json}
page_options_json={page_options_json}
/>
});
let html = format!(
@@ -198,9 +208,9 @@ pub(crate) fn render_document_title_controller_script() -> &'static str {
}
if (!documentId) return;
const escapedId = cssEscape(documentId);
setText(`.tree-row[data-node-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
setText(`.tree-row[data-document-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
setText(`.tree-row[data-doc-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
const escapedDocRowId = cssEscape(`doc:${documentId}`);
setText(`.tree-row[data-shell-mode="page"][data-node-id="${escapedId}"] > .tree-link > .tree-link-title`, title);
setText(`.tree-row[data-shell-mode="filetree"][data-row-id="${escapedDocRowId}"] > .tree-link > .tree-link-title`, title);
setText(`.wolai-page-row[data-node-id="${escapedId}"] > .wolai-row-title`, title);
setText(`a[href="/documents/${escapedId}"] > .wolai-row-title`, title);
setText(`a[href^="/documents/${escapedId}?"] > .wolai-row-title`, title);
@@ -307,8 +317,13 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return '';
};
const legacyBlockToTiptap = (block) => {
const legacyBlockToTiptap = (block, index = 0) => {
const type = typeof block?.type === 'string' ? block.type : 'paragraph';
const blockId = typeof block?.id === 'string' && block.id.trim()
? block.id.trim()
: typeof block?.blockId === 'string' && block.blockId.trim()
? block.blockId.trim()
: `block-${index + 1}`;
const text = flattenText(block?.content);
const content = text ? [{ type: 'text', text }] : [];
const textAlign = typeof block?.props?.textAlign === 'string'
@@ -318,15 +333,16 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
: undefined;
const withTextAlign = (attrs = {}) => textAlign ? { ...attrs, textAlign } : attrs;
const nestedChildren = Array.isArray(block?.children)
? block.children.map(legacyBlockToTiptap).filter(Boolean)
? block.children.map((child, childIndex) => legacyBlockToTiptap(child, childIndex)).filter(Boolean)
: [];
const withListChildren = (itemType, listType, attrs = {}) => ({
type: listType,
attrs: { blockId, ...attrs },
content: [{
type: itemType,
attrs,
attrs: { blockId },
content: [
{ type: 'paragraph', content },
{ type: 'paragraph', attrs: { blockId }, content },
...nestedChildren,
],
}],
@@ -334,7 +350,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (type === 'heading') {
const level = Number(block?.props?.level || block?.level || 1) || 1;
const collapsed = typeof block?.props?.collapsed === 'boolean' ? { collapsed: block.props.collapsed } : {};
return { type: 'heading', attrs: withTextAlign({ level: Math.max(1, Math.min(6, level)), ...collapsed }), content };
return { type: 'heading', attrs: withTextAlign({ blockId, level: Math.max(1, Math.min(6, level)), ...collapsed }), content };
}
if (type === 'bulletListItem' || type === 'bullet_list_item') {
return withListChildren('listItem', 'bulletList');
@@ -346,13 +362,13 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return withListChildren('taskItem', 'taskList', { checked: Boolean(block?.props?.checked) });
}
if (type === 'quote' || type === 'blockquote') {
return { type: 'blockquote', attrs: withTextAlign(), content: [{ type: 'paragraph', attrs: withTextAlign(), content }] };
return { type: 'blockquote', attrs: withTextAlign({ blockId }), content: [{ type: 'paragraph', attrs: withTextAlign({ blockId }), content }] };
}
if (type === 'codeBlock' || type === 'code_block') {
return { type: 'codeBlock', attrs: withTextAlign({ language: block?.props?.language || null }), content };
return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: block?.props?.language || null }), content };
}
if (type === 'divider' || type === 'horizontalRule' || type === 'horizontal_rule') {
return { type: 'horizontalRule' };
return { type: 'horizontalRule', attrs: { blockId } };
}
if (type === 'table') {
const tableSnapshot = block?.props?.tiptapTable;
@@ -395,9 +411,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
alt: block?.props?.alt || block?.alt || null,
title: block?.props?.title || block?.title || null,
};
return attrs.src ? { type: 'image', attrs } : null;
return attrs.src ? { type: 'image', attrs: { blockId, ...attrs } } : null;
}
return { type: 'paragraph', attrs: withTextAlign(), content };
return { type: 'paragraph', attrs: withTextAlign({ blockId }), content };
};
const textToTiptapDocument = (text) => ({
@@ -436,6 +452,102 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return textToTiptapDocument(fallbackText);
};
const inlineTextNodes = (node) => {
if (!node || typeof node !== 'object') return [];
if (Array.isArray(node.content)) {
return node.content.flatMap((child) => {
if (child?.type === 'text') {
const text = typeof child.text === 'string' ? child.text : '';
return text ? [{ type: 'text', text }] : [];
}
if (child?.type === 'hardBreak') {
return [{ type: 'text', text: '\n' }];
}
return inlineTextNodes(child);
});
}
return [];
};
const firstChild = (node) => Array.isArray(node?.content) ? node.content[0] : null;
const blockIdOf = (node, index) => {
const raw = typeof node?.attrs?.blockId === 'string' ? node.attrs.blockId.trim() : '';
return raw || `block-${index + 1}`;
};
const tiptapNodeToEditorBlock = (node, index) => {
const blockId = blockIdOf(node, index);
if (node?.type === 'paragraph') {
return { blockId, blockType: 'paragraph', props: {}, contentNodes: inlineTextNodes(node), childBlockIds: [] };
}
if (node?.type === 'heading') {
const level = Math.max(1, Math.min(6, Number(node?.attrs?.level || 1) || 1));
return { blockId, blockType: 'heading', props: { headingLevel: level }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
}
if (node?.type === 'bulletList') {
return { blockId, blockType: 'bullet_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] };
}
if (node?.type === 'orderedList') {
return { blockId, blockType: 'numbered_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] };
}
if (node?.type === 'taskList') {
const taskItem = firstChild(node);
return { blockId, blockType: 'todo', props: { checked: Boolean(taskItem?.attrs?.checked) }, contentNodes: inlineTextNodes(firstChild(taskItem)), childBlockIds: [] };
}
if (node?.type === 'blockquote') {
return { blockId, blockType: 'quote', props: {}, contentNodes: inlineTextNodes(firstChild(node)), childBlockIds: [] };
}
if (node?.type === 'codeBlock') {
return { blockId, blockType: 'code_block', props: { language: typeof node?.attrs?.language === 'string' ? node.attrs.language : null }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
}
if (node?.type === 'horizontalRule') {
return { blockId, blockType: 'divider', props: {}, contentNodes: [], childBlockIds: [] };
}
if (node?.type === 'image') {
return { blockId, blockType: 'image', props: { src: node?.attrs?.src || '', alt: node?.attrs?.alt || null, title: node?.attrs?.title || null, tiptapImage: node }, contentNodes: [], childBlockIds: [] };
}
if (node?.type === 'tocNode') {
return { blockId, blockType: 'toc', props: { tiptapTocNode: node }, contentNodes: [], childBlockIds: [] };
}
if (node?.type === 'table') {
return { blockId, blockType: 'table', props: { tiptapTable: node }, contentNodes: inlineTextNodes(node), childBlockIds: [] };
}
return null;
};
const editorDocumentFromTiptapDocument = (tiptapDocument) => {
const content = Array.isArray(tiptapDocument?.content) ? tiptapDocument.content : [];
const blocks = content.map(tiptapNodeToEditorBlock).filter(Boolean);
return {
documentId: bootstrap.documentId,
rootBlockIds: blocks.map((block) => block.blockId),
blocks,
};
};
const legacyBlocksFromEditorDocument = (editorDocument) => (
Array.isArray(editorDocument?.blocks) ? editorDocument.blocks : []
).map((block) => ({
id: block.blockId,
type: block.blockType,
props: block.blockType === 'heading'
? { level: block.props?.headingLevel || 1 }
: block.blockType === 'todo'
? { checked: Boolean(block.props?.checked) }
: block.blockType === 'code_block'
? { language: block.props?.language || null }
: block.blockType === 'image'
? { ...(block.props || {}) }
: block.blockType === 'toc'
? { ...(block.props || {}) }
: block.blockType === 'table'
? { ...(block.props || {}) }
: undefined,
content: Array.isArray(block.contentNodes)
? block.contentNodes.map((node) => typeof node?.text === 'string' ? node.text : '').join('')
: '',
}));
const pageBody = aggregate.body || {};
const permissions = aggregate.head?.permissions || {};
const conflictDetectionKey = typeof pageBody.conflictDetectionKey === 'string'
@@ -502,6 +614,8 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
setStatus('saved');
return;
}
const editorDocument = editorDocumentFromTiptapDocument(tiptapDocument);
const content = legacyBlocksFromEditorDocument(editorDocument);
setStatus('saving');
const response = await fetch(bootstrap.saveEndpoint || '/api/documents/save', {
method: 'POST',
@@ -511,9 +625,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
workspaceId: bootstrap.workspaceId,
revision: editorMeta.revision,
conflictDetectionKey: editorMeta.conflictDetectionKey,
content: [],
editorDocument,
content,
tiptapDocument,
blockCount: null,
blockCount: editorDocument.blocks.length,
}),
});
const result = await response.json().catch(() => null);
@@ -526,6 +641,15 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (typeof saved.conflict_detection_key === 'string') editorMeta.conflictDetectionKey = saved.conflict_detection_key;
if (typeof saved.conflictDetectionKey === 'string') editorMeta.conflictDetectionKey = saved.conflictDetectionKey;
lastSavedSerialized = serialized;
if (typeof window.__mnoteRecordPageHistorySnapshot === 'function') {
window.__mnoteRecordPageHistorySnapshot('save', {
wordCount: content.filter((block) => typeof block?.content === 'string' && block.content.trim()).length,
characterCount: currentEditorText().replace(/\s/g, '').length,
blockCount: editorDocument.blocks.length,
todoTotal: editorDocument.blocks.filter((block) => block.blockType === 'todo').length,
todoDone: editorDocument.blocks.filter((block) => block.blockType === 'todo' && block.props?.checked).length,
});
}
setStatus('saved');
};
@@ -566,6 +690,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const mountId = runtime.mount(root, mountOptions);
root.setAttribute('data-runtime-mount-id', String(mountId));
root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
if (typeof window.__mnoteApplyPageOptionsToShell === 'function') {
window.__mnoteApplyPageOptionsToShell();
}
setStatus('ready');
};
@@ -668,6 +795,47 @@ pub async fn leptos_tiptap_asset(Path(asset_path): Path<String>) -> Result<Respo
Ok(response)
}
pub async fn documents_page_compat(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
Query(query): Query<DocumentPageCompatQuery>,
) -> Result<Response, WebError> {
let document_id = query.document_id.trim().to_string();
if document_id.is_empty() {
return Err(
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
.with_context(&context),
);
}
let aggregate = build_page_aggregate_snapshot(
&state,
&context,
&document_id,
query.workspace_id.as_deref(),
)
.await?;
let projection_owner = aggregate.source_label();
let mut response = (
StatusCode::OK,
Json(json!({
"ok": true,
"owner": "mnote-web",
"schema": "mnote.documents_page_compat.v1",
"page": aggregate,
"requestId": context.trace.request_id,
"traceId": context.trace.trace_id,
})),
)
.into_response();
stamp_shell_headers(response.headers_mut(), "documents-page-compat");
if let Ok(name) = HeaderName::from_lowercase(b"x-mnote-page-aggregate-owner") {
if let Ok(value) = HeaderValue::from_str(projection_owner) {
response.headers_mut().insert(name, value);
}
}
Ok(response)
}
pub async fn page_aggregate(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
@@ -1059,9 +1227,13 @@ mod tests {
assert!(html.contains("data-page-title-input=\"true\""));
assert!(html.contains("data-title-endpoint=\"/api/documents/title\""));
assert!(html.contains("mnote.document_title_controller.v1"));
assert!(html
.contains(".tree-row[data-node-id=\"${escapedId}\"] > .tree-link > .tree-link-title"));
assert!(html.contains(
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(!html.contains("[data-node-id=\"${escapedId}\"] .tree-link-title"));
assert!(html.contains("data-testid=\"wolai-page-settings-trigger\""));
assert!(html.contains("data-mnote-action=\"open-page-settings\""));
assert!(html.contains("data-mnote-action=\"open-page-ai\""));
assert!(html.contains("\"titleEndpoint\":\"/api/documents/title\""));
assert!(html.contains("__MNOTE_TREE_LIVE_BOOTSTRAP__"));
assert!(html.contains("mnote.tree_live_bootstrap.v1"));
+209 -10
View File
@@ -1,21 +1,220 @@
//! MNOTE 登录页面组件
use super::layout::PageLayout;
use leptos::prelude::*;
const TEST_ACCOUNT_EMAIL: &str = "mnote.e2e@example.com";
const TEST_ACCOUNT_PASSWORD: &str = "MnoteE2E123!";
const TEST_ACCOUNT_NAME: &str = "mnote-e2e";
/// MNOTE 登录页面
///
/// 当 legacy compat 关闭时由 `auth_entry` 路由使用。
#[component]
pub fn AuthPage() -> impl IntoView {
view! {
<PageLayout current_nav="home">
<section class="mnote-auth">
<h1>MNOTE </h1>
<p>
"此页面由 mnote-web gateway 提供。"
</p>
<main class="mnote-auth" data-testid="mnote-auth-page">
<a class="mnote-auth-brand" href="/auth" aria-label="MNOTE">
<span class="mnote-auth-brand-mark" aria-hidden="true">"M"</span>
<span>"MNOTE"</span>
</a>
<section class="mnote-auth-panel" aria-labelledby="mnote-auth-title">
<div class="mnote-auth-heading">
<h1 id="mnote-auth-title">"邮箱登录"</h1>
<p>"登录后进入你的工作区"</p>
</div>
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="convex-password">
<input type="hidden" name="action" value="auth:signIn" />
<input type="hidden" name="provider" value="password" />
<input type="hidden" name="flow" value="signIn" data-auth-flow />
<label class="mnote-auth-field">
<span>"邮箱"</span>
<input
id="email"
name="email"
type="email"
autocomplete="email"
placeholder="请输入邮箱"
required
/>
</label>
<label class="mnote-auth-field mnote-auth-username" hidden>
<span>"用户名"</span>
<input
id="username"
name="username"
type="text"
autocomplete="username"
placeholder="用于显示的用户名"
/>
</label>
<label class="mnote-auth-field">
<span>"密码"</span>
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
placeholder="请输入密码"
minlength="8"
required
/>
</label>
<p class="mnote-auth-message" role="status" aria-live="polite" data-auth-message></p>
<button class="mnote-auth-submit" type="submit" data-auth-submit>"登录"</button>
<button
class="mnote-auth-quick-login"
type="button"
data-auth-test-login
data-test-email=TEST_ACCOUNT_EMAIL
data-test-password=TEST_ACCOUNT_PASSWORD
data-test-name=TEST_ACCOUNT_NAME
>
"测试账号快速登录"
</button>
</form>
<button class="mnote-auth-switch" type="button" data-auth-switch data-flow="signIn">
"没有账号?注册"
</button>
</section>
</PageLayout>
<script>{AUTH_SCRIPT}</script>
</main>
}
}
const AUTH_SCRIPT: &str = r#"
(function () {
var root = document.querySelector('[data-testid="mnote-auth-page"]');
if (!root) return;
var form = root.querySelector('form[data-auth-mode="convex-password"]');
var flowInput = root.querySelector('[data-auth-flow]');
var usernameField = root.querySelector('.mnote-auth-username');
var usernameInput = root.querySelector('#username');
var title = root.querySelector('#mnote-auth-title');
var subtitle = root.querySelector('.mnote-auth-heading p');
var submit = root.querySelector('[data-auth-submit]');
var quickLogin = root.querySelector('[data-auth-test-login]');
var switcher = root.querySelector('[data-auth-switch]');
var message = root.querySelector('[data-auth-message]');
if (!form || !flowInput || !submit || !quickLogin || !switcher || !message) return;
function setMessage(text, type) {
message.textContent = text || '';
message.dataset.type = type || '';
}
function setBusy(isBusy) {
submit.disabled = !!isBusy;
quickLogin.disabled = !!isBusy;
switcher.disabled = !!isBusy;
}
function setFlow(nextFlow) {
var isSignUp = nextFlow === 'signUp';
flowInput.value = nextFlow;
switcher.dataset.flow = nextFlow;
if (title) title.textContent = isSignUp ? '' : '';
if (subtitle) subtitle.textContent = isSignUp ? '' : '';
submit.textContent = isSignUp ? '' : '';
switcher.textContent = isSignUp ? '' : '';
if (usernameField) usernameField.hidden = !isSignUp;
if (usernameInput) usernameInput.required = isSignUp;
quickLogin.hidden = isSignUp;
var passwordInput = root.querySelector('#password');
if (passwordInput) passwordInput.autocomplete = isSignUp ? 'new-password' : 'current-password';
setMessage('', '');
}
function buildPayload(flow, email, password, username) {
var payload = {
action: 'auth:signIn',
args: {
provider: 'password',
params: {
email: email,
password: password,
flow: flow
}
}
};
if (flow === 'signUp' && username) {
payload.args.params.name = username;
}
return payload;
}
async function requestAuth(flow, email, password, username) {
var response = await fetch('/api/auth', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'include',
body: JSON.stringify(buildPayload(flow, email, password, username))
});
var data = await response.json().catch(function () { return {}; });
if (!response.ok || data.error) {
throw new Error(data.error || '');
}
return data;
}
switcher.addEventListener('click', function () {
setFlow(flowInput.value === 'signIn' ? 'signUp' : 'signIn');
});
form.addEventListener('submit', async function (event) {
event.preventDefault();
var email = String((root.querySelector('#email') || {}).value || '').trim();
var password = String((root.querySelector('#password') || {}).value || '');
var username = String((usernameInput || {}).value || '').trim();
var flow = flowInput.value === 'signUp' ? 'signUp' : 'signIn';
if (!email || !password || (flow === 'signUp' && !username)) {
setMessage('', 'error');
return;
}
setBusy(true);
setMessage(flow === 'signUp' ? '...' : '...', 'info');
try {
await requestAuth(flow, email, password, username);
setMessage('...', 'success');
window.location.assign('/');
} catch (error) {
setMessage(error && error.message ? error.message : '', 'error');
setBusy(false);
}
});
quickLogin.addEventListener('click', async function () {
var email = String(quickLogin.dataset.testEmail || '').trim();
var password = String(quickLogin.dataset.testPassword || '');
var username = String(quickLogin.dataset.testName || '').trim();
if (!email || !password || !username) {
setMessage('', 'error');
return;
}
var emailInput = root.querySelector('#email');
var passwordInput = root.querySelector('#password');
if (emailInput) emailInput.value = email;
if (passwordInput) passwordInput.value = password;
setBusy(true);
setMessage('...', 'info');
try {
await requestAuth('signIn', email, password, username);
setMessage('...', 'success');
window.location.assign('/');
return;
} catch (_signInError) {
setMessage('...', 'info');
}
try {
await requestAuth('signUp', email, password, username);
setMessage('...', 'success');
window.location.assign('/');
} catch (error) {
setMessage(error && error.message ? error.message : '', 'error');
setBusy(false);
}
});
setFlow('signIn');
})();
"#;
@@ -5,6 +5,7 @@
use crate::ssr::pages::layout::PageLayout;
use leptos::prelude::*;
use serde_json::Value;
/// MNOTE 文档页面
///
@@ -33,6 +34,9 @@ pub fn DocumentPage(
/// Page Aggregate 子树 JSON(可选)
#[prop(optional)]
page_subtree_json: Option<String>,
/// Page Aggregate 页面选项 JSON(可选)
#[prop(optional)]
page_options_json: Option<String>,
) -> impl IntoView {
let has_page_subtree = page_subtree_json
.as_deref()
@@ -44,9 +48,45 @@ pub fn DocumentPage(
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "个人空间".to_string());
let page_options = page_options_json
.as_deref()
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.unwrap_or(Value::Null);
let page_wide_layout = page_options
.get("wideLayout")
.and_then(Value::as_bool)
.unwrap_or(false);
let page_small_text = page_options
.get("smallText")
.and_then(Value::as_bool)
.unwrap_or(false);
let page_layout_density = page_options
.get("layoutDensity")
.and_then(Value::as_str)
.unwrap_or("normal")
.to_string();
let page_font = page_options
.get("pageFont")
.and_then(Value::as_str)
.unwrap_or("default")
.to_string();
let page_show_heading_numbers = page_options
.get("showHeadingNumbers")
.and_then(Value::as_bool)
.unwrap_or(true);
view! {
<PageLayout current_nav="documents" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={title.clone()}>
<main class="document-shell" data-editor-host="leptos_tiptap_island" data-document-id={document_id.clone()} data-workspace-id={workspace_id.clone()}>
<main
class="document-shell"
data-editor-host="leptos_tiptap_island"
data-document-id={document_id.clone()}
data-workspace-id={workspace_id.clone()}
data-page-wide-layout={page_wide_layout.to_string()}
data-page-small-text={page_small_text.to_string()}
data-layout-density={page_layout_density.clone()}
data-page-font={page_font.clone()}
data-page-show-heading-numbers={page_show_heading_numbers.to_string()}
>
<header class="document-shell-header">
<div class="document-page-icon" aria-hidden="true">
<span class="material-symbols-outlined material-symbols-filled mnote-material-page-icon" data-icon="home"></span>
+859 -12
View File
@@ -17,6 +17,15 @@ const SIDEBAR_TREE_JS: &str = r##"
var activeFileTreeDropRow = null;
var projectionRefreshTimer = 0;
var activeTreeContextMenu = null;
var pageUiState = {
pageOptions: null,
historySnapshots: [],
pageSettingsOpen: false,
pageAiOpen: false,
pageAiBusy: false,
pageAiMessages: [],
pageAiSuggestionIndex: 0
};
function closestAction(target, selector) {
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
@@ -36,11 +45,178 @@ const SIDEBAR_TREE_JS: &str = r##"
return String(value).replace(/["\\]/g, '\\$&');
}
function parseJsonScript(id) {
var node = document.getElementById(id);
if (!node) return null;
try {
return JSON.parse(node.textContent || 'null');
} catch (_) {
return null;
}
}
function currentDocumentId() {
var match = window.location.pathname.match(/^\/documents\/([^\/]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
function currentPageAggregate() {
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
}
function defaultPageOptions() {
return {
wideLayout: false,
smallText: false,
layoutDensity: 'normal',
showHeadingNumbers: true,
showToc: false,
showStructure: false,
protectEditing: false,
showWordCount: true,
collapseBacklinks: false,
pageFont: 'default',
hideChildPages: false,
showBlockRefCount: false,
embedDefaultBlockId: null
};
}
function currentPageOptions() {
if (pageUiState.pageOptions) return pageUiState.pageOptions;
var aggregate = currentPageAggregate();
var current = aggregate && aggregate.layout && aggregate.layout.pageOptions && typeof aggregate.layout.pageOptions === 'object'
? aggregate.layout.pageOptions
: null;
if (!current) {
return defaultPageOptions();
}
pageUiState.pageOptions = Object.assign(defaultPageOptions(), current);
return pageUiState.pageOptions;
}
function pageOptionIsSupported(name) {
return name === 'wideLayout'
|| name === 'smallText'
|| name === 'layoutDensity'
|| name === 'pageFont'
|| name === 'showHeadingNumbers';
}
function pageOptionDescription(name) {
if (name === 'wideLayout') return '';
if (name === 'smallText') return '';
if (name === 'showToc') return '';
if (name === 'showHeadingNumbers') return '';
if (name === 'protectEditing') return '';
if (name === 'collapseBacklinks') return '';
if (name === 'hideChildPages') return '';
if (name === 'showBlockRefCount') return '';
return name;
}
function pageOptionHint(name) {
if (name === 'wideLayout') return '';
if (name === 'smallText') return '';
if (name === 'showHeadingNumbers') return '';
if (name === 'layoutDensity') return '';
if (name === 'pageFont') return '';
if (name === 'showToc') return '线 Rust ';
if (name === 'protectEditing') return '线';
if (name === 'collapseBacklinks') return '线 Rust ';
if (name === 'hideChildPages') return '线';
if (name === 'showBlockRefCount') return '线';
return '线';
}
function ensureHistorySnapshotsSeeded() {
if (pageUiState.historySnapshots.length > 0) return;
var aggregate = currentPageAggregate();
var stats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
pageUiState.historySnapshots = [{
id: 'snapshot-initial',
timestamp: Date.now(),
stats: {
wordCount: Number(stats.wordCount || 0),
characterCount: Number(stats.characterCount || 0),
blockCount: Number(stats.blockCount || 0),
todoTotal: Number(stats.todoTotal || 0),
todoDone: Number(stats.todoDone || 0)
}
}];
}
function computeLivePageStats() {
var aggregate = currentPageAggregate();
var fallbackStats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) {
return {
wordCount: Number(fallbackStats.wordCount || 0),
characterCount: Number(fallbackStats.characterCount || 0),
blockCount: Number(fallbackStats.blockCount || 0),
todoTotal: Number(fallbackStats.todoTotal || 0),
todoDone: Number(fallbackStats.todoDone || 0)
};
}
var text = (editor.textContent || '').trim();
var compact = text.replace(/\s+/g, ' ').trim();
var wordCount = compact ? compact.split(' ').filter(Boolean).length : 0;
var characterCount = text.replace(/\s/g, '').length;
var blockCount = editor.querySelectorAll(':scope > *').length;
var todos = Array.from(editor.querySelectorAll('input[type="checkbox"]'));
return {
wordCount: wordCount || Number(fallbackStats.wordCount || 0),
characterCount: characterCount || Number(fallbackStats.characterCount || 0),
blockCount: blockCount || Number(fallbackStats.blockCount || 0),
todoTotal: todos.length || Number(fallbackStats.todoTotal || 0),
todoDone: todos.filter(function(node){ return node.checked; }).length || Number(fallbackStats.todoDone || 0)
};
}
function recordPageHistorySnapshot(reason, stats) {
ensureHistorySnapshotsSeeded();
pageUiState.historySnapshots = [{
id: 'snapshot-' + Date.now(),
timestamp: Date.now(),
reason: reason || 'save',
stats: stats || computeLivePageStats()
}].concat(pageUiState.historySnapshots).slice(0, 15);
renderPageHistoryDrawer();
}
function applyPageOptionsToShell() {
var options = currentPageOptions();
var shell = document.querySelector('.document-shell');
var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
if (shell instanceof HTMLElement) {
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
shell.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
shell.style.width = '100%';
shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
}
if (editorRoot instanceof HTMLElement) {
editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
editorRoot.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
editorRoot.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
editorRoot.setAttribute('data-page-embed-default-block-id', options.embedDefaultBlockId == null ? '' : String(options.embedDefaultBlockId));
}
if (editorSurface instanceof HTMLElement) {
editorSurface.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
editorSurface.setAttribute('data-page-font', String(options.pageFont || 'default'));
editorSurface.setAttribute('data-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
}
document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
document.documentElement.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
}
function normalizeSidebarTreeMode(value) {
var mode = String(value || '').trim();
return mode === 'filetree' ? 'filetree' : 'page';
@@ -131,8 +307,11 @@ const SIDEBAR_TREE_JS: &str = r##"
});
document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) {
if (row instanceof HTMLElement) {
if (row.getAttribute('data-shell-mode') === 'filetree') row.setAttribute('data-selected', 'true');
else row.setAttribute('data-active', 'true');
if (row.getAttribute('data-shell-mode') === 'filetree') {
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'index:' + nodeId));
} else {
row.setAttribute('data-active', 'true');
}
}
});
return;
@@ -195,10 +374,10 @@ const SIDEBAR_TREE_JS: &str = r##"
function updateTitleEverywhere(documentId, title) {
if (!documentId) return;
var escaped = cssEscape(documentId);
var escapedDocRowId = cssEscape('doc:' + documentId);
var selectors = [
'.tree-row[data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
'.tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title',
'.tree-row[data-doc-id="' + escaped + '"] > .tree-link > .tree-link-title',
'.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
'.tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title',
'.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
'a[href="/documents/' + escaped + '"] > .wolai-row-title',
'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
@@ -322,7 +501,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var children = grouped.get(nodeId) || [];
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
var expanded = expandable && item.expandedByDefault !== false;
var selected = rowId === 'doc:' + activeId || rowId === 'index:' + activeId || documentId === activeId;
var selected = rowId === 'index:' + activeId;
var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
var toggle = expandable
? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '') + '</button>'
@@ -846,10 +1025,624 @@ const SIDEBAR_TREE_JS: &str = r##"
else openSearchModal();
}
function updatePageSettingsTriggerState() {
var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
}
function updatePageAiTriggerState() {
var trigger = document.querySelector('[data-testid="wolai-floating-ai"]');
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-state', pageUiState.pageAiOpen ? 'open' : 'closed');
trigger.setAttribute('aria-expanded', pageUiState.pageAiOpen ? 'true' : 'false');
}
function createPageOptionRow(key, type) {
var inputType = type || 'checkbox';
var supported = pageOptionIsSupported(key);
if (inputType === 'checkbox') {
return '' +
'<label class="wolai-page-setting-row' + (supported ? '' : ' is-pending') + '" data-page-setting-row="' + key + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
'</span>' +
'<input type="checkbox" class="wolai-page-setting-checkbox" data-page-option-checkbox="' + key + '"' + (supported ? '' : ' data-setting-pending="true"') + ' />' +
'</label>';
}
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="' + key + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-option-select="' + key + '">' +
'<option value="compact"></option>' +
'<option value="normal"></option>' +
'<option value="spacious"></option>' +
'</select>' +
'</label>';
}
function createPageFontRow() {
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="pageFont">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label"></span>' +
'<span class="wolai-page-setting-hint"></span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-option-select="pageFont">' +
'<option value="default"></option>' +
'<option value="song"></option>' +
'<option value="kai"></option>' +
'</select>' +
'</label>';
}
function ensurePageHistoryDrawer() {
var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
if (existing instanceof HTMLElement) return existing;
var drawer = document.createElement('aside');
drawer.className = 'wolai-page-history-drawer';
drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
drawer.setAttribute('data-mnote-surface', 'page-history');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-history-panel">' +
'<div class="wolai-page-history-header">' +
'<div><div class="wolai-page-history-title"></div><div class="wolai-page-history-subtitle"> 15 </div></div>' +
'<button type="button" class="wolai-surface-close" data-page-history-action="close" aria-label="关闭页面历史">×</button>' +
'</div>' +
'<div class="wolai-page-history-list" data-page-history-list></div>' +
'</div>';
drawer.addEventListener('click', function(event) {
if (event.target === drawer) closePageHistoryDrawer();
});
document.body.appendChild(drawer);
return drawer;
}
function renderPageHistoryDrawer() {
var drawer = ensurePageHistoryDrawer();
var list = drawer.querySelector('[data-page-history-list]');
if (!(list instanceof HTMLElement)) return;
ensureHistorySnapshotsSeeded();
list.innerHTML = pageUiState.historySnapshots.length
? pageUiState.historySnapshots.map(function(snapshot) {
var stats = snapshot.stats || {};
var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return '' +
'<div class="wolai-page-history-item">' +
'<div class="wolai-page-history-item-copy">' +
'<div class="wolai-page-history-item-title">' + escapeHtml(label) + '</div>' +
'<div class="wolai-page-history-item-meta"> ' + Number(stats.wordCount || 0) + ' · ' + Number(stats.characterCount || 0) + ' · ' + Number(stats.blockCount || 0) + '</div>' +
'</div>' +
'<button type="button" class="wolai-page-history-item-ghost" data-page-history-action="noop"></button>' +
'</div>';
}).join('')
: '<div class="wolai-page-history-empty"></div>';
}
function openPageHistoryDrawer() {
renderPageHistoryDrawer();
var drawer = ensurePageHistoryDrawer();
drawer.hidden = false;
document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
}
function closePageHistoryDrawer() {
var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
if (drawer instanceof HTMLElement) drawer.hidden = true;
document.documentElement.removeAttribute('data-mnote-page-history-open');
}
function ensurePageShareDialog() {
var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
if (existing instanceof HTMLElement) return existing;
var dialog = document.createElement('div');
dialog.className = 'wolai-page-share-dialog';
dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
dialog.setAttribute('data-mnote-surface', 'page-share');
dialog.hidden = true;
dialog.innerHTML = '' +
'<div class="wolai-page-share-card" role="dialog" aria-modal="true">' +
'<div class="wolai-page-share-header">' +
'<div><div class="wolai-page-share-title"></div><div class="wolai-page-share-subtitle"> 3000 mnote-web </div></div>' +
'<button type="button" class="wolai-surface-close" data-page-share-action="close" aria-label="关闭公开分享页面">×</button>' +
'</div>' +
'<div class="wolai-page-share-state">' +
'<span class="wolai-public-pill wolai-public-pill--inline"></span>' +
'<span class="wolai-page-share-copy">访</span>' +
'</div>' +
'<div class="wolai-page-share-url-row">' +
'<input class="wolai-page-share-url" type="text" readonly data-page-share-url value="" />' +
'<button type="button" class="wolai-page-share-copy-button" data-page-share-action="copy-link"></button>' +
'</div>' +
'<div class="wolai-page-share-footer">线</div>' +
'</div>';
dialog.addEventListener('click', function(event) {
if (event.target === dialog) closePageShareDialog();
});
document.body.appendChild(dialog);
return dialog;
}
function openPageShareDialog() {
var dialog = ensurePageShareDialog();
var input = dialog.querySelector('[data-page-share-url]');
if (input instanceof HTMLInputElement) input.value = window.location.href;
dialog.hidden = false;
document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
}
function closePageShareDialog() {
var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
document.documentElement.removeAttribute('data-mnote-page-share-open');
}
function pageAiSuggestions() {
var title = searchText(document.querySelector('[data-page-title-current="true"]')?.textContent) || '';
return [
'' + title + '',
'',
'',
''
];
}
function ensurePageAiDrawer() {
var existing = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (existing instanceof HTMLElement) return existing;
var drawer = document.createElement('aside');
drawer.className = 'wolai-page-ai-drawer';
drawer.setAttribute('data-testid', 'wolai-page-ai-drawer');
drawer.setAttribute('data-mnote-surface', 'page-ai');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-header">' +
'<div class="wolai-page-ai-header-copy">' +
'<h2 class="wolai-page-ai-title" data-title="page-ai-title"></h2>' +
'<div class="wolai-page-ai-subtitle"> · mnote-cli</div>' +
'</div>' +
'<button type="button" class="wolai-surface-close" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
'</div>' +
'<div class="wolai-page-ai-body">' +
'<div class="wolai-page-ai-suggestions">' +
'<div class="wolai-page-ai-suggestions-header">' +
'<span></span>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate"></button>' +
'</div>' +
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
'</div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'</div>' +
'<div class="wolai-page-ai-footer">' +
'<div class="wolai-page-ai-toolbar">' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-action="new-session" aria-label="当前已是新会话"></button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-action="history"></button>' +
'<button type="button" class="wolai-page-ai-model-chip" data-page-ai-action="model">mnote-cli</button>' +
'</div>' +
'<div class="wolai-page-ai-input-row">' +
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问我你想知道的"></textarea>' +
'<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送"></button>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(drawer);
return drawer;
}
function renderPageAiSuggestions() {
var drawer = ensurePageAiDrawer();
var list = drawer.querySelector('[data-page-ai-suggestion-list]');
if (!(list instanceof HTMLElement)) return;
var suggestions = pageAiSuggestions();
var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
var ordered = suggestions.slice(offset).concat(suggestions.slice(0, offset)).slice(0, 3);
list.innerHTML = ordered.map(function(text) {
return '<button type="button" class="wolai-page-ai-suggestion" data-page-ai-suggestion="' + escapeHtml(text) + '">' + escapeHtml(text) + '</button>';
}).join('');
}
function renderPageAiConversation() {
var drawer = ensurePageAiDrawer();
var conversation = drawer.querySelector('[data-page-ai-conversation]');
if (!(conversation instanceof HTMLElement)) return;
if (!pageUiState.pageAiMessages.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">使</div>';
return;
}
conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '">' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(item.role === 'user' ? '你' : 'AI') + '</div>' +
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
'</div>';
}).join('');
conversation.scrollTop = conversation.scrollHeight;
}
function openPageAiDrawer() {
renderPageAiSuggestions();
renderPageAiConversation();
var drawer = ensurePageAiDrawer();
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
}
function closePageAiDrawer() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (drawer instanceof HTMLElement) drawer.hidden = true;
pageUiState.pageAiOpen = false;
updatePageAiTriggerState();
}
function isPageAiDrawerOpen() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
return drawer instanceof HTMLElement && !drawer.hidden;
}
async function streamPageAiResponse(response, onEvent) {
if (!response.body || typeof response.body.getReader !== 'function') return;
var reader = response.body.getReader();
var decoder = new TextDecoder();
var buffer = '';
while (true) {
var chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
var frames = buffer.split('\n\n');
buffer = frames.pop() || '';
frames.forEach(function(frame) {
var eventName = '';
var dataLines = [];
frame.split('\n').forEach(function(line) {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
});
if (eventName) onEvent(eventName, dataLines.join('\n'));
});
}
}
async function sendPageAiMessage(text) {
if (pageUiState.pageAiBusy) return;
var prompt = searchText(text);
if (!prompt) return;
var aggregate = currentPageAggregate();
var body = aggregate.body || {};
var subtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
var outline = subtree && subtree.outline ? subtree.outline : null;
pageUiState.pageAiBusy = true;
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
renderPageAiConversation();
try {
var response = await fetch('/api/ai-agent/run', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
stream: true,
maxSteps: 8,
scope: 'document',
messages: [{ role: 'user', content: prompt }],
toolChoice: {
mode: 'auto',
toolSets: ['toolset.readonly', 'toolset.rag_read', 'toolset.docs_read', 'toolset.media_read', 'toolset.doc_read', 'toolset.doc_write', 'toolset.slash_write']
},
context: {
documentId: currentDocumentId(),
documentBlocks: body.content || null,
node: {
documentId: currentDocumentId(),
title: aggregate.head && aggregate.head.title ? aggregate.head.title : ''
},
subtree: subtree,
outline: outline,
evidence: null,
pageOptions: currentPageOptions()
},
options: {
searxng: true,
ai: { provider: 'hermes' }
}
})
});
if (!response.ok) {
throw new Error('page_ai_failed_' + response.status);
}
var assistantText = '';
await streamPageAiResponse(response, function(eventName, payloadText) {
if (eventName === 'assistant_message') {
try {
var payload = JSON.parse(payloadText || 'null');
assistantText = searchText(payload && payload.text);
} catch (_) {}
}
});
pageUiState.pageAiMessages.push({
role: 'assistant',
content: assistantText || ' AI `mnote-cli`'
});
} catch (error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: ' AI `mnote-cli` ' + (error instanceof Error ? error.message : String(error))
});
} finally {
pageUiState.pageAiBusy = false;
renderPageAiConversation();
}
}
function ensurePageSettingsPopover() {
var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
if (existing instanceof HTMLElement) return existing;
var popover = document.createElement('div');
popover.className = 'wolai-page-settings-popover';
popover.setAttribute('data-testid', 'wolai-page-settings-popover');
popover.setAttribute('data-mnote-surface', 'page-settings');
popover.hidden = true;
popover.innerHTML = '' +
'<div class="wolai-page-settings-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-settings-tabs" data-testid="wolai-page-settings-tabs" role="tablist" aria-label="页面设置分组">' +
'<button type="button" class="wolai-page-settings-tab is-active" role="tab" aria-selected="true" data-page-settings-tab="page"></button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="custom"></button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="global"></button>' +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="page">' +
createPageOptionRow('wideLayout', 'checkbox') +
createPageOptionRow('smallText', 'checkbox') +
createPageOptionRow('showToc', 'checkbox') +
createPageOptionRow('showHeadingNumbers', 'checkbox') +
createPageOptionRow('protectEditing', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
createPageFontRow() +
createPageOptionRow('layoutDensity', 'select') +
createPageOptionRow('collapseBacklinks', 'checkbox') +
createPageOptionRow('hideChildPages', 'checkbox') +
createPageOptionRow('showBlockRefCount', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
'<div class="wolai-page-settings-global-note"> Rust Web </div>' +
'</div>' +
'<div class="wolai-page-settings-actions">' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">...</button>' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="share">...</button>' +
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="move" disabled>...</button>' +
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="embed" disabled>...</button>' +
'<button type="button" class="wolai-page-settings-action is-danger is-disabled" data-page-settings-action="delete" disabled></button>' +
'</div>' +
'<div class="wolai-page-settings-stats" data-testid="wolai-page-settings-stats"></div>' +
'</div>';
document.body.appendChild(popover);
return popover;
}
function renderPageSettingsPopover() {
var popover = ensurePageSettingsPopover();
var options = currentPageOptions();
popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
var key = input.getAttribute('data-page-option-checkbox');
input.checked = Boolean(options[key]);
input.disabled = !pageOptionIsSupported(key);
});
popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
var key = select.getAttribute('data-page-option-select');
var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
select.value = value;
});
var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
if (statsNode instanceof HTMLElement) {
var stats = computeLivePageStats();
statsNode.innerHTML = '' +
'<span> ' + Number(stats.wordCount || 0) + '</span>' +
'<span> ' + Number(stats.characterCount || 0) + '</span>' +
'<span> ' + Number(stats.blockCount || 0) + '</span>' +
'<span> ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '</span>';
}
}
function setActivePageSettingsTab(tabName) {
var popover = ensurePageSettingsPopover();
popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
var active = tab.getAttribute('data-page-settings-tab') === tabName;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
});
}
async function persistPageOptionsPatch(patch) {
var previous = Object.assign({}, currentPageOptions());
pageUiState.pageOptions = Object.assign({}, previous, patch);
var nextOptions = Object.assign({}, pageUiState.pageOptions);
applyPageOptionsToShell();
renderPageSettingsPopover();
try {
var response = await fetch('/api/documents/options', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
options: nextOptions,
commandName: 'page.layout.updateOptions'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
}
document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
} catch (error) {
pageUiState.pageOptions = previous;
applyPageOptionsToShell();
renderPageSettingsPopover();
document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
}
}
function isPageSettingsOpen() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
return popover instanceof HTMLElement && !popover.hidden;
}
function openPageSettingsPopover() {
if (!currentDocumentId()) return;
var popover = ensurePageSettingsPopover();
renderPageSettingsPopover();
setActivePageSettingsTab('page');
popover.hidden = false;
pageUiState.pageSettingsOpen = true;
updatePageSettingsTriggerState();
}
function closePageSettingsPopover() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
if (popover instanceof HTMLElement) popover.hidden = true;
pageUiState.pageSettingsOpen = false;
updatePageSettingsTriggerState();
}
function togglePageSettingsPopover() {
if (isPageSettingsOpen()) closePageSettingsPopover();
else openPageSettingsPopover();
}
document.addEventListener('click', function(e) {
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
if (activeTreeContextMenu) closeTreeContextMenu();
var historyClose = closestAction(e.target, '[data-page-history-action="close"]');
if (historyClose) {
e.preventDefault();
closePageHistoryDrawer();
return;
}
var shareClose = closestAction(e.target, '[data-page-share-action="close"]');
if (shareClose) {
e.preventDefault();
closePageShareDialog();
return;
}
var shareCopy = closestAction(e.target, '[data-page-share-action="copy-link"]');
if (shareCopy) {
e.preventDefault();
void copyTreeContextValue(window.location.href, 'page-share-link');
return;
}
var pageHistoryTrigger = closestAction(e.target, '[data-mnote-action="open-page-history"]');
if (pageHistoryTrigger) {
e.preventDefault();
openPageHistoryDrawer();
return;
}
var pageSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-page-settings"]');
if (pageSettingsTrigger) {
e.preventDefault();
togglePageSettingsPopover();
return;
}
var pageSettingsPanel = closestAction(e.target, '.wolai-page-settings-panel');
if (isPageSettingsOpen() && !pageSettingsPanel) {
closePageSettingsPopover();
}
var pageSettingsTab = closestAction(e.target, '[data-page-settings-tab]');
if (pageSettingsTab) {
e.preventDefault();
setActivePageSettingsTab(pageSettingsTab.getAttribute('data-page-settings-tab') || 'page');
return;
}
var pageSettingsAction = closestAction(e.target, '[data-page-settings-action]');
if (pageSettingsAction) {
e.preventDefault();
var actionName = pageSettingsAction.getAttribute('data-page-settings-action') || '';
if (actionName === 'history') {
openPageHistoryDrawer();
return;
}
if (actionName === 'share') {
openPageShareDialog();
return;
}
return;
}
var pageAiTrigger = closestAction(e.target, '[data-mnote-action="open-page-ai"]');
if (pageAiTrigger) {
e.preventDefault();
openPageAiDrawer();
return;
}
var pageAiClose = closestAction(e.target, '[data-page-ai-action="close"]');
if (pageAiClose) {
e.preventDefault();
closePageAiDrawer();
return;
}
var pageAiRotate = closestAction(e.target, '[data-page-ai-action="rotate"]');
if (pageAiRotate) {
e.preventDefault();
pageUiState.pageAiSuggestionIndex += 1;
renderPageAiSuggestions();
return;
}
var pageAiSuggestion = closestAction(e.target, '[data-page-ai-suggestion]');
if (pageAiSuggestion) {
e.preventDefault();
var text = pageAiSuggestion.getAttribute('data-page-ai-suggestion') || '';
var inputNode = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
if (inputNode instanceof HTMLTextAreaElement) {
inputNode.value = text;
inputNode.focus();
}
return;
}
var pageAiNewSession = closestAction(e.target, '[data-page-ai-action="new-session"]');
if (pageAiNewSession) {
e.preventDefault();
pageUiState.pageAiMessages = [];
renderPageAiConversation();
return;
}
var pageAiSend = closestAction(e.target, '[data-page-ai-action="send"]');
if (pageAiSend) {
e.preventDefault();
var input = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
if (input instanceof HTMLTextAreaElement) {
var message = input.value;
input.value = '';
void sendPageAiMessage(message);
}
return;
}
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
if (searchTrigger) {
e.preventDefault();
@@ -973,6 +1766,11 @@ const SIDEBAR_TREE_JS: &str = r##"
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && isPageSettingsOpen()) {
event.preventDefault();
closePageSettingsPopover();
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
event.preventDefault();
toggleSearchModal();
@@ -982,8 +1780,51 @@ const SIDEBAR_TREE_JS: &str = r##"
closeSearchModal();
closeTreeContextMenu();
}
if (event.key === 'Enter' && !event.shiftKey) {
var aiInput = closestAction(event.target, '[data-page-ai-input]');
if (aiInput instanceof HTMLTextAreaElement) {
event.preventDefault();
var text = aiInput.value;
aiInput.value = '';
void sendPageAiMessage(text);
}
}
});
document.addEventListener('change', function(event) {
var checkbox = closestAction(event.target, '[data-page-option-checkbox]');
if (checkbox instanceof HTMLInputElement) {
var key = checkbox.getAttribute('data-page-option-checkbox') || '';
if (!pageOptionIsSupported(key)) return;
var patch = {};
patch[key] = checkbox.checked;
void persistPageOptionsPatch(patch);
return;
}
var select = closestAction(event.target, '[data-page-option-select]');
if (select instanceof HTMLSelectElement) {
var selectKey = select.getAttribute('data-page-option-select') || '';
var next = {};
if (selectKey === 'layoutDensity') next.layoutDensity = select.value;
if (selectKey === 'pageFont') next.pageFont = select.value;
if (Object.keys(next).length) {
void persistPageOptionsPatch(next);
}
}
});
function initializePageUiSurfaces() {
pageUiState.pageOptions = null;
applyPageOptionsToShell();
updatePageSettingsTriggerState();
updatePageAiTriggerState();
ensureHistorySnapshotsSeeded();
}
window.__mnoteRecordPageHistorySnapshot = recordPageHistorySnapshot;
window.__mnoteApplyPageOptionsToShell = applyPageOptionsToShell;
setTimeout(initializePageUiSurfaces, 0);
function readPageDragNodeId(event) {
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
return (fromTransfer || draggingPageNodeId || '').trim();
@@ -1403,8 +2244,8 @@ pub fn PageLayout(
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="历史" aria-label="历史"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="更多" aria-label="更多"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="页面历史" aria-label="历史" data-mnote-action="open-page-history"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="页面选项和全局选项" aria-label="更多" data-testid="wolai-page-settings-trigger" data-mnote-action="open-page-settings"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
</div>
</header>
<article class="mnote-content">
@@ -1412,7 +2253,7 @@ pub fn PageLayout(
</article>
<div class="wolai-floating-actions" aria-label="浮动操作">
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助"><span class="material-symbols-outlined" data-icon="help" aria-hidden="true"></span></button>
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
</div>
</div>
</div>
@@ -1446,13 +2287,19 @@ mod tests {
fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
assert!(SIDEBAR_TREE_JS.contains("openTreeContextMenu"));
assert!(SIDEBAR_TREE_JS.contains("mnote-tree-context-menu"));
assert!(SIDEBAR_TREE_JS.contains("复制访问链接(带标题)"));
assert!(SIDEBAR_TREE_JS.contains("复制访问链接"));
assert!(SIDEBAR_TREE_JS.contains("删除到垃圾桶"));
assert!(SIDEBAR_TREE_JS.contains(
".tree-row[data-node-id=\"' + escaped + '\"] > .tree-link > .tree-link-title"
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_JS.contains(
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
));
assert!(!SIDEBAR_TREE_JS.contains(
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(!SIDEBAR_TREE_JS
.contains("[data-node-id=\"' + cssEscape(documentId) + '\"] .tree-link-title"));
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
}
#[test]
+700 -18
View File
@@ -877,32 +877,190 @@ a:hover {
/* ===== 认证页面 ===== */
.mnote-auth {
display: flex;
height: 100vh;
position: relative;
display: grid;
min-height: 100vh;
place-items: center;
padding: 48px 20px;
background: #FFFFFF;
}
.mnote-auth-brand {
position: fixed;
top: 28px;
left: 34px;
display: inline-flex;
align-items: center;
gap: 10px;
color: #37352F;
font-size: 18px;
font-weight: 650;
letter-spacing: 0;
}
.mnote-auth-brand:hover {
color: #37352F;
}
.mnote-auth-brand-mark {
display: inline-flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
background: var(--wolai-bg);
}
.mnote-auth-card {
text-align: center;
padding: 48px 32px;
max-width: 340px;
}
.mnote-auth-card h1 {
font-size: 32px;
border-radius: 6px;
background: #37352F;
color: #FFF;
font-size: 18px;
font-weight: 700;
color: var(--wolai-text-primary);
margin-bottom: 8px;
line-height: 1;
}
.mnote-auth-card p {
color: var(--wolai-text-secondary);
font-size: 15px;
.mnote-auth-panel {
width: min(100%, 360px);
color: #37352F;
}
.mnote-auth-heading {
margin-bottom: 30px;
text-align: center;
}
.mnote-auth-heading h1 {
margin: 0 0 8px;
color: #37352F;
font-size: 28px;
font-weight: 650;
line-height: 1.25;
letter-spacing: 0;
}
.mnote-auth-heading p {
color: #9B9A97;
font-size: 14px;
line-height: 1.5;
}
.mnote-auth-form {
display: grid;
gap: 12px;
}
.mnote-auth-field {
display: grid;
gap: 7px;
color: #6D6D69;
font-size: 13px;
line-height: 1.3;
}
.mnote-auth-field input {
width: 100%;
height: 42px;
border: 1px solid #DCDAD6;
border-radius: 4px;
background: #FFF;
color: #37352F;
font: 400 15px/1.4 var(--wolai-font-sans);
letter-spacing: 0;
outline: none;
padding: 0 12px;
}
.mnote-auth-field input::placeholder {
color: #B5B1AA;
}
.mnote-auth-field input:focus {
border-color: #A8A39A;
box-shadow: 0 0 0 2px rgba(55, 53, 47, 0.08);
}
.mnote-auth-message {
min-height: 20px;
color: #9B9A97;
font-size: 13px;
line-height: 20px;
text-align: center;
}
.mnote-auth-message[data-type="error"] {
color: #D14343;
}
.mnote-auth-message[data-type="success"] {
color: #23834D;
}
.mnote-auth-submit {
width: 100%;
height: 42px;
border: 0;
border-radius: 4px;
background: #37352F;
color: #FFF;
cursor: pointer;
font: 600 15px/1 var(--wolai-font-sans);
letter-spacing: 0;
}
.mnote-auth-submit:hover {
background: #2F2D29;
}
.mnote-auth-submit:disabled {
cursor: default;
opacity: .58;
}
.mnote-auth-quick-login {
width: 100%;
height: 42px;
border: 1px solid #DCDAD6;
border-radius: 4px;
background: #FFF;
color: #37352F;
cursor: pointer;
font: 500 14px/1 var(--wolai-font-sans);
letter-spacing: 0;
}
.mnote-auth-quick-login:hover {
background: #F7F7F5;
}
.mnote-auth-quick-login:disabled {
cursor: default;
opacity: .58;
}
.mnote-auth-switch {
display: block;
margin: 18px auto 0;
border: 0;
background: transparent;
color: #6D6D69;
cursor: pointer;
font: 400 14px/1.4 var(--wolai-font-sans);
letter-spacing: 0;
}
.mnote-auth-switch:hover {
color: #37352F;
}
@media (max-width: 640px) {
.mnote-auth {
align-items: start;
padding-top: 132px;
}
.mnote-auth-brand {
top: 24px;
left: 22px;
}
}
/* ===== ProseMirror 兼容 ===== */
.ProseMirror {
white-space: pre-wrap;
@@ -1875,6 +2033,515 @@ body {
font-size: 18px;
}
.document-shell[data-page-wide-layout="true"] {
width: min(100%, 980px);
}
.document-shell[data-page-small-text="true"] #mnote-leptos-tiptap-island-editor-root .ProseMirror,
.document-shell[data-page-small-text="true"] #mnote-leptos-tiptap-island-editor-root .ProseMirror p,
.document-shell[data-page-small-text="true"] #mnote-leptos-tiptap-island-editor-root .ProseMirror li {
font-size: 15px;
line-height: 22px;
}
.document-shell[data-layout-density="compact"] #mnote-leptos-tiptap-island-editor-root .ProseMirror p,
.document-shell[data-layout-density="compact"] #mnote-leptos-tiptap-island-editor-root .ProseMirror ul,
.document-shell[data-layout-density="compact"] #mnote-leptos-tiptap-island-editor-root .ProseMirror ol,
.document-shell[data-layout-density="compact"] #mnote-leptos-tiptap-island-editor-root .ProseMirror blockquote {
margin-bottom: 4px;
}
.document-shell[data-layout-density="spacious"] #mnote-leptos-tiptap-island-editor-root .ProseMirror p,
.document-shell[data-layout-density="spacious"] #mnote-leptos-tiptap-island-editor-root .ProseMirror ul,
.document-shell[data-layout-density="spacious"] #mnote-leptos-tiptap-island-editor-root .ProseMirror ol,
.document-shell[data-layout-density="spacious"] #mnote-leptos-tiptap-island-editor-root .ProseMirror blockquote {
margin-bottom: 14px;
}
.document-shell[data-page-font="song"] .document-title-input,
.document-shell[data-page-font="song"] #mnote-leptos-tiptap-island-editor-root .ProseMirror {
font-family: "Noto Serif SC", "Songti SC", serif;
}
.document-shell[data-page-font="kai"] .document-title-input,
.document-shell[data-page-font="kai"] #mnote-leptos-tiptap-island-editor-root .ProseMirror {
font-family: "STKaiti", "KaiTi", serif;
}
.document-shell[data-page-show-heading-numbers="true"] #mnote-leptos-tiptap-island-editor-root .ProseMirror {
counter-reset: mnote-heading;
}
.document-shell[data-page-show-heading-numbers="true"] #mnote-leptos-tiptap-island-editor-root .ProseMirror h1::before,
.document-shell[data-page-show-heading-numbers="true"] #mnote-leptos-tiptap-island-editor-root .ProseMirror h2::before,
.document-shell[data-page-show-heading-numbers="true"] #mnote-leptos-tiptap-island-editor-root .ProseMirror h3::before {
counter-increment: mnote-heading;
content: counter(mnote-heading) ". ";
color: #8B8782;
font-weight: 500;
}
.wolai-page-settings-popover {
position: fixed;
top: 52px;
right: 18px;
z-index: 90;
}
.wolai-page-settings-panel {
width: min(360px, calc(100vw - 24px));
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 14px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 18px 48px rgba(27, 28, 28, 0.12);
}
.wolai-page-settings-tabs {
display: flex;
gap: 6px;
}
.wolai-page-settings-tab {
height: 30px;
padding: 0 10px;
border: 0;
border-radius: 8px;
background: transparent;
color: #6D6A65;
font-size: 13px;
cursor: pointer;
}
.wolai-page-settings-tab.is-active {
background: #F4F3F3;
color: #1B1C1C;
font-weight: 600;
}
.wolai-page-settings-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.wolai-page-settings-section[hidden] {
display: none !important;
}
.wolai-page-setting-row {
min-height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 0;
}
.wolai-page-setting-row.is-pending {
opacity: 0.72;
}
.wolai-page-setting-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 3px;
}
.wolai-page-setting-label {
color: #1B1C1C;
font-size: 14px;
line-height: 20px;
}
.wolai-page-setting-hint,
.wolai-page-settings-global-note {
color: #8B8782;
font-size: 12px;
line-height: 18px;
}
.wolai-page-setting-checkbox {
width: 16px;
height: 16px;
flex: 0 0 auto;
}
.wolai-page-setting-select {
min-width: 96px;
height: 30px;
padding: 0 10px;
border: 1px solid rgba(27, 28, 28, 0.1);
border-radius: 8px;
background: #FFF;
color: #1B1C1C;
font-size: 13px;
}
.wolai-page-settings-actions {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 4px;
border-top: 1px solid rgba(27, 28, 28, 0.06);
}
.wolai-page-settings-action {
min-height: 34px;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 10px;
border: 0;
border-radius: 8px;
background: transparent;
color: #1B1C1C;
font-size: 13px;
cursor: pointer;
}
.wolai-page-settings-action:hover {
background: #F4F3F3;
}
.wolai-page-settings-action.is-disabled {
color: #A19D97;
cursor: not-allowed;
}
.wolai-page-settings-action.is-danger {
color: #C44B55;
}
.wolai-page-settings-stats {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
padding-top: 4px;
color: #8B8782;
font-size: 12px;
line-height: 18px;
}
.wolai-page-history-drawer {
position: fixed;
inset: 0;
z-index: 88;
background: rgba(27, 28, 28, 0.04);
}
.wolai-page-history-drawer[hidden] {
display: none !important;
}
.wolai-page-history-panel {
position: absolute;
top: 16px;
right: 16px;
bottom: 16px;
width: min(380px, calc(100vw - 24px));
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
border-left: 1px solid rgba(27, 28, 28, 0.08);
background: #FFF;
box-shadow: -18px 0 42px rgba(27, 28, 28, 0.12);
}
.wolai-page-history-header,
.wolai-page-share-header,
.wolai-page-ai-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.wolai-page-history-title,
.wolai-page-share-title,
.wolai-page-ai-title {
color: #1B1C1C;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.wolai-page-history-subtitle,
.wolai-page-share-subtitle,
.wolai-page-ai-subtitle {
color: #8B8782;
font-size: 12px;
line-height: 18px;
}
.wolai-surface-close {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 8px;
background: transparent;
color: #5A5A5A;
cursor: pointer;
}
.wolai-surface-close:hover {
background: #F4F3F3;
}
.wolai-page-history-list {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 10px;
overflow: auto;
}
.wolai-page-history-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 10px;
background: #FFF;
}
.wolai-page-history-item-title {
color: #1B1C1C;
font-size: 13px;
font-weight: 500;
line-height: 20px;
}
.wolai-page-history-item-meta,
.wolai-page-history-empty,
.wolai-page-share-copy,
.wolai-page-share-footer,
.wolai-page-ai-empty,
.wolai-page-ai-message-role {
color: #8B8782;
font-size: 12px;
line-height: 18px;
}
.wolai-page-history-item-ghost,
.wolai-page-ai-ghost,
.wolai-page-ai-tab,
.wolai-page-ai-model-chip,
.wolai-page-share-copy-button {
height: 30px;
padding: 0 10px;
border: 0;
border-radius: 8px;
background: #F4F3F3;
color: #1B1C1C;
font-size: 12px;
cursor: pointer;
}
.wolai-page-share-dialog {
position: fixed;
inset: 0;
z-index: 92;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(27, 28, 28, 0.12);
}
.wolai-page-share-dialog[hidden] {
display: none !important;
}
.wolai-page-share-card {
width: min(460px, calc(100vw - 24px));
display: flex;
flex-direction: column;
gap: 16px;
padding: 18px;
border-radius: 16px;
background: #FFF;
box-shadow: 0 18px 48px rgba(27, 28, 28, 0.18);
}
.wolai-page-share-state {
display: flex;
flex-direction: column;
gap: 8px;
}
.wolai-public-pill--inline {
width: fit-content;
}
.wolai-page-share-url-row {
display: flex;
gap: 8px;
}
.wolai-page-share-url {
flex: 1 1 auto;
height: 34px;
padding: 0 10px;
border: 1px solid rgba(27, 28, 28, 0.1);
border-radius: 8px;
background: #FFF;
color: #1B1C1C;
font-size: 13px;
}
.wolai-page-ai-drawer {
position: fixed;
top: 12px;
right: 12px;
bottom: 12px;
z-index: 89;
}
.wolai-page-ai-drawer[hidden] {
display: none !important;
}
.wolai-page-ai-panel {
width: min(440px, calc(100vw - 24px));
height: 100%;
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 18px;
background: rgba(255, 255, 255, 0.98);
box-shadow: -18px 0 42px rgba(27, 28, 28, 0.14);
}
.wolai-page-ai-header-copy {
display: flex;
flex-direction: column;
gap: 4px;
}
.wolai-page-ai-body {
display: flex;
min-height: 0;
flex: 1 1 auto;
flex-direction: column;
gap: 16px;
}
.wolai-page-ai-suggestions {
display: flex;
flex-direction: column;
gap: 10px;
}
.wolai-page-ai-suggestions-header,
.wolai-page-ai-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.wolai-page-ai-suggestion-list,
.wolai-page-ai-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.wolai-page-ai-suggestion {
min-height: 32px;
padding: 8px 10px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 10px;
background: #FFF;
color: #1B1C1C;
font-size: 12px;
text-align: left;
cursor: pointer;
}
.wolai-page-ai-conversation {
display: flex;
min-height: 0;
flex: 1 1 auto;
flex-direction: column;
gap: 10px;
overflow: auto;
}
.wolai-page-ai-message {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border-radius: 12px;
background: #F7F7F7;
}
.wolai-page-ai-message--user {
background: #F4F8FF;
}
.wolai-page-ai-message-text {
color: #1B1C1C;
font-size: 13px;
line-height: 20px;
white-space: pre-wrap;
}
.wolai-page-ai-footer {
display: flex;
flex-direction: column;
gap: 10px;
}
.wolai-page-ai-tab.is-active {
background: #1B1C1C;
color: #FFF;
}
.wolai-page-ai-input-row {
display: flex;
gap: 8px;
align-items: flex-end;
}
.wolai-page-ai-input {
flex: 1 1 auto;
min-height: 88px;
padding: 10px 12px;
border: 1px solid rgba(27, 28, 28, 0.1);
border-radius: 12px;
background: #FFF;
color: #1B1C1C;
font-size: 13px;
line-height: 20px;
resize: none;
}
.wolai-page-ai-send {
width: 36px;
height: 36px;
border: 0;
border-radius: 10px;
background: #1B1C1C;
color: #FFF;
font-size: 16px;
cursor: pointer;
}
@media (max-width: 768px) {
.mnote-sidebar,
.wolai-sidebar {
@@ -1885,6 +2552,15 @@ body {
padding: 0 12px;
}
.wolai-page-settings-popover {
right: 12px;
}
.wolai-page-history-panel,
.wolai-page-ai-panel {
width: min(100vw - 24px, 420px);
}
.wolai-public-pill,
.wolai-breadcrumb-root,
.wolai-breadcrumb-separator {
@@ -1930,6 +2606,12 @@ body {
.wolai-topbar-actions {
gap: 2px;
}
.wolai-page-settings-panel,
.wolai-page-share-card,
.wolai-page-ai-panel {
width: calc(100vw - 24px);
}
}
"##;
@@ -66,6 +66,7 @@ export interface InitOutput {
readonly wasm_bindgen__convert__closures_____invoke__h03deb55668e1377f: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h837fba73fce77300: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h60e57afd955e8441: (a: number, b: number) => number;
readonly wasm_bindgen__convert__closures_____invoke__h96c1a94f3b510398: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__hd17f30facfdd737f: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__hf726e22cfd1ee0a9: (a: number, b: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
@@ -33,10 +33,12 @@ import { register_text_style } from './snippets/leptos-tiptap-c355e6ec24c3df4c/s
import { register_toc_node } from './snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_toc_node.js';
import { register_underline } from './snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_underline.js';
import { register_youtube } from './snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/tiptap_youtube.js';
import { find_mnote_block_anchor, post_mnote_document_save, write_mnote_text_to_clipboard } from './snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js';
import * as import1 from "./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js"
import * as import2 from "./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js"
import * as import3 from "./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js"
import * as import3 from "./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js"
import * as import4 from "./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js"
import * as import5 from "./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js"
export class IntoUnderlyingByteSource {
@@ -475,6 +477,10 @@ function __wbg_get_imports() {
const ret = arg0.fetch(getStringFromWasm0(arg1, arg2), arg3);
return ret;
},
__wbg_find_mnote_block_anchor_451c3d968e5e0147: function(arg0, arg1) {
const ret = find_mnote_block_anchor(getStringFromWasm0(arg0, arg1));
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
},
__wbg_firstElementChild_f67647a589d437a2: function(arg0) {
const ret = arg0.firstElementChild;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
@@ -529,6 +535,13 @@ function __wbg_get_imports() {
const ret = arg0[arg1 >>> 0];
return ret;
},
__wbg_get_aa7ea1c497b45090: function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = arg1.get(getStringFromWasm0(arg2, arg3));
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
}, arguments); },
__wbg_get_unchecked_17f53dad852b9588: function(arg0, arg1) {
const ret = arg0[arg1 >>> 0];
return ret;
@@ -537,6 +550,17 @@ function __wbg_get_imports() {
const ret = arg0[arg1];
return ret;
},
__wbg_hash_6b96fb5ff20f84b3: function() { return handleError(function (arg0, arg1) {
const ret = arg1.hash;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
}, arguments); },
__wbg_headers_6022deb4e576fb8e: function(arg0) {
const ret = arg0.headers;
return ret;
},
__wbg_height_cc0f4b9ec7073c11: function(arg0) {
const ret = arg0.height;
return ret;
@@ -726,6 +750,10 @@ function __wbg_get_imports() {
const ret = arg0.metaKey;
return ret;
},
__wbg_new_0_4d657201ced14de3: function() {
const ret = new Date();
return ret;
},
__wbg_new_0c7403db6e782f19: function(arg0) {
const ret = new Uint8Array(arg0);
return ret;
@@ -788,6 +816,13 @@ function __wbg_get_imports() {
const ret = arg0.ok;
return ret;
},
__wbg_origin_1f038926109a2a37: function() { return handleError(function (arg0, arg1) {
const ret = arg1.origin;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
}, arguments); },
__wbg_parentElement_d1271cca94202d1f: function(arg0) {
const ret = arg0.parentElement;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
@@ -796,6 +831,25 @@ function __wbg_get_imports() {
const ret = arg0.parentNode;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
},
__wbg_pathname_94c34ca840abb4f4: function() { return handleError(function (arg0, arg1) {
const ret = arg1.pathname;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
}, arguments); },
__wbg_post_mnote_document_save_94a0f8ecea85d168: function() { return handleError(function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
const ret = post_mnote_document_save(getStringFromWasm0(arg0, arg1));
return ret;
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}, arguments); },
__wbg_preventDefault_f55c01cb5fd2bcc0: function(arg0) {
arg0.preventDefault();
},
@@ -982,6 +1036,9 @@ function __wbg_get_imports() {
const ret = arg0.scrollHeight;
return ret;
},
__wbg_scrollIntoView_7725227126cff177: function(arg0, arg1) {
arg0.scrollIntoView(arg1 !== 0);
},
__wbg_scrollTop_f548101d48000fe9: function(arg0) {
const ret = arg0.scrollTop;
return ret;
@@ -1005,6 +1062,10 @@ function __wbg_get_imports() {
__wbg_setProperty_0d903d23a71dfe70: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
arg0.setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments); },
__wbg_setTimeout_d8786dd31f90da0f: function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.setTimeout(arg1, arg2);
return ret;
}, arguments); },
__wbg_set_022bee52d0b05b19: function() { return handleError(function (arg0, arg1, arg2) {
const ret = Reflect.set(arg0, arg1, arg2);
return ret;
@@ -1096,6 +1157,10 @@ function __wbg_get_imports() {
const ret = arg0.target;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
},
__wbg_text_595ef75535aa25c1: function() { return handleError(function (arg0) {
const ret = arg0.text();
return ret;
}, arguments); },
__wbg_then_792e0c862b060889: function(arg0, arg1, arg2) {
const ret = arg0.then(arg1, arg2);
return ret;
@@ -1104,6 +1169,14 @@ function __wbg_get_imports() {
const ret = arg0.then(arg1);
return ret;
},
__wbg_toISOString_07c00b3614e865a1: function(arg0) {
const ret = arg0.toISOString();
return ret;
},
__wbg_toString_306ed0b9f320c1ca: function(arg0) {
const ret = arg0.toString();
return ret;
},
__wbg_top_158f7c4dd1427771: function(arg0) {
const ret = arg0.top;
return ret;
@@ -1133,57 +1206,74 @@ function __wbg_get_imports() {
const ret = arg0.width;
return ret;
},
__wbg_write_mnote_text_to_clipboard_d233faf092e4e4c0: function() { return handleError(function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
const ret = write_mnote_text_to_clipboard(getStringFromWasm0(arg0, arg1));
return ret;
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}, arguments); },
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1456, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1476, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hdfdf165eabd6279b);
return ret;
},
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1686, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1715, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2593b18d2d6f8a75);
return ret;
},
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1777, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1806, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h36e788bd1cd1f0f7);
return ret;
},
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1611, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1632, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h03deb55668e1377f);
return ret;
},
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1688, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1717, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h837fba73fce77300);
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1624, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd17f30facfdd737f);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1631, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h96c1a94f3b510398);
return ret;
},
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1687, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1653, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd17f30facfdd737f);
return ret;
},
__wbindgen_cast_0000000000000008: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1716, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hf726e22cfd1ee0a9);
return ret;
},
__wbindgen_cast_0000000000000008: function(arg0) {
__wbindgen_cast_0000000000000009: function(arg0) {
// Cast intrinsic for `F64 -> Externref`.
const ret = arg0;
return ret;
},
__wbindgen_cast_0000000000000009: function(arg0) {
__wbindgen_cast_000000000000000a: function(arg0) {
// Cast intrinsic for `I64 -> Externref`.
const ret = arg0;
return ret;
},
__wbindgen_cast_000000000000000a: function(arg0, arg1) {
__wbindgen_cast_000000000000000b: function(arg0, arg1) {
// Cast intrinsic for `Ref(String) -> Externref`.
const ret = getStringFromWasm0(arg0, arg1);
return ret;
},
__wbindgen_cast_000000000000000b: function(arg0) {
__wbindgen_cast_000000000000000c: function(arg0) {
// Cast intrinsic for `U64 -> Externref`.
const ret = BigInt.asUintN(64, arg0);
return ret;
@@ -1203,11 +1293,16 @@ function __wbg_get_imports() {
"./mnote-leptos-tiptap-spike-island_bg.js": import0,
"./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js": import1,
"./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js": import2,
"./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js": import3,
"./snippets/mnote-leptos-tiptap-spike-fdb5779a1690afd2/inline0.js": import3,
"./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js": import4,
"./snippets/leptos-tiptap-c355e6ec24c3df4c/src/js/generated/bridge_runtime.js": import5,
};
}
function wasm_bindgen__convert__closures_____invoke__h96c1a94f3b510398(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__h96c1a94f3b510398(arg0, arg1);
}
function wasm_bindgen__convert__closures_____invoke__hd17f30facfdd737f(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__hd17f30facfdd737f(arg0, arg1);
}
@@ -23,6 +23,7 @@ export const wasm_bindgen__convert__closures_____invoke__h2593b18d2d6f8a75: (a:
export const wasm_bindgen__convert__closures_____invoke__h03deb55668e1377f: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h837fba73fce77300: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h60e57afd955e8441: (a: number, b: number) => number;
export const wasm_bindgen__convert__closures_____invoke__h96c1a94f3b510398: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__hd17f30facfdd737f: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__hf726e22cfd1ee0a9: (a: number, b: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
@@ -1 +1 @@
var n=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(n==null||n.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var e=n.modules["@tiptap/core"];if(e==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var x=e.CommandManager,m=e.Editor,k=e.Extension,b=e.InputRule,y=e.Mark,r=e.Node,h=e.NodePos,f=e.NodeView,E=e.PasteRule,R=e.Tracker,P=e.callOrReturn,C=e.canInsertNode,T=e.combineTransactionSteps,A=e.createChainableState,S=e.createDocument,B=e.createNodeFromContent,w=e.createStyleTag,N=e.defaultBlockAt,O=e.deleteProps,v=e.elementFromString,M=e.escapeForRegEx,I=e.extensions,q=e.findChildren,F=e.findChildrenInRange,D=e.findDuplicates,j=e.findParentNode,L=e.findParentNodeClosestToPos,K=e.fromString,H=e.generateHTML,G=e.generateJSON,z=e.generateText,J=e.getAttributes,U=e.getAttributesFromExtensions,V=e.getChangedRanges,Y=e.getDebugJSON,W=e.getExtensionField,$=e.getHTMLFromFragment,Q=e.getMarkAttributes,X=e.getMarkRange,Z=e.getMarkType,ee=e.getMarksBetween,te=e.getNodeAtPosition,ne=e.getNodeAttributes,oe=e.getNodeType,ie=e.getRenderedAttributes,re=e.getSchema,se=e.getSchemaByResolvedExtensions,le=e.getSchemaTypeByName,ae=e.getSchemaTypeNameByName,de=e.getSplittedAttributes,pe=e.getText,ce=e.getTextBetween,ue=e.getTextContentFromNodes,ge=e.getTextSerializersFromSchema,_e=e.injectExtensionAttributesToParseRule,xe=e.inputRulesPlugin,me=e.isActive,ke=e.isAtEndOfNode,be=e.isAtStartOfNode,ye=e.isEmptyObject,he=e.isExtensionRulesEnabled,fe=e.isFunction,Ee=e.isList,Re=e.isMacOS,Pe=e.isMarkActive,Ce=e.isNodeActive,Te=e.isNodeEmpty,Ae=e.isNodeSelection,Se=e.isNumber,Be=e.isPlainObject,we=e.isRegExp,Ne=e.isSafari,Oe=e.isString,ve=e.isTextSelection,Me=e.isiOS,Ie=e.markInputRule,qe=e.markPasteRule,s=e.mergeAttributes,Fe=e.mergeDeep,De=e.minMax,je=e.nodeInputRule,Le=e.nodePasteRule,Ke=e.objectIncludes,He=e.pasteRulesPlugin,Ge=e.posToDOMRect,ze=e.removeDuplicates,Je=e.resolveFocusPosition,Ue=e.rewriteUnknownContent,Ve=e.selectionToInsertionEnd,Ye=e.splitExtensions,We=e.textInputRule,$e=e.textPasteRule,Qe=e.textblockTypeInputRule,l=e.wrappingInputRule;var u=/^\s*>\s$/,a=r.create({name:"blockquote",addOptions(){return{HTMLAttributes:{}}},content:"block+",group:"block",defining:!0,parseHTML(){return[{tag:"blockquote"}]},renderHTML({HTMLAttributes:t}){return["blockquote",s(this.options.HTMLAttributes,t),0]},addCommands(){return{setBlockquote:()=>({commands:t})=>t.wrapIn(this.name),toggleBlockquote:()=>({commands:t})=>t.toggleWrap(this.name),unsetBlockquote:()=>({commands:t})=>t.lift(this.name)}},addKeyboardShortcuts(){return{"Mod-Shift-b":()=>this.editor.commands.toggleBlockquote()}},addInputRules(){return[l({find:u,type:this.type})]}});var d="__LEPTOS_TIPTAP_BRIDGE__";function g(){let t=globalThis,o=t[d];if(o!=null)return o;let i={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return t[d]=i,i}function p(){return g()}function c(t){p().registerExtension(t)}var _={name:"blockquote",create:()=>a,commands:{set_blockquote:t=>t.chain().focus().setBlockquote().run(),toggle_blockquote:t=>t.chain().focus().toggleBlockquote().run(),unset_blockquote:t=>t.chain().focus().unsetBlockquote().run()},selection_keys:["blockquote"],selection_state:t=>({blockquote:t.isActive("blockquote")})};function st(){c(_)}export{st as register_blockquote};
var n=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(n==null||n.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var t=n.modules["@tiptap/core"];if(t==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var k=t.CommandManager,b=t.Editor,y=t.Extension,h=t.InputRule,f=t.Mark,r=t.Node,E=t.NodePos,R=t.NodeView,P=t.PasteRule,T=t.Tracker,A=t.callOrReturn,C=t.canInsertNode,S=t.combineTransactionSteps,B=t.createChainableState,w=t.createDocument,N=t.createNodeFromContent,O=t.createStyleTag,M=t.defaultBlockAt,v=t.deleteProps,I=t.elementFromString,q=t.escapeForRegEx,F=t.extensions,D=t.findChildren,j=t.findChildrenInRange,L=t.findDuplicates,H=t.findParentNode,K=t.findParentNodeClosestToPos,G=t.fromString,z=t.generateHTML,J=t.generateJSON,U=t.generateText,V=t.getAttributes,Y=t.getAttributesFromExtensions,W=t.getChangedRanges,$=t.getDebugJSON,Q=t.getExtensionField,X=t.getHTMLFromFragment,Z=t.getMarkAttributes,tt=t.getMarkRange,et=t.getMarkType,nt=t.getMarksBetween,ot=t.getNodeAtPosition,it=t.getNodeAttributes,rt=t.getNodeType,st=t.getRenderedAttributes,lt=t.getSchema,at=t.getSchemaByResolvedExtensions,dt=t.getSchemaTypeByName,pt=t.getSchemaTypeNameByName,ut=t.getSplittedAttributes,ct=t.getText,gt=t.getTextBetween,_t=t.getTextContentFromNodes,xt=t.getTextSerializersFromSchema,mt=t.injectExtensionAttributesToParseRule,kt=t.inputRulesPlugin,bt=t.isActive,yt=t.isAtEndOfNode,ht=t.isAtStartOfNode,ft=t.isEmptyObject,Et=t.isExtensionRulesEnabled,Rt=t.isFunction,Pt=t.isList,Tt=t.isMacOS,At=t.isMarkActive,Ct=t.isNodeActive,St=t.isNodeEmpty,Bt=t.isNodeSelection,wt=t.isNumber,Nt=t.isPlainObject,Ot=t.isRegExp,Mt=t.isSafari,vt=t.isString,It=t.isTextSelection,qt=t.isiOS,Ft=t.markInputRule,Dt=t.markPasteRule,s=t.mergeAttributes,jt=t.mergeDeep,Lt=t.minMax,Ht=t.nodeInputRule,Kt=t.nodePasteRule,Gt=t.objectIncludes,zt=t.pasteRulesPlugin,Jt=t.posToDOMRect,Ut=t.removeDuplicates,Vt=t.resolveFocusPosition,Yt=t.rewriteUnknownContent,Wt=t.selectionToInsertionEnd,$t=t.splitExtensions,Qt=t.textInputRule,Xt=t.textPasteRule,Zt=t.textblockTypeInputRule,l=t.wrappingInputRule;var c=/^\s*>\s$/,a=r.create({name:"blockquote",addOptions(){return{HTMLAttributes:{}}},content:"block+",group:"block",defining:!0,parseHTML(){return[{tag:"blockquote"}]},renderHTML({HTMLAttributes:e}){return["blockquote",s(this.options.HTMLAttributes,e),0]},addCommands(){return{setBlockquote:()=>({commands:e})=>e.wrapIn(this.name),toggleBlockquote:()=>({commands:e})=>e.toggleWrap(this.name),unsetBlockquote:()=>({commands:e})=>e.lift(this.name)}},addKeyboardShortcuts(){return{"Mod-Shift-b":()=>this.editor.commands.toggleBlockquote()}},addInputRules(){return[l({find:c,type:this.type})]}});var d="__LEPTOS_TIPTAP_BRIDGE__";function g(){let e=globalThis,o=e[d];if(o!=null)return o;let i={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return e[d]=i,i}function p(){return g()}function u(e){p().registerExtension(e)}function _(e){return typeof e=="string"&&e.length>0?{"data-block-id":e,id:e}:{}}var x=a.extend({addAttributes(){return{...this.parent?.(),blockId:{default:null,parseHTML:e=>e.getAttribute("data-block-id"),renderHTML:e=>_(e.blockId)}}}}),m={name:"blockquote",create:()=>x,commands:{set_blockquote:e=>e.chain().focus().setBlockquote().run(),toggle_blockquote:e=>e.chain().focus().toggleBlockquote().run(),unset_blockquote:e=>e.chain().focus().unsetBlockquote().run()},selection_keys:["blockquote"],selection_state:e=>({blockquote:e.isActive("blockquote")})};function ae(){u(m)}export{ae as register_blockquote};
@@ -1,4 +1,4 @@
var p=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(p==null||p.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var e=p.modules["@tiptap/core"];if(e==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var N=e.CommandManager,B=e.Editor,O=e.Extension,v=e.InputRule,M=e.Mark,_=e.Node,D=e.NodePos,I=e.NodeView,F=e.PasteRule,j=e.Tracker,L=e.callOrReturn,K=e.canInsertNode,H=e.combineTransactionSteps,G=e.createChainableState,q=e.createDocument,z=e.createNodeFromContent,$=e.createStyleTag,J=e.defaultBlockAt,W=e.deleteProps,V=e.elementFromString,U=e.escapeForRegEx,Y=e.extensions,Q=e.findChildren,X=e.findChildrenInRange,Z=e.findDuplicates,ee=e.findParentNode,te=e.findParentNodeClosestToPos,ne=e.fromString,oe=e.generateHTML,re=e.generateJSON,ie=e.generateText,se=e.getAttributes,le=e.getAttributesFromExtensions,ae=e.getChangedRanges,de=e.getDebugJSON,pe=e.getExtensionField,ce=e.getHTMLFromFragment,ue=e.getMarkAttributes,ge=e.getMarkRange,_e=e.getMarkType,xe=e.getMarksBetween,me=e.getNodeAtPosition,ke=e.getNodeAttributes,be=e.getNodeType,ye=e.getRenderedAttributes,fe=e.getSchema,he=e.getSchemaByResolvedExtensions,Ee=e.getSchemaTypeByName,Se=e.getSchemaTypeNameByName,Pe=e.getSplittedAttributes,Ce=e.getText,Ae=e.getTextBetween,Re=e.getTextContentFromNodes,Te=e.getTextSerializersFromSchema,we=e.injectExtensionAttributesToParseRule,Ne=e.inputRulesPlugin,Be=e.isActive,Oe=e.isAtEndOfNode,ve=e.isAtStartOfNode,Me=e.isEmptyObject,De=e.isExtensionRulesEnabled,Ie=e.isFunction,Fe=e.isList,je=e.isMacOS,Le=e.isMarkActive,Ke=e.isNodeActive,He=e.isNodeEmpty,Ge=e.isNodeSelection,qe=e.isNumber,ze=e.isPlainObject,$e=e.isRegExp,Je=e.isSafari,We=e.isString,Ve=e.isTextSelection,Ue=e.isiOS,Ye=e.markInputRule,Qe=e.markPasteRule,x=e.mergeAttributes,Xe=e.mergeDeep,Ze=e.minMax,et=e.nodeInputRule,tt=e.nodePasteRule,nt=e.objectIncludes,ot=e.pasteRulesPlugin,rt=e.posToDOMRect,it=e.removeDuplicates,st=e.resolveFocusPosition,lt=e.rewriteUnknownContent,at=e.selectionToInsertionEnd,dt=e.splitExtensions,pt=e.textInputRule,ct=e.textPasteRule,c=e.textblockTypeInputRule,ut=e.wrappingInputRule;var u=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(u==null||u.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var l=u.modules["@tiptap/pm/state"];if(l==null)throw new Error('leptos-tiptap bridge module "@tiptap/pm/state" is unavailable');var _t=l.AllSelection,xt=l.EditorState,mt=l.NodeSelection,m=l.Plugin,k=l.PluginKey,b=l.Selection,kt=l.SelectionRange,y=l.TextSelection,bt=l.Transaction;var A=/^```([a-z]+)?[\s\n]$/,R=/^~~~([a-z]+)?[\s\n]$/,f=_.create({name:"codeBlock",addOptions(){return{languageClassPrefix:"language-",exitOnTripleEnter:!0,exitOnArrowDown:!0,defaultLanguage:null,HTMLAttributes:{}}},content:"text*",marks:"",group:"block",code:!0,defining:!0,addAttributes(){return{language:{default:this.options.defaultLanguage,parseHTML:t=>{var n;let{languageClassPrefix:o}=this.options,a=[...((n=t.firstElementChild)===null||n===void 0?void 0:n.classList)||[]].filter(r=>r.startsWith(o)).map(r=>r.replace(o,""))[0];return a||null},rendered:!1}}},parseHTML(){return[{tag:"pre",preserveWhitespace:"full"}]},renderHTML({node:t,HTMLAttributes:n}){return["pre",x(this.options.HTMLAttributes,n),["code",{class:t.attrs.language?this.options.languageClassPrefix+t.attrs.language:null},0]]},addCommands(){return{setCodeBlock:t=>({commands:n})=>n.setNode(this.name,t),toggleCodeBlock:t=>({commands:n})=>n.toggleNode(this.name,"paragraph",t)}},addKeyboardShortcuts(){return{"Mod-Alt-c":()=>this.editor.commands.toggleCodeBlock(),Backspace:()=>{let{empty:t,$anchor:n}=this.editor.state.selection,o=n.pos===1;return!t||n.parent.type.name!==this.name?!1:o||!n.parent.textContent.length?this.editor.commands.clearNodes():!1},Enter:({editor:t})=>{if(!this.options.exitOnTripleEnter)return!1;let{state:n}=t,{selection:o}=n,{$from:i,empty:s}=o;if(!s||i.parent.type!==this.type)return!1;let a=i.parentOffset===i.parent.nodeSize-2,r=i.parent.textContent.endsWith(`
var p=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(p==null||p.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var e=p.modules["@tiptap/core"];if(e==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var O=e.CommandManager,v=e.Editor,M=e.Extension,D=e.InputRule,I=e.Mark,_=e.Node,F=e.NodePos,L=e.NodeView,j=e.PasteRule,H=e.Tracker,K=e.callOrReturn,G=e.canInsertNode,q=e.combineTransactionSteps,z=e.createChainableState,$=e.createDocument,J=e.createNodeFromContent,W=e.createStyleTag,V=e.defaultBlockAt,U=e.deleteProps,Y=e.elementFromString,Q=e.escapeForRegEx,X=e.extensions,Z=e.findChildren,ee=e.findChildrenInRange,te=e.findDuplicates,ne=e.findParentNode,oe=e.findParentNodeClosestToPos,re=e.fromString,ie=e.generateHTML,se=e.generateJSON,le=e.generateText,ae=e.getAttributes,de=e.getAttributesFromExtensions,pe=e.getChangedRanges,ce=e.getDebugJSON,ue=e.getExtensionField,ge=e.getHTMLFromFragment,_e=e.getMarkAttributes,xe=e.getMarkRange,me=e.getMarkType,ke=e.getMarksBetween,be=e.getNodeAtPosition,ye=e.getNodeAttributes,fe=e.getNodeType,he=e.getRenderedAttributes,Ee=e.getSchema,Ae=e.getSchemaByResolvedExtensions,Se=e.getSchemaTypeByName,Pe=e.getSchemaTypeNameByName,Ce=e.getSplittedAttributes,Re=e.getText,Te=e.getTextBetween,we=e.getTextContentFromNodes,Be=e.getTextSerializersFromSchema,Ne=e.injectExtensionAttributesToParseRule,Oe=e.inputRulesPlugin,ve=e.isActive,Me=e.isAtEndOfNode,De=e.isAtStartOfNode,Ie=e.isEmptyObject,Fe=e.isExtensionRulesEnabled,Le=e.isFunction,je=e.isList,He=e.isMacOS,Ke=e.isMarkActive,Ge=e.isNodeActive,qe=e.isNodeEmpty,ze=e.isNodeSelection,$e=e.isNumber,Je=e.isPlainObject,We=e.isRegExp,Ve=e.isSafari,Ue=e.isString,Ye=e.isTextSelection,Qe=e.isiOS,Xe=e.markInputRule,Ze=e.markPasteRule,x=e.mergeAttributes,et=e.mergeDeep,tt=e.minMax,nt=e.nodeInputRule,ot=e.nodePasteRule,rt=e.objectIncludes,it=e.pasteRulesPlugin,st=e.posToDOMRect,lt=e.removeDuplicates,at=e.resolveFocusPosition,dt=e.rewriteUnknownContent,pt=e.selectionToInsertionEnd,ct=e.splitExtensions,ut=e.textInputRule,gt=e.textPasteRule,c=e.textblockTypeInputRule,_t=e.wrappingInputRule;var u=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(u==null||u.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var l=u.modules["@tiptap/pm/state"];if(l==null)throw new Error('leptos-tiptap bridge module "@tiptap/pm/state" is unavailable');var mt=l.AllSelection,kt=l.EditorState,bt=l.NodeSelection,m=l.Plugin,k=l.PluginKey,b=l.Selection,yt=l.SelectionRange,y=l.TextSelection,ft=l.Transaction;var C=/^```([a-z]+)?[\s\n]$/,R=/^~~~([a-z]+)?[\s\n]$/,f=_.create({name:"codeBlock",addOptions(){return{languageClassPrefix:"language-",exitOnTripleEnter:!0,exitOnArrowDown:!0,defaultLanguage:null,HTMLAttributes:{}}},content:"text*",marks:"",group:"block",code:!0,defining:!0,addAttributes(){return{language:{default:this.options.defaultLanguage,parseHTML:t=>{var n;let{languageClassPrefix:o}=this.options,a=[...((n=t.firstElementChild)===null||n===void 0?void 0:n.classList)||[]].filter(r=>r.startsWith(o)).map(r=>r.replace(o,""))[0];return a||null},rendered:!1}}},parseHTML(){return[{tag:"pre",preserveWhitespace:"full"}]},renderHTML({node:t,HTMLAttributes:n}){return["pre",x(this.options.HTMLAttributes,n),["code",{class:t.attrs.language?this.options.languageClassPrefix+t.attrs.language:null},0]]},addCommands(){return{setCodeBlock:t=>({commands:n})=>n.setNode(this.name,t),toggleCodeBlock:t=>({commands:n})=>n.toggleNode(this.name,"paragraph",t)}},addKeyboardShortcuts(){return{"Mod-Alt-c":()=>this.editor.commands.toggleCodeBlock(),Backspace:()=>{let{empty:t,$anchor:n}=this.editor.state.selection,o=n.pos===1;return!t||n.parent.type.name!==this.name?!1:o||!n.parent.textContent.length?this.editor.commands.clearNodes():!1},Enter:({editor:t})=>{if(!this.options.exitOnTripleEnter)return!1;let{state:n}=t,{selection:o}=n,{$from:i,empty:s}=o;if(!s||i.parent.type!==this.type)return!1;let a=i.parentOffset===i.parent.nodeSize-2,r=i.parent.textContent.endsWith(`
`);return!a||!r?!1:t.chain().command(({tr:d})=>(d.delete(i.pos-2,i.pos),!0)).exitCode().run()},ArrowDown:({editor:t})=>{if(!this.options.exitOnArrowDown)return!1;let{state:n}=t,{selection:o,doc:i}=n,{$from:s,empty:a}=o;if(!a||s.parent.type!==this.type||!(s.parentOffset===s.parent.nodeSize-2))return!1;let d=s.after();return d===void 0?!1:i.nodeAt(d)?t.commands.command(({tr:C})=>(C.setSelection(b.near(i.resolve(d))),!0)):t.commands.exitCode()}}},addInputRules(){return[c({find:A,type:this.type,getAttributes:t=>({language:t[1]})}),c({find:R,type:this.type,getAttributes:t=>({language:t[1]})})]},addProseMirrorPlugins(){return[new m({key:new k("codeBlockVSCodeHandler"),props:{handlePaste:(t,n)=>{if(!n.clipboardData||this.editor.isActive(this.type.name))return!1;let o=n.clipboardData.getData("text/plain"),i=n.clipboardData.getData("vscode-editor-data"),s=i?JSON.parse(i):void 0,a=s?.mode;if(!o||!a)return!1;let{tr:r,schema:d}=t.state,g=d.text(o.replace(/\r\n?/g,`
`));return r.replaceSelectionWith(this.type.create({language:a},g)),r.selection.$from.parent.type!==this.type&&r.setSelection(y.near(r.doc.resolve(Math.max(0,r.selection.from-2)))),r.setMeta("paste",!0),t.dispatch(r),!0}}})]}});var h="__LEPTOS_TIPTAP_BRIDGE__";function T(){let t=globalThis,n=t[h];if(n!=null)return n;let o={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return t[h]=o,o}function E(){return T()}function S(t){E().registerExtension(t)}function P(t){if(t?.language!=null)return{language:t.language}}var w={name:"code_block",create:()=>f,commands:{set_code_block:(t,n)=>n.kind==="set_code_block"?t.chain().focus().setCodeBlock(P(n.attributes)).run():!1,toggle_code_block:(t,n)=>n.kind==="toggle_code_block"?t.chain().focus().toggleCodeBlock(P(n.attributes)).run():!1}};function Tt(){S(w)}export{Tt as register_code_block};
`);return!a||!r?!1:t.chain().command(({tr:d})=>(d.delete(i.pos-2,i.pos),!0)).exitCode().run()},ArrowDown:({editor:t})=>{if(!this.options.exitOnArrowDown)return!1;let{state:n}=t,{selection:o,doc:i}=n,{$from:s,empty:a}=o;if(!a||s.parent.type!==this.type||!(s.parentOffset===s.parent.nodeSize-2))return!1;let d=s.after();return d===void 0?!1:i.nodeAt(d)?t.commands.command(({tr:P})=>(P.setSelection(b.near(i.resolve(d))),!0)):t.commands.exitCode()}}},addInputRules(){return[c({find:C,type:this.type,getAttributes:t=>({language:t[1]})}),c({find:R,type:this.type,getAttributes:t=>({language:t[1]})})]},addProseMirrorPlugins(){return[new m({key:new k("codeBlockVSCodeHandler"),props:{handlePaste:(t,n)=>{if(!n.clipboardData||this.editor.isActive(this.type.name))return!1;let o=n.clipboardData.getData("text/plain"),i=n.clipboardData.getData("vscode-editor-data"),s=i?JSON.parse(i):void 0,a=s?.mode;if(!o||!a)return!1;let{tr:r,schema:d}=t.state,g=d.text(o.replace(/\r\n?/g,`
`));return r.replaceSelectionWith(this.type.create({language:a},g)),r.selection.$from.parent.type!==this.type&&r.setSelection(y.near(r.doc.resolve(Math.max(0,r.selection.from-2)))),r.setMeta("paste",!0),t.dispatch(r),!0}}})]}});var h="__LEPTOS_TIPTAP_BRIDGE__";function T(){let t=globalThis,n=t[h];if(n!=null)return n;let o={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return t[h]=o,o}function E(){return T()}function A(t){E().registerExtension(t)}function w(t){return typeof t=="string"&&t.length>0?{"data-block-id":t,id:t}:{}}function S(t){if(t?.language!=null)return{language:t.language}}var B=f.extend({addAttributes(){return{...this.parent?.(),blockId:{default:null,parseHTML:t=>t.getAttribute("data-block-id"),renderHTML:t=>w(t.blockId)}}}}),N={name:"code_block",create:()=>B,commands:{set_code_block:(t,n)=>n.kind==="set_code_block"?t.chain().focus().setCodeBlock(S(n.attributes)).run():!1,toggle_code_block:(t,n)=>n.kind==="toggle_code_block"?t.chain().focus().toggleCodeBlock(S(n.attributes)).run():!1}};function Bt(){A(N)}export{Bt as register_code_block};
@@ -1 +1 @@
var i=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(i==null||i.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var e=i.modules["@tiptap/core"];if(e==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var x=e.CommandManager,m=e.Editor,k=e.Extension,b=e.InputRule,y=e.Mark,r=e.Node,h=e.NodePos,f=e.NodeView,E=e.PasteRule,R=e.Tracker,A=e.callOrReturn,P=e.canInsertNode,T=e.combineTransactionSteps,v=e.createChainableState,C=e.createDocument,S=e.createNodeFromContent,w=e.createStyleTag,N=e.defaultBlockAt,O=e.deleteProps,B=e.elementFromString,M=e.escapeForRegEx,I=e.extensions,F=e.findChildren,D=e.findChildrenInRange,j=e.findDuplicates,H=e.findParentNode,L=e.findParentNodeClosestToPos,K=e.fromString,q=e.generateHTML,G=e.generateJSON,$=e.generateText,z=e.getAttributes,J=e.getAttributesFromExtensions,U=e.getChangedRanges,V=e.getDebugJSON,Y=e.getExtensionField,Q=e.getHTMLFromFragment,W=e.getMarkAttributes,X=e.getMarkRange,Z=e.getMarkType,ee=e.getMarksBetween,te=e.getNodeAtPosition,ne=e.getNodeAttributes,oe=e.getNodeType,ie=e.getRenderedAttributes,re=e.getSchema,se=e.getSchemaByResolvedExtensions,le=e.getSchemaTypeByName,ae=e.getSchemaTypeNameByName,de=e.getSplittedAttributes,pe=e.getText,ue=e.getTextBetween,ce=e.getTextContentFromNodes,ge=e.getTextSerializersFromSchema,_e=e.injectExtensionAttributesToParseRule,xe=e.inputRulesPlugin,me=e.isActive,ke=e.isAtEndOfNode,be=e.isAtStartOfNode,ye=e.isEmptyObject,he=e.isExtensionRulesEnabled,fe=e.isFunction,Ee=e.isList,Re=e.isMacOS,Ae=e.isMarkActive,Pe=e.isNodeActive,Te=e.isNodeEmpty,ve=e.isNodeSelection,Ce=e.isNumber,Se=e.isPlainObject,we=e.isRegExp,Ne=e.isSafari,Oe=e.isString,Be=e.isTextSelection,Me=e.isiOS,Ie=e.markInputRule,Fe=e.markPasteRule,s=e.mergeAttributes,De=e.mergeDeep,je=e.minMax,He=e.nodeInputRule,Le=e.nodePasteRule,Ke=e.objectIncludes,qe=e.pasteRulesPlugin,Ge=e.posToDOMRect,$e=e.removeDuplicates,ze=e.resolveFocusPosition,Je=e.rewriteUnknownContent,Ue=e.selectionToInsertionEnd,Ve=e.splitExtensions,Ye=e.textInputRule,Qe=e.textPasteRule,l=e.textblockTypeInputRule,We=e.wrappingInputRule;var a=r.create({name:"heading",addOptions(){return{levels:[1,2,3,4,5,6],HTMLAttributes:{}}},content:"inline*",group:"block",defining:!0,addAttributes(){return{level:{default:1,rendered:!1}}},parseHTML(){return this.options.levels.map(t=>({tag:`h${t}`,attrs:{level:t}}))},renderHTML({node:t,HTMLAttributes:n}){return[`h${this.options.levels.includes(t.attrs.level)?t.attrs.level:this.options.levels[0]}`,s(this.options.HTMLAttributes,n),0]},addCommands(){return{setHeading:t=>({commands:n})=>this.options.levels.includes(t.level)?n.setNode(this.name,t):!1,toggleHeading:t=>({commands:n})=>this.options.levels.includes(t.level)?n.toggleNode(this.name,"paragraph",t):!1}},addKeyboardShortcuts(){return this.options.levels.reduce((t,n)=>({...t,[`Mod-Alt-${n}`]:()=>this.editor.commands.toggleHeading({level:n})}),{})},addInputRules(){return this.options.levels.map(t=>l({find:new RegExp(`^(#{${Math.min(...this.options.levels)},${t}})\\s$`),type:this.type,getAttributes:{level:t}}))}});var d="__LEPTOS_TIPTAP_BRIDGE__";function c(){let t=globalThis,n=t[d];if(n!=null)return n;let o={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return t[d]=o,o}function p(){return c()}function u(t){p().registerExtension(t)}var g={name:"heading",create:()=>a.extend({addAttributes(){return{...this.parent?.(),collapsed:{default:null,parseHTML:t=>{let n=t.getAttribute("data-collapsed");return n==="true"?!0:n==="false"?!1:null},renderHTML:t=>t.collapsed===!0?{"data-collapsed":"true"}:{}}}}}),commands:{set_heading:(t,n)=>n.kind==="set_heading"?t.chain().focus().setHeading({level:n.level,...n.collapsed==null?{}:{collapsed:n.collapsed}}).run():!1,toggle_heading:(t,n)=>n.kind==="toggle_heading"?t.chain().focus().toggleHeading({level:n.level,...n.collapsed==null?{}:{collapsed:n.collapsed}}).run():!1},selection_keys:["h1","h2","h3","h4","h5","h6"],selection_state:t=>({h1:t.isActive("heading",{level:1}),h2:t.isActive("heading",{level:2}),h3:t.isActive("heading",{level:3}),h4:t.isActive("heading",{level:4}),h5:t.isActive("heading",{level:5}),h6:t.isActive("heading",{level:6})})};function st(){u(g)}export{st as register_heading};
var i=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(i==null||i.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var e=i.modules["@tiptap/core"];if(e==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var m=e.CommandManager,k=e.Editor,b=e.Extension,y=e.InputRule,h=e.Mark,r=e.Node,f=e.NodePos,E=e.NodeView,R=e.PasteRule,A=e.Tracker,T=e.callOrReturn,P=e.canInsertNode,v=e.combineTransactionSteps,C=e.createChainableState,S=e.createDocument,w=e.createNodeFromContent,N=e.createStyleTag,O=e.defaultBlockAt,B=e.deleteProps,M=e.elementFromString,I=e.escapeForRegEx,F=e.extensions,D=e.findChildren,H=e.findChildrenInRange,L=e.findDuplicates,j=e.findParentNode,K=e.findParentNodeClosestToPos,q=e.fromString,G=e.generateHTML,$=e.generateJSON,z=e.generateText,J=e.getAttributes,U=e.getAttributesFromExtensions,V=e.getChangedRanges,Y=e.getDebugJSON,Q=e.getExtensionField,W=e.getHTMLFromFragment,X=e.getMarkAttributes,Z=e.getMarkRange,ee=e.getMarkType,te=e.getMarksBetween,ne=e.getNodeAtPosition,oe=e.getNodeAttributes,ie=e.getNodeType,re=e.getRenderedAttributes,se=e.getSchema,le=e.getSchemaByResolvedExtensions,ae=e.getSchemaTypeByName,de=e.getSchemaTypeNameByName,pe=e.getSplittedAttributes,ue=e.getText,ce=e.getTextBetween,ge=e.getTextContentFromNodes,_e=e.getTextSerializersFromSchema,xe=e.injectExtensionAttributesToParseRule,me=e.inputRulesPlugin,ke=e.isActive,be=e.isAtEndOfNode,ye=e.isAtStartOfNode,he=e.isEmptyObject,fe=e.isExtensionRulesEnabled,Ee=e.isFunction,Re=e.isList,Ae=e.isMacOS,Te=e.isMarkActive,Pe=e.isNodeActive,ve=e.isNodeEmpty,Ce=e.isNodeSelection,Se=e.isNumber,we=e.isPlainObject,Ne=e.isRegExp,Oe=e.isSafari,Be=e.isString,Me=e.isTextSelection,Ie=e.isiOS,Fe=e.markInputRule,De=e.markPasteRule,s=e.mergeAttributes,He=e.mergeDeep,Le=e.minMax,je=e.nodeInputRule,Ke=e.nodePasteRule,qe=e.objectIncludes,Ge=e.pasteRulesPlugin,$e=e.posToDOMRect,ze=e.removeDuplicates,Je=e.resolveFocusPosition,Ue=e.rewriteUnknownContent,Ve=e.selectionToInsertionEnd,Ye=e.splitExtensions,Qe=e.textInputRule,We=e.textPasteRule,l=e.textblockTypeInputRule,Xe=e.wrappingInputRule;var a=r.create({name:"heading",addOptions(){return{levels:[1,2,3,4,5,6],HTMLAttributes:{}}},content:"inline*",group:"block",defining:!0,addAttributes(){return{level:{default:1,rendered:!1}}},parseHTML(){return this.options.levels.map(t=>({tag:`h${t}`,attrs:{level:t}}))},renderHTML({node:t,HTMLAttributes:n}){return[`h${this.options.levels.includes(t.attrs.level)?t.attrs.level:this.options.levels[0]}`,s(this.options.HTMLAttributes,n),0]},addCommands(){return{setHeading:t=>({commands:n})=>this.options.levels.includes(t.level)?n.setNode(this.name,t):!1,toggleHeading:t=>({commands:n})=>this.options.levels.includes(t.level)?n.toggleNode(this.name,"paragraph",t):!1}},addKeyboardShortcuts(){return this.options.levels.reduce((t,n)=>({...t,[`Mod-Alt-${n}`]:()=>this.editor.commands.toggleHeading({level:n})}),{})},addInputRules(){return this.options.levels.map(t=>l({find:new RegExp(`^(#{${Math.min(...this.options.levels)},${t}})\\s$`),type:this.type,getAttributes:{level:t}}))}});var d="__LEPTOS_TIPTAP_BRIDGE__";function c(){let t=globalThis,n=t[d];if(n!=null)return n;let o={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return t[d]=o,o}function p(){return c()}function u(t){p().registerExtension(t)}function g(t){return typeof t=="string"&&t.length>0?{"data-block-id":t,id:t}:{}}var _={name:"heading",create:()=>a.extend({addAttributes(){return{...this.parent?.(),blockId:{default:null,parseHTML:t=>t.getAttribute("data-block-id"),renderHTML:t=>g(t.blockId)},collapsed:{default:null,parseHTML:t=>{let n=t.getAttribute("data-collapsed");return n==="true"?!0:n==="false"?!1:null},renderHTML:t=>t.collapsed===!0?{"data-collapsed":"true"}:{}}}}}),commands:{set_heading:(t,n)=>n.kind==="set_heading"?t.chain().focus().setHeading({level:n.level,...n.collapsed==null?{}:{collapsed:n.collapsed}}).run():!1,toggle_heading:(t,n)=>n.kind==="toggle_heading"?t.chain().focus().toggleHeading({level:n.level,...n.collapsed==null?{}:{collapsed:n.collapsed}}).run():!1},selection_keys:["h1","h2","h3","h4","h5","h6"],selection_state:t=>({h1:t.isActive("heading",{level:1}),h2:t.isActive("heading",{level:2}),h3:t.isActive("heading",{level:3}),h4:t.isActive("heading",{level:4}),h5:t.isActive("heading",{level:5}),h6:t.isActive("heading",{level:6})})};function lt(){u(_)}export{lt as register_heading};
@@ -1 +1 @@
var i=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(i==null||i.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var t=i.modules["@tiptap/core"];if(t==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var b=t.CommandManager,y=t.Editor,f=t.Extension,h=t.InputRule,E=t.Mark,r=t.Node,R=t.NodePos,P=t.NodeView,T=t.PasteRule,A=t.Tracker,C=t.callOrReturn,S=t.canInsertNode,w=t.combineTransactionSteps,N=t.createChainableState,O=t.createDocument,B=t.createNodeFromContent,I=t.createStyleTag,M=t.defaultBlockAt,v=t.deleteProps,F=t.elementFromString,D=t.escapeForRegEx,j=t.extensions,L=t.findChildren,H=t.findChildrenInRange,K=t.findDuplicates,q=t.findParentNode,G=t.findParentNodeClosestToPos,z=t.fromString,J=t.generateHTML,U=t.generateJSON,V=t.generateText,Y=t.getAttributes,$=t.getAttributesFromExtensions,Q=t.getChangedRanges,W=t.getDebugJSON,X=t.getExtensionField,Z=t.getHTMLFromFragment,tt=t.getMarkAttributes,et=t.getMarkRange,nt=t.getMarkType,ot=t.getMarksBetween,it=t.getNodeAtPosition,rt=t.getNodeAttributes,st=t.getNodeType,lt=t.getRenderedAttributes,at=t.getSchema,dt=t.getSchemaByResolvedExtensions,pt=t.getSchemaTypeByName,ut=t.getSchemaTypeNameByName,ct=t.getSplittedAttributes,gt=t.getText,_t=t.getTextBetween,mt=t.getTextContentFromNodes,xt=t.getTextSerializersFromSchema,kt=t.injectExtensionAttributesToParseRule,bt=t.inputRulesPlugin,yt=t.isActive,ft=t.isAtEndOfNode,ht=t.isAtStartOfNode,Et=t.isEmptyObject,Rt=t.isExtensionRulesEnabled,Pt=t.isFunction,Tt=t.isList,At=t.isMacOS,Ct=t.isMarkActive,St=t.isNodeActive,wt=t.isNodeEmpty,Nt=t.isNodeSelection,Ot=t.isNumber,Bt=t.isPlainObject,It=t.isRegExp,Mt=t.isSafari,vt=t.isString,Ft=t.isTextSelection,Dt=t.isiOS,jt=t.markInputRule,Lt=t.markPasteRule,s=t.mergeAttributes,Ht=t.mergeDeep,Kt=t.minMax,l=t.nodeInputRule,qt=t.nodePasteRule,Gt=t.objectIncludes,zt=t.pasteRulesPlugin,Jt=t.posToDOMRect,Ut=t.removeDuplicates,Vt=t.resolveFocusPosition,Yt=t.rewriteUnknownContent,$t=t.selectionToInsertionEnd,Qt=t.splitExtensions,Wt=t.textInputRule,Xt=t.textPasteRule,Zt=t.textblockTypeInputRule,te=t.wrappingInputRule;var g=/(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/,a=r.create({name:"image",addOptions(){return{inline:!1,allowBase64:!1,HTMLAttributes:{}}},inline(){return this.options.inline},group(){return this.options.inline?"inline":"block"},draggable:!0,addAttributes(){return{src:{default:null},alt:{default:null},title:{default:null}}},parseHTML(){return[{tag:this.options.allowBase64?"img[src]":'img[src]:not([src^="data:"])'}]},renderHTML({HTMLAttributes:e}){return["img",s(this.options.HTMLAttributes,e)]},addCommands(){return{setImage:e=>({commands:n})=>n.insertContent({type:this.name,attrs:e})}},addInputRules(){return[l({find:g,type:this.type,getAttributes:e=>{let[,,n,o,c]=e;return{src:o,alt:n,title:c}}})]}});var d="__LEPTOS_TIPTAP_BRIDGE__";function _(){let e=globalThis,n=e[d];if(n!=null)return n;let o={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return e[d]=o,o}function p(){return _()}function u(e){p().registerExtension(e)}function m(e){return{src:e.src,alt:e.alt??void 0,title:e.title??void 0}}var x=a.extend({addAttributes(){return{...this.parent?.(),"data-align":{default:null,parseHTML:e=>e.getAttribute("data-align"),renderHTML:e=>{let n=e["data-align"];return typeof n=="string"&&n.length>0?{"data-align":n}:{}}}}}}),k={name:"image",create:()=>x,commands:{set_image:(e,n)=>n.kind==="set_image"?e.chain().focus().setImage(m(n)).run():!1}};function de(){u(k)}export{de as register_image};
var i=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(i==null||i.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var t=i.modules["@tiptap/core"];if(t==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var y=t.CommandManager,f=t.Editor,h=t.Extension,E=t.InputRule,R=t.Mark,r=t.Node,T=t.NodePos,A=t.NodeView,P=t.PasteRule,C=t.Tracker,S=t.callOrReturn,w=t.canInsertNode,N=t.combineTransactionSteps,B=t.createChainableState,O=t.createDocument,M=t.createNodeFromContent,I=t.createStyleTag,v=t.defaultBlockAt,F=t.deleteProps,D=t.elementFromString,L=t.escapeForRegEx,j=t.extensions,H=t.findChildren,K=t.findChildrenInRange,q=t.findDuplicates,G=t.findParentNode,z=t.findParentNodeClosestToPos,J=t.fromString,U=t.generateHTML,V=t.generateJSON,Y=t.generateText,$=t.getAttributes,Q=t.getAttributesFromExtensions,W=t.getChangedRanges,X=t.getDebugJSON,Z=t.getExtensionField,tt=t.getHTMLFromFragment,et=t.getMarkAttributes,nt=t.getMarkRange,ot=t.getMarkType,it=t.getMarksBetween,rt=t.getNodeAtPosition,st=t.getNodeAttributes,lt=t.getNodeType,at=t.getRenderedAttributes,dt=t.getSchema,pt=t.getSchemaByResolvedExtensions,ut=t.getSchemaTypeByName,ct=t.getSchemaTypeNameByName,gt=t.getSplittedAttributes,_t=t.getText,mt=t.getTextBetween,xt=t.getTextContentFromNodes,kt=t.getTextSerializersFromSchema,bt=t.injectExtensionAttributesToParseRule,yt=t.inputRulesPlugin,ft=t.isActive,ht=t.isAtEndOfNode,Et=t.isAtStartOfNode,Rt=t.isEmptyObject,Tt=t.isExtensionRulesEnabled,At=t.isFunction,Pt=t.isList,Ct=t.isMacOS,St=t.isMarkActive,wt=t.isNodeActive,Nt=t.isNodeEmpty,Bt=t.isNodeSelection,Ot=t.isNumber,Mt=t.isPlainObject,It=t.isRegExp,vt=t.isSafari,Ft=t.isString,Dt=t.isTextSelection,Lt=t.isiOS,jt=t.markInputRule,Ht=t.markPasteRule,s=t.mergeAttributes,Kt=t.mergeDeep,qt=t.minMax,l=t.nodeInputRule,Gt=t.nodePasteRule,zt=t.objectIncludes,Jt=t.pasteRulesPlugin,Ut=t.posToDOMRect,Vt=t.removeDuplicates,Yt=t.resolveFocusPosition,$t=t.rewriteUnknownContent,Qt=t.selectionToInsertionEnd,Wt=t.splitExtensions,Xt=t.textInputRule,Zt=t.textPasteRule,te=t.textblockTypeInputRule,ee=t.wrappingInputRule;var g=/(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/,a=r.create({name:"image",addOptions(){return{inline:!1,allowBase64:!1,HTMLAttributes:{}}},inline(){return this.options.inline},group(){return this.options.inline?"inline":"block"},draggable:!0,addAttributes(){return{src:{default:null},alt:{default:null},title:{default:null}}},parseHTML(){return[{tag:this.options.allowBase64?"img[src]":'img[src]:not([src^="data:"])'}]},renderHTML({HTMLAttributes:e}){return["img",s(this.options.HTMLAttributes,e)]},addCommands(){return{setImage:e=>({commands:n})=>n.insertContent({type:this.name,attrs:e})}},addInputRules(){return[l({find:g,type:this.type,getAttributes:e=>{let[,,n,o,c]=e;return{src:o,alt:n,title:c}}})]}});var d="__LEPTOS_TIPTAP_BRIDGE__";function _(){let e=globalThis,n=e[d];if(n!=null)return n;let o={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return e[d]=o,o}function p(){return _()}function u(e){p().registerExtension(e)}function m(e){return typeof e=="string"&&e.length>0?{"data-block-id":e,id:e}:{}}function x(e){return{src:e.src,alt:e.alt??void 0,title:e.title??void 0}}var k=a.extend({addAttributes(){return{...this.parent?.(),blockId:{default:null,parseHTML:e=>e.getAttribute("data-block-id"),renderHTML:e=>m(e.blockId)},"data-align":{default:null,parseHTML:e=>e.getAttribute("data-align"),renderHTML:e=>{let n=e["data-align"];return typeof n=="string"&&n.length>0?{"data-align":n}:{}}}}}}),b={name:"image",create:()=>k,commands:{set_image:(e,n)=>n.kind==="set_image"?e.chain().focus().setImage(x(n)).run():!1}};function pe(){u(b)}export{pe as register_image};
@@ -1 +1 @@
var n=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(n==null||n.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var e=n.modules["@tiptap/core"];if(e==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var g=e.CommandManager,_=e.Editor,x=e.Extension,m=e.InputRule,k=e.Mark,i=e.Node,b=e.NodePos,y=e.NodeView,h=e.PasteRule,E=e.Tracker,f=e.callOrReturn,P=e.canInsertNode,R=e.combineTransactionSteps,C=e.createChainableState,T=e.createDocument,A=e.createNodeFromContent,S=e.createStyleTag,w=e.defaultBlockAt,N=e.deleteProps,O=e.elementFromString,B=e.escapeForRegEx,v=e.extensions,M=e.findChildren,I=e.findChildrenInRange,F=e.findDuplicates,D=e.findParentNode,j=e.findParentNodeClosestToPos,L=e.fromString,K=e.generateHTML,H=e.generateJSON,q=e.generateText,G=e.getAttributes,z=e.getAttributesFromExtensions,J=e.getChangedRanges,U=e.getDebugJSON,V=e.getExtensionField,Y=e.getHTMLFromFragment,Q=e.getMarkAttributes,W=e.getMarkRange,X=e.getMarkType,Z=e.getMarksBetween,$=e.getNodeAtPosition,ee=e.getNodeAttributes,te=e.getNodeType,ne=e.getRenderedAttributes,oe=e.getSchema,re=e.getSchemaByResolvedExtensions,ie=e.getSchemaTypeByName,se=e.getSchemaTypeNameByName,ae=e.getSplittedAttributes,le=e.getText,de=e.getTextBetween,pe=e.getTextContentFromNodes,ce=e.getTextSerializersFromSchema,ue=e.injectExtensionAttributesToParseRule,ge=e.inputRulesPlugin,_e=e.isActive,xe=e.isAtEndOfNode,me=e.isAtStartOfNode,ke=e.isEmptyObject,be=e.isExtensionRulesEnabled,ye=e.isFunction,he=e.isList,Ee=e.isMacOS,fe=e.isMarkActive,Pe=e.isNodeActive,Re=e.isNodeEmpty,Ce=e.isNodeSelection,Te=e.isNumber,Ae=e.isPlainObject,Se=e.isRegExp,we=e.isSafari,Ne=e.isString,Oe=e.isTextSelection,Be=e.isiOS,ve=e.markInputRule,Me=e.markPasteRule,s=e.mergeAttributes,Ie=e.mergeDeep,Fe=e.minMax,De=e.nodeInputRule,je=e.nodePasteRule,Le=e.objectIncludes,Ke=e.pasteRulesPlugin,He=e.posToDOMRect,qe=e.removeDuplicates,Ge=e.resolveFocusPosition,ze=e.rewriteUnknownContent,Je=e.selectionToInsertionEnd,Ue=e.splitExtensions,Ve=e.textInputRule,Ye=e.textPasteRule,Qe=e.textblockTypeInputRule,We=e.wrappingInputRule;var a=i.create({name:"paragraph",priority:1e3,addOptions(){return{HTMLAttributes:{}}},group:"block",content:"inline*",parseHTML(){return[{tag:"p"}]},renderHTML({HTMLAttributes:t}){return["p",s(this.options.HTMLAttributes,t),0]},addCommands(){return{setParagraph:()=>({commands:t})=>t.setNode(this.name)}},addKeyboardShortcuts(){return{"Mod-Alt-0":()=>this.editor.commands.setParagraph()}}});var l="__LEPTOS_TIPTAP_BRIDGE__";function c(){let t=globalThis,o=t[l];if(o!=null)return o;let r={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return t[l]=r,r}function d(){return c()}function p(t){d().registerExtension(t)}var u={name:"paragraph",create:()=>a,commands:{set_paragraph:t=>t.chain().focus().setParagraph().run()},selection_keys:["paragraph"],selection_state:t=>({paragraph:t.isActive("paragraph")})};function it(){p(u)}export{it as register_paragraph};
var n=globalThis.__LEPTOS_TIPTAP_BRIDGE__;if(n==null||n.modules==null)throw new Error("leptos-tiptap bridge bindings are unavailable");var t=n.modules["@tiptap/core"];if(t==null)throw new Error('leptos-tiptap bridge module "@tiptap/core" is unavailable');var x=t.CommandManager,m=t.Editor,k=t.Extension,b=t.InputRule,y=t.Mark,i=t.Node,h=t.NodePos,E=t.NodeView,f=t.PasteRule,R=t.Tracker,P=t.callOrReturn,A=t.canInsertNode,T=t.combineTransactionSteps,C=t.createChainableState,S=t.createDocument,w=t.createNodeFromContent,N=t.createStyleTag,O=t.defaultBlockAt,B=t.deleteProps,M=t.elementFromString,v=t.escapeForRegEx,F=t.extensions,I=t.findChildren,D=t.findChildrenInRange,j=t.findDuplicates,L=t.findParentNode,H=t.findParentNodeClosestToPos,K=t.fromString,q=t.generateHTML,G=t.generateJSON,z=t.generateText,J=t.getAttributes,U=t.getAttributesFromExtensions,V=t.getChangedRanges,Y=t.getDebugJSON,Q=t.getExtensionField,W=t.getHTMLFromFragment,X=t.getMarkAttributes,Z=t.getMarkRange,$=t.getMarkType,tt=t.getMarksBetween,et=t.getNodeAtPosition,nt=t.getNodeAttributes,ot=t.getNodeType,rt=t.getRenderedAttributes,it=t.getSchema,st=t.getSchemaByResolvedExtensions,at=t.getSchemaTypeByName,lt=t.getSchemaTypeNameByName,dt=t.getSplittedAttributes,pt=t.getText,ct=t.getTextBetween,ut=t.getTextContentFromNodes,gt=t.getTextSerializersFromSchema,_t=t.injectExtensionAttributesToParseRule,xt=t.inputRulesPlugin,mt=t.isActive,kt=t.isAtEndOfNode,bt=t.isAtStartOfNode,yt=t.isEmptyObject,ht=t.isExtensionRulesEnabled,Et=t.isFunction,ft=t.isList,Rt=t.isMacOS,Pt=t.isMarkActive,At=t.isNodeActive,Tt=t.isNodeEmpty,Ct=t.isNodeSelection,St=t.isNumber,wt=t.isPlainObject,Nt=t.isRegExp,Ot=t.isSafari,Bt=t.isString,Mt=t.isTextSelection,vt=t.isiOS,Ft=t.markInputRule,It=t.markPasteRule,s=t.mergeAttributes,Dt=t.mergeDeep,jt=t.minMax,Lt=t.nodeInputRule,Ht=t.nodePasteRule,Kt=t.objectIncludes,qt=t.pasteRulesPlugin,Gt=t.posToDOMRect,zt=t.removeDuplicates,Jt=t.resolveFocusPosition,Ut=t.rewriteUnknownContent,Vt=t.selectionToInsertionEnd,Yt=t.splitExtensions,Qt=t.textInputRule,Wt=t.textPasteRule,Xt=t.textblockTypeInputRule,Zt=t.wrappingInputRule;var a=i.create({name:"paragraph",priority:1e3,addOptions(){return{HTMLAttributes:{}}},group:"block",content:"inline*",parseHTML(){return[{tag:"p"}]},renderHTML({HTMLAttributes:e}){return["p",s(this.options.HTMLAttributes,e),0]},addCommands(){return{setParagraph:()=>({commands:e})=>e.setNode(this.name)}},addKeyboardShortcuts(){return{"Mod-Alt-0":()=>this.editor.commands.setParagraph()}}});var l="__LEPTOS_TIPTAP_BRIDGE__";function c(){let e=globalThis,o=e[l];if(o!=null)return o;let r={modules:{},registerExtension:()=>{throw new Error("leptos-tiptap bridge runtime is not initialized")}};return e[l]=r,r}function d(){return c()}function p(e){d().registerExtension(e)}function u(e){return typeof e=="string"&&e.length>0?{"data-block-id":e,id:e}:{}}var g=a.extend({addAttributes(){return{...this.parent?.(),blockId:{default:null,parseHTML:e=>e.getAttribute("data-block-id"),renderHTML:e=>u(e.blockId)}}}}),_={name:"paragraph",create:()=>g,commands:{set_paragraph:e=>e.chain().focus().setParagraph().run()},selection_keys:["paragraph"],selection_state:e=>({paragraph:e.isActive("paragraph")})};function ae(){p(_)}export{ae as register_paragraph};
@@ -1,4 +1,21 @@
export function write_mnote_text_to_clipboard(text) {
try {
if (navigator?.clipboard?.writeText) {
return navigator.clipboard.writeText(String(text)).then(() => true, () => false);
}
} catch (_error) {}
return Promise.resolve(false);
}
export function find_mnote_block_anchor(blockId) {
const wanted = String(blockId || '');
if (!wanted) return null;
const root = document.querySelector('.editor-surface .ProseMirror');
if (!root) return null;
return Array.from(root.querySelectorAll('[data-block-id]')).find((node) => node.getAttribute('data-block-id') === wanted) || null;
}
export function select_mnote_image_node(image) {
try {
const root = image?.closest?.('.ProseMirror');
@@ -56,3 +73,26 @@ export function download_mnote_selected_image() {
return false;
}
}
export function bump_mnote_e27_ai_bridge_request_count() {
try {
const key = '__MNOTE_E27_AI_BRIDGE_REQUEST_COUNT__';
window[key] = Number(window[key] || 0) + 1;
return window[key];
} catch (_error) {
return 0;
}
}
export async function post_mnote_document_save(body) {
const response = await fetch('/api/documents/save', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: String(body || '{}'),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`document save failed: HTTP ${response.status}; ${text.slice(0, 1200)}`);
}
return text || '{}';
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
{"rustc_fingerprint":9228011546279038255,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/lix/.rustup/toolchains/1.89.0-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.89.0 (29483883e 2025-08-04)\nbinary: rustc\ncommit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2\ncommit-date: 2025-08-04\nhost: x86_64-unknown-linux-gnu\nrelease: 1.89.0\nLLVM version: 20.1.7\n","stderr":""},"11652014622397750202":{"success":true,"status":"","code":0,"stdout":"___.wasm\nlib___.rlib\n___.wasm\nlib___.a\n/home/lix/.rustup/toolchains/1.89.0-x86_64-unknown-linux-gnu\noff\n___\ndebug_assertions\npanic=\"abort\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"wasm32\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"wasm\"\ntarget_feature=\"bulk-memory\"\ntarget_feature=\"multivalue\"\ntarget_feature=\"mutable-globals\"\ntarget_feature=\"nontrapping-fptoint\"\ntarget_feature=\"reference-types\"\ntarget_feature=\"sign-ext\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"unknown\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"unknown\"\n","stderr":"warning: dropping unsupported crate type `dylib` for target `wasm32-unknown-unknown`\n\nwarning: dropping unsupported crate type `proc-macro` for target `wasm32-unknown-unknown`\n\nwarning: 2 warnings emitted\n\n"}},"successes":{}}
{"rustc_fingerprint":9228011546279038255,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.89.0 (29483883e 2025-08-04)\nbinary: rustc\ncommit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2\ncommit-date: 2025-08-04\nhost: x86_64-unknown-linux-gnu\nrelease: 1.89.0\nLLVM version: 20.1.7\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/lix/.rustup/toolchains/1.89.0-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}