From 451a5bcd2706e41cd752261feeb4104ae0b6cf90 Mon Sep 17 00:00:00 2001 From: rooyca Date: Tue, 21 May 2024 21:12:33 -0500 Subject: [PATCH] update(feat): query in codeblocks - Now we are using MarkdownPostProcessor instead of workspace.on('editor-change') - Now is posible to query elements inside an array using numbers --- .gitignore | 2 +- README.md | 34 ++++++++++++ manifest.json | 2 +- package.json | 2 +- src/functions.ts | 121 ++++++++++++++++++++++++++++++++++++++++ src/main.ts | 140 ++++++++++++++++++++++++++++++++--------------- 6 files changed, 255 insertions(+), 46 deletions(-) create mode 100644 src/functions.ts diff --git a/.gitignore b/.gitignore index e09a007..3d332c7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ # Intellij *.iml -.idea +.idea* # npm node_modules diff --git a/README.md b/README.md index f639267..c52fea4 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,18 @@ Furthermore, QJSON extends its functionality to external JSON files. For example @data.json>store.books.0.author; ``` +It's also posible to query the file directly using the `#qj-query` flag: + +~~~markdown +```qjson +#qj-id: 24 +#qj-file: data.json +#qj-show-json +#qj-hide-id +#qj-query: pageProps.heroData[win_rate >= 55 && role == Mage] +``` +~~~ + ## 🏳️ Flags Query JSON supports various flags to enhance customization and functionality: @@ -96,6 +108,28 @@ This flag allows you to display the JSON within the rendered output. By default If provided, this flag specifies the file path containing the JSON data. In its absence, the plugin scans for JSON data within the code block. +#### `#qj-query` (optional) + +This flag allows you to query the JSON file directly from your codeblock. The query syntax must be inside brackets `[]`. The supported operators are: + +- Logical operators: `&&`, `||` +- Comparison operators: `==`, `!=`, `>`, `>=`, `<`, `<=` + +**Example**: + +~~~markdown +```qjson +#qj-id: 24 +#qj-file: data.json +#qj-show-json +#qj-hide-id +#qj-query: pageProps.heroData[win_rate >= 55 && role == Mage] +``` +~~~ + +> [!NOTE] +> The `#qj-show-json` flag is mandatory when using the `#qj-query` flag. + ## 🛠️ Contribution If you encounter any issues or have suggestions for improvement, please feel free to contribute to the project. Your feedback is invaluable in enhancing the plugin's functionality and user experience. diff --git a/manifest.json b/manifest.json index 3065076..5a6500e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "query-json", "name": "Query JSON", - "version": "0.0.6", + "version": "0.0.7", "minAppVersion": "0.15.0", "description": "Read, query and work with JSON.", "author": "rooyca", diff --git a/package.json b/package.json index f6a9e70..06b6600 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "query-json", - "version": "0.0.6", + "version": "0.0.7", "description": "Read, query and work with JSON.", "main": "main.js", "scripts": { diff --git a/src/functions.ts b/src/functions.ts new file mode 100644 index 0000000..ed720cf --- /dev/null +++ b/src/functions.ts @@ -0,0 +1,121 @@ +export function parseQuery(query) { + const parts = query.split('.'); + return parts.map(part => { + const match = part.match(/(\w+)\[(.*)\]/); + if (match) { + return { + type: 'filter', + field: match[1], + condition: match[2] + }; + } + return { + type: 'field', + name: part + }; + }); +} + +function parseCondition(condition) { + const logicalOperators = ['&&', '||']; + let conditions = []; + let operators = []; + + let remainingCondition = condition; + logicalOperators.forEach(operator => { + if (remainingCondition.includes(operator)) { + const parts = remainingCondition.split(operator).map(s => s.trim()); + conditions.push(parseSingleCondition(parts[0])); + operators.push(operator); + remainingCondition = parts[1]; + } + }); + + if (conditions.length === 0) { + conditions.push(parseSingleCondition(remainingCondition)); + } else { + conditions.push(parseSingleCondition(remainingCondition)); + } + + return { conditions, operators }; +} + +function parseSingleCondition(condition) { + const comparisonOperators = ['>=', '<=', '==', '>', '<', '!=']; + for (let operator of comparisonOperators) { + if (condition.includes(operator)) { + const [key, value] = condition.split(operator).map(s => s.trim()); + return { key, operator, value }; + } + } + throw new Error(`Invalid condition: ${condition}`); +} + +export function executeQuery(json, parsedQuery) { + let result = json; + let finalResult; + + parsedQuery.forEach(part => { + if (part.type === 'field') { + result = result[part.name]; + } else if (part.type === 'filter') { + const { conditions, operators } = parseCondition(part.condition); + const filteredResult = result[part.field].filter(item => { + return evaluateConditions(item, conditions, operators); + }); + // Store the filtered result in finalResult + if (!finalResult) { + finalResult = filteredResult; + } else { + // Merge the filtered result with finalResult + finalResult = finalResult.concat(filteredResult); + } + } + }); + + // If no filter conditions were applied, finalResult will be undefined, so set it to the original result + finalResult = finalResult || result; + + try { + if (parsedQuery.length > 0 && parsedQuery[parsedQuery.length - 1].type === 'field') { + // Map only the required field from finalResult if the last part of the query is a 'field' type + finalResult = finalResult.map(item => item[parsedQuery[parsedQuery.length - 1].name]); + } + } catch (e) { + console.error(e); + // Handle any potential errors during mapping + return finalResult; + } + + return finalResult; +} + +function evaluateConditions(item, conditions, operators) { + let result = evaluateCondition(item, conditions[0]); + + for (let i = 0; i < operators.length; i++) { + const operator = operators[i]; + const nextConditionResult = evaluateCondition(item, conditions[i + 1]); + + if (operator === '&&') { + result = result && nextConditionResult; + } else if (operator === '||') { + result = result || nextConditionResult; + } + } + + return result; +} + +function evaluateCondition(item, condition) { + const { key, operator, value } = condition; + switch (operator) { + case '>=': return item[key] >= Number(value); + case '<=': return item[key] <= Number(value); + case '==': return item[key] == value; + case '>': return item[key] > Number(value); + case '<': return item[key] < Number(value); + case '!=': return item[key] != value; + default: return false; + } +} diff --git a/src/main.ts b/src/main.ts index cf5ee09..938da92 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ import { App, Notice, Plugin } from 'obsidian'; +import { parseQuery, executeQuery} from './functions'; export default class QJSON extends Plugin { @@ -23,6 +24,13 @@ export default class QJSON extends Plugin { return; } + let query; + + if (source.includes('#qj-query:')) { + query = source.match(/#qj-query: (.+)/)[1]; + query = parseQuery(query); + } + let desc; if (!source.includes('#qj-hide-id')) { @@ -48,65 +56,111 @@ export default class QJSON extends Plugin { } const json = JSON.parse(source); - el.createEl('pre', {text: JSON.stringify(json, null, 2), cls: 'QJSON-'+id+' cdQjson '+showJson}); + + if (query) { + const result = executeQuery(json, query); + el.createEl('pre', {text: JSON.stringify(result, null, 2), cls: 'QJSON-'+id+' cdQjson '+showJson}); + } else { + el.createEl('pre', {text: JSON.stringify(json, null, 2), cls: 'QJSON-'+id+' cdQjson '+showJson}); + } qjCount = document.querySelectorAll('.cdQjson').length; statusBarItemEl.setText('QJSON: ' + qjCount); }); - this.registerEvent(this.app.workspace.on('editor-change', async (editor, info) => { - const cursor = editor.getCursor(); - const line = editor.getLine(cursor.line); + // this.registerEvent(this.app.workspace.on('editor-change', async (editor, info) => { + // const cursor = editor.getCursor(); + // const line = editor.getLine(cursor.line); - const lastChar = line[line.length - 1]; + // const lastChar = line[line.length - 1]; - const match = line.match(/@(.+)>(.+)|@(.+)>/); - if (!match) return; + // const match = line.match(/@(.+)>(.+)|@(.+)>/); + // if (!match) return; - if (lastChar !== ';' && lastChar !== '.' && lastChar !== '>') return; + // if (lastChar !== ';' && lastChar !== '.' && lastChar !== '>') return; - const id = match[1] || match[3]; - let path = ""; + // const id = match[1] || match[3]; + // let path = ""; - if (match[2] !== undefined) { - path = match[2].slice(0, -1).replace(/>/, '') - } + // if (match[2] !== undefined) { + // path = match[2].slice(0, -1).replace(/>/, '') + // } - let json; + // let json; - if (!isNaN(parseInt(id)) && !id.includes('.json')) { - const el = document.querySelector('.QJSON-' + id); - if (!el) return; - json = JSON.parse(el.innerText); - } else { - json = JSON.parse(await this.app.vault.adapter.read(id)); - } + // if (!isNaN(parseInt(id)) && !id.includes('.json')) { + // const el = document.querySelector('.QJSON-' + id); + // if (!el) return; + // json = JSON.parse(el.innerText); + // } else { + // json = JSON.parse(await this.app.vault.adapter.read(id)); + // } - const value = getJSONPath(json, path); + // const value = getJSONPath(json, path); - if (lastChar !== ';') { - if (value !== undefined) { - const notice = document.querySelector('.notice'); - if (notice) notice.remove(); + // if (lastChar !== ';') { + // if (value !== undefined) { + // const notice = document.querySelector('.notice'); + // if (notice) notice.remove(); - const keys = Object.keys(value); - const keysAreNumbers = keys.every(key => !isNaN(parseInt(key))); + // const keys = Object.keys(value); + // const keysAreNumbers = keys.every(key => !isNaN(parseInt(key))); - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - new Notice(value.toString()); - } else if (keysAreNumbers) { - new Notice('Total Keys: ' + (keys.length- 1 )); - } else { - new Notice('Total Keys: ' + keys.length + '\n' + '____________' + '\n' + keys.join('\n')); - } - } - } else { - const atIndex = line.indexOf('@'); - const replaceEnd = { line: cursor.line, ch: line.length }; // Replace to end of line - const stringValue = typeof value === 'string' ? value : JSON.stringify(value); - editor.replaceRange(stringValue, { line: cursor.line, ch: atIndex }, replaceEnd); - } - })); + // if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + // new Notice(value.toString()); + // } else if (keysAreNumbers) { + // new Notice('Total Keys: ' + (keys.length- 1 )); + // } else { + // new Notice('Total Keys: ' + keys.length + '\n' + '____________' + '\n' + keys.join('\n')); + // } + // } + // } else { + // const atIndex = line.indexOf('@'); + // const replaceEnd = { line: cursor.line, ch: line.length }; // Replace to end of line + // const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + // editor.replaceRange(stringValue, { line: cursor.line, ch: atIndex }, replaceEnd); + // } + // })); + + this.registerMarkdownPostProcessor( async (element, context) => { + const codeblocks = element.findAll("code"); + + for (let codeblock of codeblocks) { + if (codeblock.classList.length >= 1) return; + + const text = codeblock.innerText; + const regex = /@(.+)\.json>(.+);/; + const match = text.match(regex); + + if (match) { + let result; + + try { + let file = await this.app.vault.adapter.read(match[1] + ".json"); + file = JSON.parse(file); + result = getJSONPath(file, match[2]); + } catch (e) { + console.error(e); + new Notice("Error! Something went wrong!"); + result = "Error!"; + } + + let stringResult; + let tagName; + + if (typeof result === "string" || typeof result === "number" || typeof result === "boolean") { + stringResult = result; + tagName = "span"; + } else { + stringResult = JSON.stringify(result, null, 2); + tagName = "pre"; + } + + const resultEl = codeblock.createEl(tagName, {text: stringResult}); + codeblock.replaceWith(resultEl); + } + } + }); } }