mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af389c7097 | ||
|
|
f11ddd290f | ||
|
|
4975e76358 |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "margin-app",
|
"name": "margin-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.10",
|
"version": "0.1.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Generated
+26
-1
@@ -4713,11 +4713,14 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "margin-app"
|
name = "margin-app"
|
||||||
version = "0.1.7"
|
version = "0.1.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
"harper-core",
|
"harper-core",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation",
|
||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5138,9 +5141,17 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
"objc2-cloud-kit",
|
||||||
|
"objc2-core-data",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-core-image",
|
||||||
|
"objc2-core-text",
|
||||||
|
"objc2-core-video",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
|
"objc2-quartz-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5160,6 +5171,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
@@ -5220,6 +5232,19 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-video"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
|
|||||||
+13
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "margin-app"
|
name = "margin-app"
|
||||||
version = "0.1.10"
|
version = "0.1.12"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -31,7 +31,6 @@ fontdb = "0.23"
|
|||||||
typst = "0.14.2"
|
typst = "0.14.2"
|
||||||
typst-pdf = "0.14.2"
|
typst-pdf = "0.14.2"
|
||||||
typst-as-lib = "0.15.5"
|
typst-as-lib = "0.15.5"
|
||||||
spellbook = "0.4"
|
|
||||||
# Pinned exactly: the [patch] stubs below are tied to this version's burn/cubecl graph. A minor
|
# Pinned exactly: the [patch] stubs below are tied to this version's burn/cubecl graph. A minor
|
||||||
# bump could silently invalidate the patch ("unused") and pull the broken CUDA subtree back in,
|
# bump could silently invalidate the patch ("unused") and pull the broken CUDA subtree back in,
|
||||||
# so bump deliberately and re-audit the stubs. (see [patch.crates-io])
|
# so bump deliberately and re-audit the stubs. (see [patch.crates-io])
|
||||||
@@ -41,6 +40,18 @@ sha2 = "0.10"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
url = "2"
|
url = "2"
|
||||||
|
|
||||||
|
# Spelling on macOS uses the system NSSpellChecker (same engine as TextEdit/Safari): far better
|
||||||
|
# vocabulary than the bundled Hunspell dictionary and it skips URLs. These objc2 crates are already
|
||||||
|
# in the graph via Tauri, so this adds nothing new to the build.
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2 = "0.6"
|
||||||
|
objc2-app-kit = { version = "0.3", features = ["NSSpellChecker"] }
|
||||||
|
objc2-foundation = { version = "0.3", features = ["NSString", "NSArray", "NSRange", "NSTextCheckingResult"] }
|
||||||
|
|
||||||
|
# Other platforms fall back to the bundled Hunspell dictionary via spellbook.
|
||||||
|
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||||
|
spellbook = "0.4"
|
||||||
|
|
||||||
# harper-core transitively declares an optional, disabled `burn-cuda` (CUDA backend).
|
# harper-core transitively declares an optional, disabled `burn-cuda` (CUDA backend).
|
||||||
# Cargo still version-resolves that dead subtree, which currently fails (broken tracel-llvm
|
# Cargo still version-resolves that dead subtree, which currently fails (broken tracel-llvm
|
||||||
# prereleases) and links-collides with `zip`'s lzma. Replace it with an empty stub so the
|
# prereleases) and links-collides with `zip`'s lzma. Replace it with an empty stub so the
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ mod epub;
|
|||||||
mod fonts;
|
mod fonts;
|
||||||
mod gdrive;
|
mod gdrive;
|
||||||
mod library;
|
mod library;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macspell;
|
||||||
mod pdf;
|
mod pdf;
|
||||||
mod project;
|
mod project;
|
||||||
mod proofing;
|
mod proofing;
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use objc2::rc::autoreleasepool;
|
||||||
|
use objc2_app_kit::NSSpellChecker;
|
||||||
|
use objc2_foundation::{NSRange, NSString, NSTextCheckingType};
|
||||||
|
|
||||||
|
pub struct MacIssue {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub word: String,
|
||||||
|
pub suggestions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utf16_to_codepoint(text: &str, utf16_len: usize) -> Vec<usize> {
|
||||||
|
let mut map = Vec::with_capacity(utf16_len + 1);
|
||||||
|
let mut cp = 0;
|
||||||
|
for ch in text.chars() {
|
||||||
|
for _ in 0..ch.len_utf16() {
|
||||||
|
map.push(cp);
|
||||||
|
}
|
||||||
|
cp += 1;
|
||||||
|
}
|
||||||
|
map.push(cp);
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(text: &str, custom: &HashSet<String>) -> Vec<MacIssue> {
|
||||||
|
autoreleasepool(|_| {
|
||||||
|
let checker = NSSpellChecker::sharedSpellChecker();
|
||||||
|
let ns = NSString::from_str(text);
|
||||||
|
let len = ns.length();
|
||||||
|
let results = unsafe {
|
||||||
|
checker.checkString_range_types_options_inSpellDocumentWithTag_orthography_wordCount(
|
||||||
|
&ns,
|
||||||
|
NSRange { location: 0, length: len },
|
||||||
|
NSTextCheckingType::Spelling.bits(),
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let map = utf16_to_codepoint(text, len);
|
||||||
|
let chars: Vec<char> = text.chars().collect();
|
||||||
|
let mut issues = Vec::new();
|
||||||
|
for result in results.iter() {
|
||||||
|
let range = result.range();
|
||||||
|
let start = map[range.location.min(len)];
|
||||||
|
let end = map[(range.location + range.length).min(len)];
|
||||||
|
let word: String = chars[start..end].iter().collect();
|
||||||
|
if custom.contains(&word.to_lowercase()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut suggestions = Vec::new();
|
||||||
|
if let Some(guesses) =
|
||||||
|
checker.guessesForWordRange_inString_language_inSpellDocumentWithTag(range, &ns, None, 0)
|
||||||
|
{
|
||||||
|
for guess in guesses.iter() {
|
||||||
|
suggestions.push(guess.to_string());
|
||||||
|
if suggestions.len() >= 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
issues.push(MacIssue { start, end, word, suggestions });
|
||||||
|
}
|
||||||
|
issues
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ struct Harper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
speller: Option<spellbook::Dictionary>,
|
speller: Option<spellbook::Dictionary>,
|
||||||
harper: Option<Harper>,
|
harper: Option<Harper>,
|
||||||
custom: HashSet<String>,
|
custom: HashSet<String>,
|
||||||
@@ -87,6 +88,7 @@ fn load_custom(app: &tauri::AppHandle) -> HashSet<String> {
|
|||||||
set
|
set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn build_speller(app: &tauri::AppHandle) -> Result<spellbook::Dictionary, String> {
|
fn build_speller(app: &tauri::AppHandle) -> Result<spellbook::Dictionary, String> {
|
||||||
let aff = dict_file(app, "index.aff")?;
|
let aff = dict_file(app, "index.aff")?;
|
||||||
let dic = dict_file(app, "index.dic")?;
|
let dic = dict_file(app, "index.dic")?;
|
||||||
@@ -100,18 +102,22 @@ fn build_harper() -> Harper {
|
|||||||
Harper { linter, dict }
|
Harper { linter, dict }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn is_word_char(c: char) -> bool {
|
fn is_word_char(c: char) -> bool {
|
||||||
c.is_alphabetic()
|
c.is_alphabetic()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn has_irregular_case(word: &str) -> bool {
|
fn has_irregular_case(word: &str) -> bool {
|
||||||
word.chars().skip(1).any(|c| c.is_uppercase())
|
word.chars().skip(1).any(|c| c.is_uppercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn is_apostrophe(c: char) -> bool {
|
fn is_apostrophe(c: char) -> bool {
|
||||||
c == '\'' || c == '\u{2019}'
|
c == '\'' || c == '\u{2019}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn collect_spelling(
|
fn collect_spelling(
|
||||||
speller: &spellbook::Dictionary,
|
speller: &spellbook::Dictionary,
|
||||||
custom: &HashSet<String>,
|
custom: &HashSet<String>,
|
||||||
@@ -143,6 +149,10 @@ fn collect_spelling(
|
|||||||
}
|
}
|
||||||
let mut suggestions = Vec::new();
|
let mut suggestions = Vec::new();
|
||||||
speller.suggest(&word, &mut suggestions);
|
speller.suggest(&word, &mut suggestions);
|
||||||
|
let lower = word.to_lowercase();
|
||||||
|
if suggestions.iter().any(|s| s.to_lowercase() == lower) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
suggestions.truncate(5);
|
suggestions.truncate(5);
|
||||||
issues.push(Issue {
|
issues.push(Issue {
|
||||||
start,
|
start,
|
||||||
@@ -197,6 +207,7 @@ pub async fn proof_text(
|
|||||||
let mut custom = load_custom(&app);
|
let mut custom = load_custom(&app);
|
||||||
load_tech(&app, &mut custom);
|
load_tech(&app, &mut custom);
|
||||||
*guard = Some(Engine {
|
*guard = Some(Engine {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
speller: None,
|
speller: None,
|
||||||
harper: None,
|
harper: None,
|
||||||
custom,
|
custom,
|
||||||
@@ -207,10 +218,24 @@ pub async fn proof_text(
|
|||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
let mut issues = Vec::new();
|
let mut issues = Vec::new();
|
||||||
if spelling {
|
if spelling {
|
||||||
if engine.speller.is_none() {
|
#[cfg(target_os = "macos")]
|
||||||
engine.speller = Some(build_speller(&app)?);
|
for mi in crate::macspell::check(&text, &engine.custom) {
|
||||||
|
issues.push(Issue {
|
||||||
|
start: mi.start,
|
||||||
|
end: mi.end,
|
||||||
|
kind: "spelling".into(),
|
||||||
|
category: "Spelling".into(),
|
||||||
|
message: format!("“{}” may be misspelled", mi.word),
|
||||||
|
suggestions: mi.suggestions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
if engine.speller.is_none() {
|
||||||
|
engine.speller = Some(build_speller(&app)?);
|
||||||
|
}
|
||||||
|
collect_spelling(engine.speller.as_ref().unwrap(), &engine.custom, &chars, &mut issues);
|
||||||
}
|
}
|
||||||
collect_spelling(engine.speller.as_ref().unwrap(), &engine.custom, &chars, &mut issues);
|
|
||||||
}
|
}
|
||||||
if grammar {
|
if grammar {
|
||||||
if engine.harper.is_none() {
|
if engine.harper.is_none() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Margin",
|
"productName": "Margin",
|
||||||
"version": "0.1.10",
|
"version": "0.1.12",
|
||||||
"identifier": "studio.margin.app",
|
"identifier": "studio.margin.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
Reference in new issue
Block a user