From c588867625d25f58484fca44b67a45865a6d12d2 Mon Sep 17 00:00:00 2001 From: default Date: Wed, 8 Apr 2026 14:26:50 +0000 Subject: [PATCH] Added Word Count --- README.md | 13 +++- server/src/compiler.rs | 101 +++++++++++++++++++++++----- server/src/db.rs | 2 +- server/src/handlers.rs | 9 ++- src/lib/components/DocFooter.svelte | 60 +++++++++++++++++ src/lib/components/Toolbar.svelte | 10 +-- src/lib/ts/store.ts | 1 + src/lib/ts/typst-api.ts | 1 + src/routes/doc/[id]/+page.svelte | 8 ++- 9 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 src/lib/components/DocFooter.svelte diff --git a/README.md b/README.md index cff368e..228dd77 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ TypstDrive is completely self-hostable. We provide a Docker image that packages - [Docker](https://docs.docker.com/get-docker/) - [Docker Compose](https://docs.docker.com/compose/install/) +- [Tinymist](https://github.com/Myriad-Dreamin/tinymist) (Required if running the backend locally for Language Server features) ### Getting Started @@ -94,9 +95,15 @@ git clone https://github.com/typst/typst.git typst 2. Run the dev server: `npm run dev` ### Backend -1. Start the local database: `docker-compose up -d db` -2. Navigate to the `server/` directory. -3. Build and run: `cargo run` +1. Ensure you have the required dependencies installed (e.g., `libssl-dev` on Ubuntu: `sudo apt-get install libssl-dev`). +2. Install the `tinymist` CLI and ensure it is in your system's PATH, as the backend relies on it for Language Server Protocol (LSP) functionality. + (e.g., via `cargo binstall tinymist` or downloading from [releases](https://github.com/Myriad-Dreamin/tinymist/releases)). Example for Linux x64: + ```bash + curl -L -o ~/.cargo/bin/tinymist https://github.com/Myriad-Dreamin/tinymist/releases/latest/download/tinymist-linux-x64 && chmod +x ~/.cargo/bin/tinymist + ``` +3. Start the local database: `docker-compose up -d db` +4. Navigate to the `server/` directory. +5. Build and run: `cargo run` Note: The frontend expects the backend to be running on port 3000. During local development via Vite, API calls are proxied automatically. diff --git a/server/src/compiler.rs b/server/src/compiler.rs index 4f4f85c..9f51849 100644 --- a/server/src/compiler.rs +++ b/server/src/compiler.rs @@ -1,10 +1,54 @@ use crate::world::MemoryWorld; +use serde::Serialize; use std::collections::HashMap; use typst::diag::{SourceDiagnostic, Warned}; +use typst::layout::{Frame, FrameItem}; use typst_layout::PagedDocument; use typst_pdf::{pdf, PdfOptions}; use typst_render::render; +#[derive(Serialize, Clone)] +pub struct DocumentStats { + pub pages: usize, + pub words: usize, + pub characters: usize, + pub characters_excluding_spaces: usize, +} + +fn extract_stats(doc: &PagedDocument) -> DocumentStats { + let mut text = String::new(); + let pages = doc.pages().len(); + for page in doc.pages() { + extract_frame_text(&page.frame, &mut text); + } + + let words = text.split_whitespace().count(); + let characters = text.chars().count(); + let characters_excluding_spaces = text.chars().filter(|c| !c.is_whitespace()).count(); + + DocumentStats { + pages, + words, + characters, + characters_excluding_spaces, + } +} + +fn extract_frame_text(frame: &Frame, text: &mut String) { + for (_, item) in frame.items() { + match item { + FrameItem::Text(text_item) => { + text.push_str(&text_item.text); + text.push(' '); + } + FrameItem::Group(group) => { + extract_frame_text(&group.frame, text); + } + _ => {} + } + } +} + pub struct TypstCompiler; impl TypstCompiler { @@ -16,30 +60,41 @@ impl TypstCompiler { &self, text: String, files: HashMap>, - ) -> Result<(Vec, String), Vec<(SourceDiagnostic, Option>)>> { + ) -> Result< + (Vec, String, DocumentStats), + Vec<(SourceDiagnostic, Option>)>, + > { let world = MemoryWorld::new(text, files); match typst::compile::(&world) { Warned { output: Ok(doc), warnings: _, } => { + let stats = extract_stats(&doc); let svgs = doc.pages().iter().map(typst_svg::svg).collect(); let thumbnail = if let Some(page) = doc.pages().first() { typst_svg::svg(page) } else { String::new() }; - Ok((svgs, thumbnail)) + Ok((svgs, thumbnail, stats)) } Warned { output: Err(errors), warnings: _, } => { use typst::World; - let diag = errors.into_iter().map(|d| { - let range = d.span.id().and_then(|id| world.source(id).ok()).and_then(|s| s.range(d.span)); - (d, range) - }).collect(); + let diag = errors + .into_iter() + .map(|d| { + let range = d + .span + .id() + .and_then(|id| world.source(id).ok()) + .and_then(|s| s.range(d.span)); + (d, range) + }) + .collect(); Err(diag) } } @@ -67,11 +122,18 @@ impl TypstCompiler { warnings: _, } => { use typst::World; - Err(errors.into_iter().map(|d| { - let range = d.span.id().and_then(|id| world.source(id).ok()).and_then(|s| s.range(d.span)); - (d, range) - }).collect()) - }, + Err(errors + .into_iter() + .map(|d| { + let range = d + .span + .id() + .and_then(|id| world.source(id).ok()) + .and_then(|s| s.range(d.span)); + (d, range) + }) + .collect()) + } } } @@ -99,11 +161,18 @@ impl TypstCompiler { warnings: _, } => { use typst::World; - Err(errors.into_iter().map(|d| { - let range = d.span.id().and_then(|id| world.source(id).ok()).and_then(|s| s.range(d.span)); - (d, range) - }).collect()) - }, + Err(errors + .into_iter() + .map(|d| { + let range = d + .span + .id() + .and_then(|id| world.source(id).ok()) + .and_then(|s| s.range(d.span)); + (d, range) + }) + .collect()) + } } } } diff --git a/server/src/db.rs b/server/src/db.rs index d325a28..509a59d 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -3,7 +3,7 @@ use sqlx::{Pool, Postgres}; pub async fn init_db() -> Pool { let db_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/typstdrive".to_string()); + .unwrap_or_else(|_| "postgres://postgres:password@192.168.1.214:5432/typstdrive".to_string()); let pool = PgPoolOptions::new() .max_connections(5) diff --git a/server/src/handlers.rs b/server/src/handlers.rs index 02ad2e8..b6e8451 100644 --- a/server/src/handlers.rs +++ b/server/src/handlers.rs @@ -54,10 +54,13 @@ pub struct CompileRequest { pub document_id: Option, } +use crate::compiler::DocumentStats; + #[derive(Serialize)] pub struct CompileResponse { pub svgs: Option>, pub errors: Option>, + pub stats: Option, } #[derive(Serialize)] @@ -215,7 +218,7 @@ pub async fn compile_handler( let compiler = state.compiler.lock().await; match compiler.compile_svg(payload.text, files_map) { - Ok((svgs, thumbnail)) => { + Ok((svgs, thumbnail, stats)) => { if let Some(doc_id) = &payload.document_id { if can_save_thumbnail { let _ = sqlx::query("UPDATE documents SET thumbnail_svg = $1 WHERE id = $2") @@ -229,6 +232,7 @@ pub async fn compile_handler( Json(CompileResponse { svgs: Some(svgs), errors: None, + stats: Some(stats), }) } Err(diags) => { @@ -244,6 +248,7 @@ pub async fn compile_handler( Json(CompileResponse { svgs: None, errors: Some(errors), + stats: None, }) } } @@ -318,7 +323,7 @@ pub async fn export_handler( Err(_) => (StatusCode::BAD_REQUEST, "Compilation failed").into_response(), }, "svg" => match compiler.compile_svg(payload.text, files_map.clone()) { - Ok((svgs, _)) => { + Ok((svgs, _, _)) => { let mut combined = String::new(); diff --git a/src/lib/components/DocFooter.svelte b/src/lib/components/DocFooter.svelte new file mode 100644 index 0000000..53fffae --- /dev/null +++ b/src/lib/components/DocFooter.svelte @@ -0,0 +1,60 @@ + + +
+
+
+
+ {$connectionStatus === 'connected' ? 'Document synced' : 'Connecting...'} +
+ + {#if $documentStatsStore} + + {/if} +
+
+ +{#if showStatsModal} + + +
+
e.stopPropagation()}> +
+

Word count

+ +
+
+
+ Pages + {$documentStatsStore?.pages || 0} +
+
+ Words + {$documentStatsStore?.words || 0} +
+
+ Characters + {$documentStatsStore?.characters || 0} +
+
+ Characters excluding spaces + {$documentStatsStore?.characters_excluding_spaces || 0} +
+
+
+
+{/if} \ No newline at end of file diff --git a/src/lib/components/Toolbar.svelte b/src/lib/components/Toolbar.svelte index b801305..f32295b 100644 --- a/src/lib/components/Toolbar.svelte +++ b/src/lib/components/Toolbar.svelte @@ -1,7 +1,7 @@