diff --git a/CHANGELOG.md b/CHANGELOG.md index b08e122..6fa6123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.28.1 (2025-03-14) +- Adding syntax highlighting for javascript (to be used with GRAPH view) + # 0.28.0 (2025-03-14) - Added TEMPLATE view that allow to render your template with custom Handlebars template. - Improved syntax highlighting - now lines with errors will get highlighted with appropriate colour to indicate the issue diff --git a/manifest.json b/manifest.json index 7c98eb9..4a0d2f9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "sqlseal", "name": "SQLSeal", - "version": "0.28.0", + "version": "0.28.1", "minAppVersion": "0.15.0", "description": "Use SQL in your notes to query your vault files and CSV content.", "author": "hypersphere", diff --git a/package.json b/package.json index b0664d0..819209b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqlseal", - "version": "0.28.0", + "version": "0.28.1", "description": "A plugin for Obsidian that allows you to run SQL queries on your notes.", "main": "main.js", "scripts": { @@ -23,6 +23,8 @@ "author": "", "license": "MIT", "devDependencies": { + "@types/esprima": "^4.0.6", + "@types/estraverse": "^5.1.7", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.13", "@types/node": "^22.9.3", @@ -56,6 +58,8 @@ "ag-grid-community": "^32.3.3", "comlink": "^4.4.2", "esbuild-plugin-polyfill-node": "^0.3.0", + "esprima": "^4.0.1", + "estraverse": "^5.3.0", "handlebars": "^4.7.8", "json5": "^2.2.3", "jsonpath": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ab2acd..0044949 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,12 @@ importers: esbuild-plugin-polyfill-node: specifier: ^0.3.0 version: 0.3.0(esbuild@0.24.0) + esprima: + specifier: ^4.0.1 + version: 4.0.1 + estraverse: + specifier: ^5.3.0 + version: 5.3.0 handlebars: specifier: ^4.7.8 version: 4.7.8 @@ -78,6 +84,12 @@ importers: specifier: ^11.0.4 version: 11.0.4 devDependencies: + '@types/esprima': + specifier: ^4.0.6 + version: 4.0.6 + '@types/estraverse': + specifier: ^5.1.7 + version: 5.1.7 '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -1046,6 +1058,12 @@ packages: '@types/emscripten@1.39.13': resolution: {integrity: sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==} + '@types/esprima@4.0.6': + resolution: {integrity: sha512-lIk+kSt9lGv5hxK6aZNjiUEGZqKmOTpmg0tKiJQI+Ow98fLillxsiZNik5+RcP7mXL929KiTH/D9jGtpDlMbVw==} + + '@types/estraverse@5.1.7': + resolution: {integrity: sha512-JRVtdKYZz7VkNp7hMC/WKoiZ8DS3byw20ZGoMZ1R8eBrBPIY7iBaDAS1zcrnXQCwK44G4vbXkimeU7R0VLG8UQ==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1356,8 +1374,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true @@ -4225,6 +4243,14 @@ snapshots: '@types/emscripten@1.39.13': {} + '@types/esprima@4.0.6': + dependencies: + '@types/estree': 1.0.6 + + '@types/estraverse@5.1.7': + dependencies: + '@types/estree': 1.0.6 + '@types/estree@1.0.6': {} '@types/graceful-fs@4.1.9': @@ -4598,11 +4624,11 @@ snapshots: dependencies: safari-14-idb-fix: 1.0.6 - acorn-jsx@5.3.2(acorn@8.14.0): + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: - acorn: 8.14.0 + acorn: 8.14.1 - acorn@8.14.0: {} + acorn@8.14.1: {} ag-charts-types@10.3.3: {} @@ -5158,8 +5184,8 @@ snapshots: espree@10.3.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 esprima@1.2.2: {} diff --git a/src/editorExtension/syntaxHighlight.ts b/src/editorExtension/syntaxHighlight.ts index 36b885f..1a56b7c 100644 --- a/src/editorExtension/syntaxHighlight.ts +++ b/src/editorExtension/syntaxHighlight.ts @@ -23,6 +23,8 @@ const markDecorations = { blockTable: Decoration.mark({ class: 'cm-sqlseal-block-table' }), identifier: Decoration.mark({ class: 'cm-sqlseal-identifier' }), literal: Decoration.mark({ class: 'cm-sqlseal-literal' }), + number: Decoration.mark({ class: 'cm-sqlseal-literal' }), + string: Decoration.mark({ class: 'cm-sqlseal-literal' }), parameter: Decoration.mark({ class: 'cm-sqlseal-parameter' }), comment: Decoration.mark({ class: 'cm-sqlseal-comment' }), keyword: Decoration.mark({ class: 'cm-sqlseal-keyword' }), diff --git a/src/grammar/highlighter/jsHighlighter.ts b/src/grammar/highlighter/jsHighlighter.ts new file mode 100644 index 0000000..ea6b6ca --- /dev/null +++ b/src/grammar/highlighter/jsHighlighter.ts @@ -0,0 +1,357 @@ +import * as esprima from 'esprima'; +import * as estraverse from 'estraverse'; + +interface Decorator { + type: 'keyword' | 'identifier' | 'string' | 'string-escape' | 'string-interpolation' | + 'number' | 'comment' | 'function' | 'operator' | 'punctuation' | 'regex' | 'error'; + start: number; + end: number; +} + +/** + * Highlights JavaScript syntax using Esprima and estraverse + * @param source - The JavaScript source code to highlight + * @returns Array of decorator objects for syntax highlighting + */ +function highlightJavaScript(source: string): Decorator[] { + const decorators: Decorator[] = []; + + // Keywords in JavaScript + const keywords = new Set([ + 'var', 'let', 'const', 'if', 'else', 'switch', 'case', 'default', 'for', 'while', 'do', + 'break', 'continue', 'return', 'function', 'class', 'extends', 'new', 'this', 'super', + 'import', 'export', 'from', 'as', 'async', 'await', 'try', 'catch', 'finally', 'throw', + 'typeof', 'instanceof', 'in', 'of', 'void', 'delete', 'yield', 'static', 'get', 'set', + 'true', 'false', 'null', 'undefined', 'NaN', 'Infinity' + ]); + + // Helper function to add a decorator + function addDecorator(type: Decorator['type'], start: number, end: number): void { + if (start >= 0 && end > start && end <= source.length) { + decorators.push({ type, start, end }); + } + } + + try { + // Parse the source code with location information and comment handling + const ast = esprima.parseScript(source, { + loc: true, + range: true, + comment: true, + tokens: true, + jsx: true + }); + + // Process comments (they're separate from the AST in Esprima) + if (ast.comments) { + for (const comment of ast.comments) { + addDecorator('comment', comment.range![0], comment.range![1]); + } + } + + // Process tokens for basic syntax elements + if (ast.tokens) { + for (const token of ast.tokens) { + switch (token.type) { + case 'Keyword': + addDecorator('keyword', token.range[0], token.range[1]); + break; + case 'Identifier': + // We'll handle function identifiers separately + addDecorator('identifier', token.range[0], token.range[1]); + break; + case 'Punctuator': + if (['+', '-', '*', '/', '%', '=', '>', '<', '!', '&', '|', '^', '~', '?', ':'].includes(token.value) || + token.value.length > 1 && /^[+\-*/%=>= 0 && source.substring(exprStart, exprStart + 2) === '${') { + addDecorator('string-interpolation', exprStart, exprStart + 2); + + // The expression itself will be handled by the normal traversal + + // Find the closing } + const closingBrace = expr.range[1]; // The end range should be right after the expression + if (closingBrace < source.length && source[closingBrace] === '}') { + addDecorator('string-interpolation', closingBrace, closingBrace + 1); + } + } + } + break; + } + } + }); + + // Sort decorators by start position + return decorators.sort((a, b) => a.start - b.start); + } catch (error) { + // If parsing fails, return an error decorator + console.error('Parsing error:', error); + return [{ type: 'error', start: 0, end: source.length }]; + } +} + +// Helper function to apply highlighting to HTML +function applyHighlighting(source: string, decorators: Decorator[]): string { + let html = ''; + let lastIndex = 0; + + // First, resolve overlapping decorators by sorting and removing overlaps + // We prioritize more specific types (like string-escape) over more general types (like string) + const resolvedDecorators = resolveOverlappingDecorators(decorators); + + for (const decorator of resolvedDecorators) { + // Add any text before this decorator + html += escapeHtml(source.substring(lastIndex, decorator.start)); + + // Add the decorated text + const content = escapeHtml(source.substring(decorator.start, decorator.end)); + html += `${content}`; + + lastIndex = decorator.end; + } + + // Add any remaining text + html += escapeHtml(source.substring(lastIndex)); + + return html; +} + +// Helper function to resolve overlapping decorators +function resolveOverlappingDecorators(decorators: Decorator[]): Decorator[] { + // First, sort by start position and then by length (shortest first) + const sorted = [...decorators].sort((a, b) => { + if (a.start !== b.start) return a.start - b.start; + return (a.end - a.start) - (b.end - b.start); + }); + + // Then resolve overlaps by splitting overlapping decorators + const result: Decorator[] = []; + let lastEnd = 0; + + for (const decorator of sorted) { + // Skip if this decorator is completely contained in the previous one + if (decorator.start >= lastEnd) { + result.push(decorator); + lastEnd = decorator.end; + } else if (decorator.end > lastEnd) { + // Partial overlap - only add the non-overlapping part + if (decorator.start < lastEnd && decorator.end > lastEnd) { + result.push({ + type: decorator.type, + start: lastEnd, + end: decorator.end + }); + lastEnd = decorator.end; + } + } + // Completely contained - skip it + } + + return result; +} + +// Helper function to escape HTML special characters +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export { highlightJavaScript, applyHighlighting }; + +// Usage example: +// import { highlightJavaScript, applyHighlighting } from './esprima-js-highlighter'; +// +// const source = ` +// // This is a comment +// function calculateTotal(items) { +// return items +// .map(item => item.price * item.quantity) +// .reduce((total, value) => total + value, 0); +// } +// +// class ShoppingCart { +// constructor() { +// this.items = []; +// } +// +// addItem(item) { +// this.items.push(item); +// } +// +// get total() { +// return calculateTotal(this.items); +// } +// } +// +// export default ShoppingCart; +// `; +// +// const decorators = highlightJavaScript(source); +// const html = applyHighlighting(source, decorators); +// document.getElementById('code-display').innerHTML = html; \ No newline at end of file diff --git a/src/grammar/highlighterOperation.ts b/src/grammar/highlighterOperation.ts index c004e16..b4c153a 100644 --- a/src/grammar/highlighterOperation.ts +++ b/src/grammar/highlighterOperation.ts @@ -1,6 +1,7 @@ import * as ohm from 'ohm-js'; import { cstVisitor, Literal, parse } from 'sql-parser-cst'; import highlightHandlebars from './highlighter/handlebarsHighlighter'; +import { highlightJavaScript } from './highlighter/jsHighlighter'; const nodes = new Map([ ['FlagExpression', { terminal: true, type: 'blockFlag' }], @@ -93,6 +94,34 @@ export const highlighterOperation = (grammar: ohm.Grammar) => { } catch (e) { return [] } + }, + javascriptTemplate(_node) { + let prefix = `async function x () { + ` + let sufix = ' }' + + + if (this.source.contents.trim()[0] === '{') { + // we are parsing simple object, we need to adjust it + prefix = `const data = ` + sufix = ';' + } + + const source = prefix + this.source.contents + sufix + const offset = this.source.startIdx + try { + const decorators = highlightJavaScript(source) + return decorators.map(d => ({ + type: d.type === 'keyword' ? 'template-keyword' : d.type, + start: d.start - prefix.length + offset, + end: d.end - prefix.length + offset + })).filter(x => x.start >= offset) + } catch (error) { + console.error(error) + // If parsing fails, return an error decorator + // return [{ type: 'error', start: 0, end: source.length }]; + return [{ type: 'error', start: this.source.startIdx, end: this.source.endIdx }] + } } }) diff --git a/src/grammar/parser.ts b/src/grammar/parser.ts index 52efb31..f71d2e9 100644 --- a/src/grammar/parser.ts +++ b/src/grammar/parser.ts @@ -50,6 +50,7 @@ export const SQLSealLangDefinition = (views: ViewDefinition[], flags: readonly F ExtraFlags = ${flagsDefinitions} anyObject = "{" (~selectKeyword any)* handlebarsTemplate = (~selectKeyword any)* + javascriptTemplate = (~selectKeyword any)* selectKeyword = caseInsensitive<"WITH"> | caseInsensitive<"SELECT"> tableKeyword = caseInsensitive<"TABLE"> nl = "\n" @@ -143,6 +144,7 @@ const generateSemantic = (grammar: ohm.Grammar) => { if ((grammar.rules['ExtraFlags'].body as any).ruleName) { operations.ExtraFlags = (flag) => { const key = flag.ctorName.substring('ExtraFlags_'.length) + console.log('FLAG PARSING', flag.ctorName, key) return { [key]: true } } } diff --git a/versions.json b/versions.json index 4163646..df6275f 100644 --- a/versions.json +++ b/versions.json @@ -49,5 +49,6 @@ "0.25.0": "0.15.0", "0.26.0": "0.15.0", "0.27.0": "0.15.0", - "0.28.0": "0.15.0" + "0.28.0": "0.15.0", + "0.28.1": "0.15.0" } \ No newline at end of file