import { Controller } from "@hotwired/stimulus"; export default class QueryInputController extends Controller { static targets = ["input", "highlight"]; static values = { query: String }; connect() { this.queryValue = this.inputTarget.value; } update() { this.queryValue = this.inputTarget.value; } queryValueChanged(query) { this.highlightTarget.innerHTML = ""; new Parser().parse(query).tokens.forEach((token) => { this.highlightTarget.appendChild(token.render()); }); } } class Parser { constructor() { this.tokens = []; this.values = null; } parse(input) { const query = new StringScanner(input); while (!query.isEos()) { this.push(this.skipWhitespace(query)); const value = this.takeTagged(query) || this.takeUntagged(query); if (!this.push(value)) break; } return this; } push(token) { if (token) { this.values ? this.values.push(token) : this.tokens.push(token); } return !!token; } skipWhitespace(query) { if (!query.scan(/\s+/)) return; return new Token(query.matched()); } takeUntagged(query) { if (!query.scan(/\S+/)) return; return new Untagged(query.matched()); } takeTagged(query) { if (!query.scan(/(\w+(?:\.\w+)?)(:\s*)/)) return; const key = query.valueAt(1); const separator = query.valueAt(2); const value = this.takeArrayValue(query) || this.takeSingleValue(query) || new Token(); return new Tagged(key, separator, value); } takeArrayValue(query) { if (!query.scan(/\[\s*/)) return; const start = new Token(query.matched()); const values = (this.values = []); while (!query.isEos()) { if (!this.push(this.takeSingleValue(query))) break; if (!this.push(this.takeDelimiter(query))) break; } query.scan(/\s*]/); const end = new Token(query.matched()); this.values = null; return new Array(start, values, end); } takeDelimiter(query) { if (!query.scan(/\s*,\s*/)) return; return new Token(query.matched()); } takeSingleValue(query) { return this.takeQuotedValue(query) || this.takeUnquotedValue(query); } takeQuotedValue(query) { if (!query.scan(/"([^"]*)"/)) return; return new Value(query.matched()); } takeUnquotedValue(query) { if (!query.scan(/[^ \],]*/)) return; return new Value(query.matched()); } } class Token { constructor(value = "") { this.value = value; } render() { return document.createTextNode(this.value); } } class Value extends Token { render() { const span = document.createElement("span"); span.className = "value"; span.innerText = this.value; return span; } } class Tagged extends Token { constructor(key, separator, value) { super(); this.key = key; this.separator = separator; this.value = value; } render() { const span = document.createElement("span"); span.className = "tag"; const key = document.createElement("span"); key.className = "key"; key.innerText = this.key; span.appendChild(key); span.appendChild(document.createTextNode(this.separator)); span.appendChild(this.value.render()); return span; } } class Untagged extends Token { render() { const span = document.createElement("span"); span.className = "untagged"; span.innerText = this.value; return span; } } class Array extends Token { constructor(start, values, end) { super(); this.start = start; this.values = values; this.end = end; } render() { const array = document.createElement("span"); array.className = "array-values"; array.appendChild(this.start.render()); this.values.forEach((value) => { const span = document.createElement("span"); span.appendChild(value.render()); array.appendChild(span); }); array.appendChild(this.end.render()); return array; } } class StringScanner { constructor(input) { this.input = input; this.position = 0; this.last = null; } isEos() { return this.position >= this.input.length; } scan(regex) { const match = regex.exec(this.input.substring(this.position)); if (match?.index === 0) { this.last = match; this.position += match[0].length; return true; } else { this.last = {}; return false; } } matched() { return this.last && this.last[0]; } valueAt(index) { return this.last && this.last[index]; } }