// LinkSync Query Engine // Client-side query parser and executor for building queries const QueryEngine = { tokenize(expression) { const tokens = []; let pos = 0; const len = expression.length; while (pos < len) { const ch = expression[pos]; if (ch === " " || ch === "\t") { pos++; continue; } if (ch === "(") { tokens.push({ type: "LPAREN", value: "(" }); pos++; continue; } if (ch === ")") { tokens.push({ type: "RPAREN", value: ")" }); pos++; continue; } if (ch === ",") { tokens.push({ type: "COMMA", value: "," }); pos++; continue; } if (expression.substring(pos, pos + 3) === "AND") { tokens.push({ type: "OPERATOR", value: "AND" }); pos += 3; continue; } if (expression.substring(pos, pos + 2) === "OR") { tokens.push({ type: "OPERATOR", value: "OR" }); pos += 2; continue; } if (expression.substring(pos, pos + 3) === "XOR") { tokens.push({ type: "OPERATOR", value: "XOR" }); pos += 3; continue; } if (ch === "'" || ch === '"') { const quote = ch; pos++; let value = ""; while (pos < len && expression[pos] !== quote) { value += expression[pos]; pos++; } pos++; tokens.push({ type: "TERM", value }); continue; } if (/[a-zA-Z0-9_.\-]/.test(ch)) { let value = ""; while (pos < len && /[a-zA-Z0-9_.\-:\/?&=%]/.test(expression[pos])) { value += expression[pos]; pos++; } if (value.includes(":")) { const [field, ...rest] = value.split(":"); const fieldValue = rest.join(":"); const knownFields = ["url", "tag", "title", "description", "path", "id"]; if (knownFields.includes(field.toLowerCase())) { tokens.push({ type: "FIELD", value: field.toUpperCase() }); tokens.push({ type: "TERM", value: fieldValue }); continue; } } tokens.push({ type: "TERM", value }); continue; } pos++; } return tokens; }, parse(expression) { if (!expression || !expression.trim()) return null; const tokens = this.tokenize(expression); if (tokens.length === 0) return null; const parser = new QueryParserState(tokens); return parser.parseOr(); }, buildQueryString(ast) { if (!ast) return ""; switch (ast.type) { case "TERM": return ast.value; case "TERM_SET": return `(${ast.values.join(", ")})`; case "FIELD": return `${ast.field.toLowerCase()}:${ast.value}`; case "AND": case "OR": case "XOR": return `(${this.buildQueryString(ast.left)} ${ast.type} ${this.buildQueryString(ast.right)})`; default: return ""; } }, validate(expression) { try { const ast = this.parse(expression); return { valid: true, ast }; } catch (e) { return { valid: false, error: e.message }; } }, }; class QueryParserState { constructor(tokens) { this.tokens = tokens; this.pos = 0; } current() { return this.pos < this.tokens.length ? this.tokens[this.pos] : null; } advance() { const token = this.current(); this.pos++; return token; } expect(type) { const token = this.current(); if (!token) throw new Error(`Expected ${type}, got end of input`); if (token.type !== type) throw new Error(`Expected ${type}, got ${token.type}`); return this.advance(); } parseOr() { let left = this.parseAnd(); while (this.current() && this.current().type === "OPERATOR" && this.current().value === "OR") { this.advance(); const right = this.parseAnd(); left = { type: "OR", left, right }; } return left; } parseAnd() { let left = this.parseXor(); while (this.current() && this.current().type === "OPERATOR" && this.current().value === "AND") { this.advance(); const right = this.parseXor(); left = { type: "AND", left, right }; } return left; } parseXor() { let left = this.parsePrimary(); while (this.current() && this.current().type === "OPERATOR" && this.current().value === "XOR") { this.advance(); const right = this.parsePrimary(); left = { type: "XOR", left, right }; } return left; } parsePrimary() { const token = this.current(); if (!token) throw new Error("Unexpected end of input"); if (token.type === "LPAREN") { this.advance(); const node = this.parseOr(); this.expect("RPAREN"); return node; } if (token.type === "FIELD") { const field = this.advance().value; const valueToken = this.current(); if (valueToken && valueToken.type === "TERM") { this.advance(); return { type: "FIELD", field, value: valueToken.value }; } return { type: "FIELD", field, value: "" }; } if (token.type === "TERM") { return this.parseTerm(); } throw new Error(`Unexpected token: ${token.value}`); } parseTerm() { const token = this.advance(); const next = this.current(); if (next && next.type === "COMMA") { const values = [token.value]; while (this.current() && this.current().type === "COMMA") { this.advance(); const termToken = this.current(); if (termToken && termToken.type === "TERM") { values.push(this.advance().value); } } return { type: "TERM_SET", values }; } return { type: "TERM", value: token.value }; } }