Merge pull request #8 from h-sphere/feat/custom-sql-functions-for-links-and-images

Feat/custom sql functions for links and images
This commit is contained in:
Kacper Kula 2024-09-05 21:04:20 +01:00 committed by GitHub
commit cf0984d29a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 188 additions and 6 deletions

View file

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

View file

@ -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' },
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

53
docs/links-and-images.md Normal file
View file

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

BIN
docs/links.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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