mirror of
https://github.com/PR0M3TH3AN/Marlin.git
synced 2026-10-30 02:11:00 +00:00
update
This commit is contained in:
1 parent
6157ac5233
commit
f6fca2c0dd
44 files changed
+492
-508
No files matched your search
@@ -0,0 +1,28 @@
|
||||
// src/cli/annotate.rs
|
||||
use clap::{Subcommand, Args};
|
||||
use rusqlite::Connection;
|
||||
use crate::cli::Format;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum AnnotateCmd {
|
||||
Add (ArgsAdd),
|
||||
List(ArgsList),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsAdd {
|
||||
pub file: String,
|
||||
pub note: String,
|
||||
#[arg(long)] pub range: Option<String>,
|
||||
#[arg(long)] pub highlight: bool,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsList { pub file_pattern: String }
|
||||
|
||||
pub fn run(cmd: &AnnotateCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
AnnotateCmd::Add(a) => todo!("annotate add {:?}", a),
|
||||
AnnotateCmd::List(a) => todo!("annotate list {:?}", a),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! `marlin coll …` – named collections of files (simple “playlists”).
|
||||
|
||||
use clap::{Args, Subcommand};
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::cli::Format; // local enum for text / json output
|
||||
use libmarlin::db; // core DB helpers from the library crate
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum CollCmd {
|
||||
/// Create an empty collection
|
||||
Create(CreateArgs),
|
||||
/// Add files (glob) to a collection
|
||||
Add(AddArgs),
|
||||
/// List files inside a collection
|
||||
List(ListArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct CreateArgs {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct AddArgs {
|
||||
pub name: String,
|
||||
pub file_pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ListArgs {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Look-up an existing collection **without** implicitly creating it.
|
||||
///
|
||||
/// Returns the collection ID or an error if it doesn’t exist.
|
||||
fn lookup_collection_id(conn: &Connection, name: &str) -> anyhow::Result<i64> {
|
||||
conn.query_row(
|
||||
"SELECT id FROM collections WHERE name = ?1",
|
||||
[name],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.map_err(|_| anyhow::anyhow!("collection not found: {}", name))
|
||||
}
|
||||
|
||||
pub fn run(cmd: &CollCmd, conn: &mut Connection, fmt: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
/* ── coll create ──────────────────────────────────────────── */
|
||||
CollCmd::Create(a) => {
|
||||
db::ensure_collection(conn, &a.name)?;
|
||||
if matches!(fmt, Format::Text) {
|
||||
println!("Created collection '{}'", a.name);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── coll add ─────────────────────────────────────────────── */
|
||||
CollCmd::Add(a) => {
|
||||
// Fail if the target collection does not yet exist
|
||||
let coll_id = lookup_collection_id(conn, &a.name)?;
|
||||
|
||||
let like = a.file_pattern.replace('*', "%");
|
||||
let mut stmt = conn.prepare("SELECT id FROM files WHERE path LIKE ?1")?;
|
||||
let ids: Vec<i64> = stmt
|
||||
.query_map([&like], |r| r.get::<_, i64>(0))?
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
for fid in &ids {
|
||||
db::add_file_to_collection(conn, coll_id, *fid)?;
|
||||
}
|
||||
|
||||
match fmt {
|
||||
Format::Text => println!("Added {} file(s) → '{}'", ids.len(), a.name),
|
||||
Format::Json => {
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
println!(
|
||||
"{{\"collection\":\"{}\",\"added\":{}}}",
|
||||
a.name,
|
||||
ids.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── coll list ────────────────────────────────────────────── */
|
||||
CollCmd::List(a) => {
|
||||
let files = db::list_collection(conn, &a.name)?;
|
||||
match fmt {
|
||||
Format::Text => {
|
||||
for f in files {
|
||||
println!("{f}");
|
||||
}
|
||||
}
|
||||
Format::Json => {
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
println!("{}", serde_json::to_string(&files)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# cli/commands.yaml
|
||||
# Philosophy: one canonical spec stops drift between docs & code.
|
||||
link:
|
||||
description: "Manage typed relationships between files"
|
||||
actions:
|
||||
add:
|
||||
args: [from, to]
|
||||
flags: ["--type"]
|
||||
rm:
|
||||
args: [from, to]
|
||||
flags: ["--type"]
|
||||
list:
|
||||
args: [pattern]
|
||||
flags: ["--direction", "--type"]
|
||||
backlinks:
|
||||
args: [pattern]
|
||||
|
||||
coll:
|
||||
description: "Manage named collections of files"
|
||||
actions:
|
||||
create:
|
||||
args: [name]
|
||||
add:
|
||||
args: [name, file_pattern]
|
||||
list:
|
||||
args: [name]
|
||||
|
||||
view:
|
||||
description: "Save and use smart views (saved queries)"
|
||||
actions:
|
||||
save:
|
||||
args: [view_name, query]
|
||||
list: {}
|
||||
exec:
|
||||
args: [view_name]
|
||||
|
||||
state:
|
||||
description: "Track workflow states on files"
|
||||
actions:
|
||||
set:
|
||||
args: [file_pattern, new_state]
|
||||
transitions-add:
|
||||
args: [from_state, to_state]
|
||||
log:
|
||||
args: [file_pattern]
|
||||
|
||||
task:
|
||||
description: "Extract TODOs and manage tasks"
|
||||
actions:
|
||||
scan:
|
||||
args: [directory]
|
||||
list:
|
||||
flags: ["--due-today"]
|
||||
|
||||
remind:
|
||||
description: "Attach reminders to files"
|
||||
actions:
|
||||
set:
|
||||
args: [file_pattern, timestamp, message]
|
||||
|
||||
annotate:
|
||||
description: "Add notes or highlights to files"
|
||||
actions:
|
||||
add:
|
||||
args: [file, note]
|
||||
flags: ["--range", "--highlight"]
|
||||
list:
|
||||
args: [file_pattern]
|
||||
|
||||
version:
|
||||
description: "Versioning and diffs"
|
||||
actions:
|
||||
diff:
|
||||
args: [file]
|
||||
|
||||
event:
|
||||
description: "Link files to dates/events"
|
||||
actions:
|
||||
add:
|
||||
args: [file, date, description]
|
||||
timeline: {}
|
||||
@@ -0,0 +1,24 @@
|
||||
// src/cli/event.rs
|
||||
use clap::{Subcommand, Args};
|
||||
use rusqlite::Connection;
|
||||
use crate::cli::Format;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum EventCmd {
|
||||
Add (ArgsAdd),
|
||||
Timeline,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsAdd {
|
||||
pub file: String,
|
||||
pub date: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
pub fn run(cmd: &EventCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
EventCmd::Add(a) => todo!("event add {:?}", a),
|
||||
EventCmd::Timeline => todo!("event timeline"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//! src/cli/link.rs – manage typed relationships between files
|
||||
|
||||
use clap::{Subcommand, Args};
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::cli::Format; // output selector
|
||||
use libmarlin::db; // ← switched from `crate::db`
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum LinkCmd {
|
||||
Add(LinkArgs),
|
||||
Rm (LinkArgs),
|
||||
List(ListArgs),
|
||||
Backlinks(BacklinksArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct LinkArgs {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
#[arg(long)]
|
||||
pub r#type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ListArgs {
|
||||
pub pattern: String,
|
||||
#[arg(long)]
|
||||
pub direction: Option<String>,
|
||||
#[arg(long)]
|
||||
pub r#type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct BacklinksArgs {
|
||||
pub pattern: String,
|
||||
}
|
||||
|
||||
pub fn run(cmd: &LinkCmd, conn: &mut Connection, format: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
LinkCmd::Add(args) => {
|
||||
let src_id = db::file_id(conn, &args.from)?;
|
||||
let dst_id = db::file_id(conn, &args.to)?;
|
||||
db::add_link(conn, src_id, dst_id, args.r#type.as_deref())?;
|
||||
match format {
|
||||
Format::Text => {
|
||||
if let Some(t) = &args.r#type {
|
||||
println!("Linked '{}' → '{}' [type='{}']", args.from, args.to, t);
|
||||
} else {
|
||||
println!("Linked '{}' → '{}'", args.from, args.to);
|
||||
}
|
||||
}
|
||||
Format::Json => {
|
||||
let typ = args
|
||||
.r#type
|
||||
.as_ref()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.unwrap_or_else(|| "null".into());
|
||||
println!(
|
||||
"{{\"from\":\"{}\",\"to\":\"{}\",\"type\":{}}}",
|
||||
args.from, args.to, typ
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
LinkCmd::Rm(args) => {
|
||||
let src_id = db::file_id(conn, &args.from)?;
|
||||
let dst_id = db::file_id(conn, &args.to)?;
|
||||
db::remove_link(conn, src_id, dst_id, args.r#type.as_deref())?;
|
||||
match format {
|
||||
Format::Text => {
|
||||
if let Some(t) = &args.r#type {
|
||||
println!("Removed link '{}' → '{}' [type='{}']", args.from, args.to, t);
|
||||
} else {
|
||||
println!("Removed link '{}' → '{}'", args.from, args.to);
|
||||
}
|
||||
}
|
||||
Format::Json => {
|
||||
let typ = args
|
||||
.r#type
|
||||
.as_ref()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.unwrap_or_else(|| "null".into());
|
||||
println!(
|
||||
"{{\"from\":\"{}\",\"to\":\"{}\",\"type\":{}}}",
|
||||
args.from, args.to, typ
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
LinkCmd::List(args) => {
|
||||
let results = db::list_links(
|
||||
conn,
|
||||
&args.pattern,
|
||||
args.direction.as_deref(),
|
||||
args.r#type.as_deref(),
|
||||
)?;
|
||||
match format {
|
||||
Format::Json => {
|
||||
let items: Vec<String> = results
|
||||
.into_iter()
|
||||
.map(|(src, dst, t)| {
|
||||
let typ = t
|
||||
.as_ref()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.unwrap_or_else(|| "null".into());
|
||||
format!(
|
||||
"{{\"from\":\"{}\",\"to\":\"{}\",\"type\":{}}}",
|
||||
src, dst, typ
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!("[{}]", items.join(","));
|
||||
}
|
||||
Format::Text => {
|
||||
for (src, dst, t) in results {
|
||||
if let Some(t) = t {
|
||||
println!("{} → {} [type='{}']", src, dst, t);
|
||||
} else {
|
||||
println!("{} → {}", src, dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LinkCmd::Backlinks(args) => {
|
||||
let results = db::find_backlinks(conn, &args.pattern)?;
|
||||
match format {
|
||||
Format::Json => {
|
||||
let items: Vec<String> = results
|
||||
.into_iter()
|
||||
.map(|(src, t)| {
|
||||
let typ = t
|
||||
.as_ref()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.unwrap_or_else(|| "null".into());
|
||||
format!("{{\"from\":\"{}\",\"type\":{}}}", src, typ)
|
||||
})
|
||||
.collect();
|
||||
println!("[{}]", items.join(","));
|
||||
}
|
||||
Format::Text => {
|
||||
for (src, t) in results {
|
||||
if let Some(t) = t {
|
||||
println!("{} [type='{}']", src, t);
|
||||
} else {
|
||||
println!("{}", src);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// src/cli/remind.rs
|
||||
use clap::{Subcommand, Args};
|
||||
use rusqlite::Connection;
|
||||
use crate::cli::Format;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum RemindCmd {
|
||||
Set(ArgsSet),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsSet {
|
||||
pub file_pattern: String,
|
||||
pub timestamp: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub fn run(cmd: &RemindCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
RemindCmd::Set(a) => todo!("remind set {:?}", a),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// src/cli/state.rs
|
||||
use clap::{Subcommand, Args};
|
||||
use rusqlite::Connection;
|
||||
use crate::cli::Format;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum StateCmd {
|
||||
Set(ArgsSet),
|
||||
TransitionsAdd(ArgsTrans),
|
||||
Log(ArgsLog),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsSet { pub file_pattern: String, pub new_state: String }
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsTrans { pub from_state: String, pub to_state: String }
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsLog { pub file_pattern: String }
|
||||
|
||||
pub fn run(cmd: &StateCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
StateCmd::Set(a) => todo!("state set {:?}", a),
|
||||
StateCmd::TransitionsAdd(a)=> todo!("state transitions-add {:?}", a),
|
||||
StateCmd::Log(a) => todo!("state log {:?}", a),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// src/cli/task.rs
|
||||
use clap::{Subcommand, Args};
|
||||
use rusqlite::Connection;
|
||||
use crate::cli::Format;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum TaskCmd {
|
||||
Scan(ArgsScan),
|
||||
List(ArgsList),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsScan { pub directory: String }
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsList { #[arg(long)] pub due_today: bool }
|
||||
|
||||
pub fn run(cmd: &TaskCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
TaskCmd::Scan(a) => todo!("task scan {:?}", a),
|
||||
TaskCmd::List(a) => todo!("task list {:?}", a),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// src/cli/version.rs
|
||||
use clap::{Subcommand, Args};
|
||||
use rusqlite::Connection;
|
||||
use crate::cli::Format;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum VersionCmd {
|
||||
Diff(ArgsDiff),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsDiff { pub file: String }
|
||||
|
||||
pub fn run(cmd: &VersionCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
VersionCmd::Diff(a) => todo!("version diff {:?}", a),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//! `marlin view …` – save & use “smart folders” (named queries).
|
||||
|
||||
use std::fs;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Args, Subcommand};
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::cli::Format; // output selector stays local
|
||||
use libmarlin::db; // ← path switched from `crate::db`
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum ViewCmd {
|
||||
/// Save (or update) a view
|
||||
Save(ArgsSave),
|
||||
/// List all saved views
|
||||
List,
|
||||
/// Execute a view (print matching paths)
|
||||
Exec(ArgsExec),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsSave {
|
||||
pub view_name: String,
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ArgsExec {
|
||||
pub view_name: String,
|
||||
}
|
||||
|
||||
pub fn run(cmd: &ViewCmd, conn: &mut Connection, fmt: Format) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
/* ── view save ───────────────────────────────────────────── */
|
||||
ViewCmd::Save(a) => {
|
||||
db::save_view(conn, &a.view_name, &a.query)?;
|
||||
if matches!(fmt, Format::Text) {
|
||||
println!("Saved view '{}' = {}", a.view_name, a.query);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── view list ───────────────────────────────────────────── */
|
||||
ViewCmd::List => {
|
||||
let views = db::list_views(conn)?;
|
||||
match fmt {
|
||||
Format::Text => {
|
||||
for (name, q) in views {
|
||||
println!("{name}: {q}");
|
||||
}
|
||||
}
|
||||
Format::Json => {
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
println!("{}", serde_json::to_string(&views)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── view exec ───────────────────────────────────────────── */
|
||||
ViewCmd::Exec(a) => {
|
||||
let raw = db::view_query(conn, &a.view_name)?;
|
||||
|
||||
// Re-use the tiny parser from marlin search
|
||||
let fts_expr = build_fts_match(&raw);
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT f.path
|
||||
FROM files_fts
|
||||
JOIN files f ON f.rowid = files_fts.rowid
|
||||
WHERE files_fts MATCH ?1
|
||||
ORDER BY rank
|
||||
"#,
|
||||
)?;
|
||||
let mut paths: Vec<String> = stmt
|
||||
.query_map([fts_expr], |r| r.get::<_, String>(0))?
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
/* ── NEW: graceful fallback when FTS finds nothing ───── */
|
||||
if paths.is_empty() && !raw.contains(':') {
|
||||
paths = naive_search(conn, &raw)?;
|
||||
}
|
||||
|
||||
if paths.is_empty() && matches!(fmt, Format::Text) {
|
||||
eprintln!("(view '{}' has no matches)", a.view_name);
|
||||
} else {
|
||||
for p in paths {
|
||||
println!("{p}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/* ─── naive substring path/content search (≤ 64 kB files) ───────── */
|
||||
|
||||
fn naive_search(conn: &Connection, term: &str) -> Result<Vec<String>> {
|
||||
let term_lc = term.to_lowercase();
|
||||
let mut stmt = conn.prepare("SELECT path FROM files")?;
|
||||
let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
|
||||
|
||||
let mut hits = Vec::new();
|
||||
for p in rows {
|
||||
let p = p?;
|
||||
/* path match */
|
||||
if p.to_lowercase().contains(&term_lc) {
|
||||
hits.push(p);
|
||||
continue;
|
||||
}
|
||||
/* small-file content match */
|
||||
if let Ok(meta) = fs::metadata(&p) {
|
||||
if meta.len() > 64_000 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Ok(content) = fs::read_to_string(&p) {
|
||||
if content.to_lowercase().contains(&term_lc) {
|
||||
hits.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(hits)
|
||||
}
|
||||
|
||||
/* ─── minimal copy of search-string → FTS5 translator ───────────── */
|
||||
|
||||
fn build_fts_match(raw_query: &str) -> String {
|
||||
use shlex;
|
||||
let mut parts = Vec::new();
|
||||
let toks = shlex::split(raw_query).unwrap_or_else(|| vec![raw_query.to_string()]);
|
||||
for tok in toks {
|
||||
if ["AND", "OR", "NOT"].contains(&tok.as_str()) {
|
||||
parts.push(tok);
|
||||
} else if let Some(tag) = tok.strip_prefix("tag:") {
|
||||
for (i, seg) in tag.split('/').filter(|s| !s.is_empty()).enumerate() {
|
||||
if i > 0 {
|
||||
parts.push("AND".into());
|
||||
}
|
||||
parts.push(format!("tags_text:{}", escape(seg)));
|
||||
}
|
||||
} else if let Some(attr) = tok.strip_prefix("attr:") {
|
||||
let mut kv = attr.splitn(2, '=');
|
||||
let key = kv.next().unwrap();
|
||||
if let Some(val) = kv.next() {
|
||||
parts.push(format!("attrs_text:{}", escape(key)));
|
||||
parts.push("AND".into());
|
||||
parts.push(format!("attrs_text:{}", escape(val)));
|
||||
} else {
|
||||
parts.push(format!("attrs_text:{}", escape(key)));
|
||||
}
|
||||
} else {
|
||||
parts.push(escape(&tok));
|
||||
}
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
fn escape(term: &str) -> String {
|
||||
if term.contains(|c: char| c.is_whitespace() || "-:()\"".contains(c))
|
||||
|| ["AND", "OR", "NOT", "NEAR"].contains(&term.to_uppercase().as_str())
|
||||
{
|
||||
format!("\"{}\"", term.replace('"', "\"\""))
|
||||
} else {
|
||||
term.to_string()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user