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
This commit is contained in:
rooyca 2024-05-21 21:12:33 -05:00
parent 5d6f8ee710
commit 451a5bcd27
6 changed files with 255 additions and 46 deletions

2
.gitignore vendored
View file

@ -3,7 +3,7 @@
# Intellij
*.iml
.idea
.idea*
# npm
node_modules

View file

@ -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.

View file

@ -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",

View file

@ -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": {

121
src/functions.ts Normal file
View file

@ -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;
}
}

View file

@ -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);
}
}
});
}
}