diff --git a/CHANGELOG.md b/CHANGELOG.md index e4093ec..73e6f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.8.0 +Now you can embed links and images (both local and external). Introduced `a` and `img` custom SQL functions. + # 0.7.0 A lot of changes packed in this one! Now SQLSeal is parsing the input using proper language parser (thanks to ANTLR4) rather than relying on RegExes. This finally allows for proper support of CTE statements (WITH) and fixes a lot of other minor problems with the syntax. Also improved the way files are being observed and updated which should lead to performance improvements. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 26e7748..a891b7d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -19,6 +19,7 @@ export default defineConfig({ { text: 'Quick Start', link: '/quick-start' }, { text: 'Using properties', link: '/using-properties' }, { text: 'Query Vault Content', link: '/query-vault-content' }, + { text: 'Links and Images', link: '/links-and-images' }, { text: 'Troubleshooting', link: '/troubleshooting' }, { text: 'Future Plans', link: '/future-plans' }, ] diff --git a/docs/links-and-images-advanced.png b/docs/links-and-images-advanced.png new file mode 100644 index 0000000..7f7f3e8 Binary files /dev/null and b/docs/links-and-images-advanced.png differ diff --git a/docs/links-and-images.md b/docs/links-and-images.md new file mode 100644 index 0000000..51f46f8 --- /dev/null +++ b/docs/links-and-images.md @@ -0,0 +1,53 @@ +# Links and Images +Introduced in version 0.8. Make sure you are using up to date version. + +SQLSeal allows for rendering links and images. For now the images needs to be external ones (no support for stored images for now but should be added in the future releases). + +## Links +To display a link, wrap use the `a` SQL function, for example: +```sql +SELECT a(path) FROM files LIMIT 10 +``` +![Example of links](./links.png) + +You can use second parameter to provide name for the link: + +```sql +SELECT a(path, name) from files LIMIT 10 +``` + + +This API works for both filesystem and CSV files. + +## Images +You can embed images within your results. You need to wrap your resulting column with `img` function. + +```sql +SELECT name, img(coverImg) FROM files +``` + +### Local images +When using local images (stored in Obsidian) you need to pass second parameter being path of the original note. For example: + +```sql +SELECT path, img(cover, path) FROM files +``` + +## Advanced Examples +The example below uses [Goodreads-books](https://www.kaggle.com/datasets/jealousleopard/goodreadsbooks) Kaggle dataset in CSV loaded in obsidian to display books with links to Open Library and showing the covers from Open Library Cover API. It uses Obsidian property to filter the author name. + + +```sql +TABLE books = file(books.csv) + +SELECT +a('https://openlibrary.org/isbn/' || isbn13, title) as title, +authors, +isbn13, +img('https://covers.openlibrary.org/b/isbn/' || isbn13 || '-L.jpg') as cover +from books +WHERE authors LIKE concat('%', @author, '%') +LIMIT 10 +``` + +![Advanced links and images](links-and-images-advanced.png) \ No newline at end of file diff --git a/docs/links.png b/docs/links.png new file mode 100644 index 0000000..be75e2e Binary files /dev/null and b/docs/links.png differ diff --git a/manifest.json b/manifest.json index 8a539a4..c8379b4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "sqlseal", "name": "SQLSeal", - "version": "0.7.0", + "version": "0.8.0", "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 768d667..aeba1c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqlseal", - "version": "0.7.0", + "version": "0.8.0", "description": "A plugin for Obsidian that allows you to run SQL queries on your notes.", "main": "main.js", "scripts": { diff --git a/src/SqlSealCodeblockHandler.ts b/src/SqlSealCodeblockHandler.ts index 2795bf0..8127de6 100644 --- a/src/SqlSealCodeblockHandler.ts +++ b/src/SqlSealCodeblockHandler.ts @@ -75,7 +75,7 @@ export class SqlSealCodeblockHandler { const stmt = this.db.db.prepare(statement) const columns = stmt.columns().map(column => column.name); const data = stmt.all(frontmatter ?? {}) - displayData(el, columns, data) + displayData(el, columns, data, this.app) } catch (e) { displayError(el, e) } diff --git a/src/database.ts b/src/database.ts index c0300b0..9ea2b5e 100644 --- a/src/database.ts +++ b/src/database.ts @@ -53,6 +53,8 @@ export class SqlSealDatabase { const dbPath = path.resolve(this.app.vault.adapter.basePath, this.app.vault.configDir, `plugins/sqlseal/database.sqlite`) this.db = new Database(dbPath, { verbose: this.verbose ? console.log : undefined }) + await this.defineCustomFunctions() + this.isConnected = true this.connectingPromiseResolve() @@ -68,6 +70,44 @@ export class SqlSealDatabase { } } + private async defineCustomFunctions() { + this.db.function('a', (href: string, name: string) => { + const linkObject = { + type: 'link', + href: href, + name: name || href + }; + return `SQLSEALCUSTOM(${JSON.stringify(linkObject)})`; + }); + + this.db.function('a', (href: string) => { + const linkObject = { + type: 'link', + href: href, + name: href + }; + return `SQLSEALCUSTOM(${JSON.stringify(linkObject)})`; + }); + + this.db.function('img', (href: string) => { + const imgObject = { + type: 'img', + href: href + } + return `SQLSEALCUSTOM(${JSON.stringify(imgObject)})` + }) + + this.db.function('img', (href: string, path: string) => { + const imgObject = { + type: 'img', + path, + href + } + return `SQLSEALCUSTOM(${JSON.stringify(imgObject)})` + }) + + } + async disconect() { if (!this.isConnected) { return diff --git a/src/ui.ts b/src/ui.ts index cf8068b..1097895 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,4 +1,6 @@ -export const displayData = (el: HTMLElement, columns, data) => { +import { App } from "obsidian" + +export const displayData = (el: HTMLElement, columns, data, app: App) => { el.empty() const container = el.createDiv({ cls: 'sqlseal-table-container' @@ -15,12 +17,94 @@ export const displayData = (el: HTMLElement, columns, data) => { data.forEach(d => { const row = body.createEl("tr") columns.forEach(c => { - row.createEl("td", { text: d[c] }) + row.createEl("td", { text: parseCell(d[c], app) }) }) }) } +const parseCell = (data: string, app: App) => { + if (data && typeof data === 'string' && data.startsWith('SQLSEALCUSTOM')) { + const parsedData = JSON.parse(data.slice('SQLSEALCUSTOM('.length, -1)) + return renderSqlSealCustomElement(parsedData, app) + } + return data +} + +type SqlSealAnchorElement = { + type: 'link', + href: string, + name: string +} + +type SQLSealImgElement = { + type: 'img', + path?: string, + href: string +} + +type SqlSealCustomElement = SqlSealAnchorElement | SQLSealImgElement; + +const isLinkLocal = (link: string) => !link?.trim().startsWith('http') + +const generateLink = (config: SqlSealAnchorElement, app: App) => { + let href = config.href + if (isLinkLocal(config.href)) { + const link = createEl('a', { + text: config.name, + cls: 'internal-link' // This class is used by Obsidian for internal links + }); + + link.addEventListener('click', (event) => { + event.preventDefault(); + + // Open the file in the active leaf (same tab) + const leaf = app.workspace.getLeaf(); + const file = app.vault.getFileByPath(config.href) + if (!file) { + return + } + leaf.openFile(file); + }); + const encodedPath = encodeURIComponent(config.href); + // Create the Obsidian URI + href = `app://obsidian.md/${encodedPath}`; + return link + } + const el = createEl('a', { text: config.name, href: href }) + return el +} + +const renderSqlSealCustomElement = (customConfig: SqlSealCustomElement, app: App) => { + switch (customConfig.type) { + case 'link': + return generateLink(customConfig, app) + case 'img': + if (!isLinkLocal(customConfig.href)) { + return createEl('img', { attr: { src: customConfig.href } }); + } + let href = (customConfig.href ?? '').trim() + if (href.startsWith('![[')) { + href = href.slice(3, -2) + } + let parentPath = '' + if (customConfig.path) { + const parent = app.vault.getFileByPath(customConfig.path) + if (parent) { + parentPath = parent.parent?.path ?? '' + } + } + const path = (customConfig?.path ? parentPath + '/' : '') + href + const file = app.vault.getFileByPath(path); + if (!file) { + return 'File does not exist' + } + return createEl('img', { attr: { src: app.vault.getResourcePath(file) } }); + default: + return 'Invalid Custom Element' + } +} + export const displayError = (el: HTMLElement, e: Error) => { el.empty() const callout = el.createEl("div", { text: e.toString(), cls: 'callout' }) diff --git a/versions.json b/versions.json index 291f00d..19ebccb 100644 --- a/versions.json +++ b/versions.json @@ -6,5 +6,6 @@ "0.4.1": "0.15.0", "0.5.0": "0.15.0", "0.6.0": "0.15.0", - "0.7.0": "0.15.0" + "0.7.0": "0.15.0", + "0.8.0": "0.15.0" } \ No newline at end of file