Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 24s
The on-device core that makes the desktop app work with no server and no login. - rusqlite (bundled SQLite, so no system libsqlite dependency to vary across builds); uuid v4 ids; RFC3339/Date.toISOString-compatible timestamps. - Schema mirroring the note model: notes, labels, note_labels (with via_tag), checklist_items, attachments, link_previews, note_revisions, saved_filters, plus per-row sync_revision/dirty + a sync_state row for the M10.7 engine. user_version-gated migrations. - derive.rs: pure [[wiki-link]] + #tag scanners (mirror the frontend inline rules, no regex dep) with unit tests; #tags re-sync via_tag labels on save, [[links]] drive backlinks at query time (derived, never stored). - store.rs: the full repository surface (facet/label/date/text list, create, PATCH-semantics update, pin/archive/color/kind, checklist items, labels CRUD + merge, reminders complete/snooze, reorder, trash/restore/delete, revisions + restore, titles/search/backlinks/link-search, saved filters). - commands.rs: ~38 #[tauri::command]s over a Mutex<Connection> in managed state. - lib.rs: opens the DB in the platform app-data dir on setup; synthetic offline config/user so the auth-gated router resolves with no login. Attachment upload / URL unfurl / import are intentionally deferred (network/file concerns); adapters/local.ts (M10.5) wires all of the above via invoke. Task 1993. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
114 lines
3.7 KiB
Rust
114 lines
3.7 KiB
Rust
//! Deriving `[[wiki-links]]` and `#tags` from a note's body — the local mirror of
|
|
//! what the server computes on save. Pure string scanning (no regex dependency),
|
|
//! kept in lockstep with the frontend's inline rules (see frontend notes/markdown.ts):
|
|
//!
|
|
//! - `[[link]]`: `[[` … `]]` with no brackets inside, inner text trimmed. Used to
|
|
//! compute backlinks at query time (links are derived, never stored/synced).
|
|
//! - `#tag`: `#` at a word boundary followed by tag characters (letter first).
|
|
//! On save these become labels attached with `via_tag = true`.
|
|
//!
|
|
//! Both dedupe case-insensitively, preserving first-seen order.
|
|
|
|
/// Extract the trimmed inner text of every `[[wiki-link]]` in `body`.
|
|
pub fn extract_links(body: &str) -> Vec<String> {
|
|
let bytes = body.as_bytes();
|
|
let mut out: Vec<String> = Vec::new();
|
|
let mut i = 0;
|
|
while i + 1 < bytes.len() {
|
|
if bytes[i] == b'[' && bytes[i + 1] == b'[' {
|
|
if let Some(rel) = body[i + 2..].find("]]") {
|
|
let inner = &body[i + 2..i + 2 + rel];
|
|
// Mirror the frontend's `[^[\]]+`: no stray brackets inside.
|
|
if !inner.contains('[') && !inner.contains(']') {
|
|
let t = inner.trim();
|
|
if !t.is_empty() {
|
|
push_unique(&mut out, t);
|
|
}
|
|
}
|
|
i += 2 + rel + 2;
|
|
continue;
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Extract every `#tag` name (without the leading `#`) from `body`.
|
|
pub fn extract_tags(body: &str) -> Vec<String> {
|
|
let chars: Vec<char> = body.chars().collect();
|
|
let mut out: Vec<String> = Vec::new();
|
|
let mut i = 0;
|
|
while i < chars.len() {
|
|
if chars[i] == '#' {
|
|
let boundary = i == 0 || (!is_tag_char(chars[i - 1]) && chars[i - 1] != '#');
|
|
// A tag must start with a letter (so "#1" or a bare "#" is not a tag).
|
|
if boundary && i + 1 < chars.len() && chars[i + 1].is_alphabetic() {
|
|
let mut j = i + 1;
|
|
while j < chars.len() && is_tag_char(chars[j]) {
|
|
j += 1;
|
|
}
|
|
let tag: String = chars[i + 1..j].iter().collect();
|
|
push_unique(&mut out, &tag);
|
|
i = j;
|
|
continue;
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
out
|
|
}
|
|
|
|
fn is_tag_char(c: char) -> bool {
|
|
c.is_alphanumeric() || c == '_' || c == '-'
|
|
}
|
|
|
|
fn push_unique(out: &mut Vec<String>, candidate: &str) {
|
|
if !out.iter().any(|x| x.eq_ignore_ascii_case(candidate)) {
|
|
out.push(candidate.to_string());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn links_basic_and_trim() {
|
|
assert_eq!(extract_links("see [[ Alpha ]] and [[Beta]]"), vec!["Alpha", "Beta"]);
|
|
}
|
|
|
|
#[test]
|
|
fn links_dedupe_case_insensitive_first_seen() {
|
|
assert_eq!(extract_links("[[Note]] then [[note]] again"), vec!["Note"]);
|
|
}
|
|
|
|
#[test]
|
|
fn links_ignore_malformed_and_nested_brackets() {
|
|
assert_eq!(extract_links("[[a[b]] [[]] [ [x] ] plain"), Vec::<String>::new());
|
|
assert_eq!(extract_links("[[ok]] [[a]b]]"), vec!["ok"]);
|
|
}
|
|
|
|
#[test]
|
|
fn tags_basic() {
|
|
assert_eq!(extract_tags("a #todo and #Work-item_2 here"), vec!["todo", "Work-item_2"]);
|
|
}
|
|
|
|
#[test]
|
|
fn tags_require_letter_start_and_boundary() {
|
|
// "#1" (digit) and an in-word "#" (email-ish) are not tags.
|
|
assert_eq!(extract_tags("#1 nope a#b no but #Yes"), vec!["Yes"]);
|
|
}
|
|
|
|
#[test]
|
|
fn tags_dedupe_case_insensitive() {
|
|
assert_eq!(extract_tags("#Home #home #HOME"), vec!["Home"]);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_body() {
|
|
assert!(extract_links("").is_empty());
|
|
assert!(extract_tags("").is_empty());
|
|
}
|
|
}
|