mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
Merge pull request #134 from h-sphere/feat/new-cell-renderer
feat: new cell parser
This commit is contained in:
commit
3a1fcaccce
26 changed files with 494 additions and 330 deletions
|
|
@ -1,3 +1,5 @@
|
|||
# 0.31.0 (2025-03-31)
|
||||
- feat: TEMPLATE renderer can now use checkboxes, links and images like other views.
|
||||
# 0.30.1 (2025-03-27)
|
||||
- feat: added basename and parent SQL variables
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "sqlseal",
|
||||
"name": "SQLSeal",
|
||||
"version": "0.30.1",
|
||||
"version": "0.31.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Use SQL in your notes to query your vault files and CSV content.",
|
||||
"author": "hypersphere",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sqlseal",
|
||||
"version": "0.30.1",
|
||||
"version": "0.31.0",
|
||||
"description": "A plugin for Obsidian that allows you to run SQL queries on your notes.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import { Database } from "sql.js";
|
||||
import { parse } from 'json5'
|
||||
import { SqlSealDatabase } from "./database/database";
|
||||
import { isStringifiedArray, renderStringifiedArray } from "./utils/ui";
|
||||
|
||||
export interface CellParser {
|
||||
render: (content: string) => Node | string;
|
||||
}
|
||||
|
||||
export class CellParserRegistar implements CellParser {
|
||||
functions: Map<string, any> = new Map()
|
||||
constructor(private db: SqlSealDatabase) {
|
||||
|
||||
}
|
||||
|
||||
register(name: string, fn: any, argsCount = 1) {
|
||||
if (this.functions.has(name)) {
|
||||
throw new Error('Function with that name already exists: ' + name)
|
||||
}
|
||||
this.functions.set(name, fn)
|
||||
this.db.registerCustomFunction(name, argsCount)
|
||||
}
|
||||
|
||||
unregister(name: string) {
|
||||
this.functions.delete(name)
|
||||
}
|
||||
|
||||
private renderCustomElement(el: any): Node | string {
|
||||
if (this.functions.has(el.type)) {
|
||||
return this.functions.get(el.type)!(el.values)
|
||||
} else {
|
||||
throw new Error(`Custom function processor ${el.type} is not registered`)
|
||||
}
|
||||
}
|
||||
|
||||
render(content: string) {
|
||||
try {
|
||||
if (!content) {
|
||||
return content
|
||||
}
|
||||
if (typeof content !== 'string') {
|
||||
return content
|
||||
}
|
||||
if (content.startsWith('SQLSEALCUSTOM')) {
|
||||
const parsedData = parse(content.slice('SQLSEALCUSTOM('.length, -1))
|
||||
return this.renderCustomElement(parsedData)
|
||||
}
|
||||
|
||||
// If it's an array, decode it and render each individual elements
|
||||
if (isStringifiedArray(content)) {
|
||||
try {
|
||||
return renderStringifiedArray(content)
|
||||
} catch (e) {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
} catch (e) {
|
||||
console.error('Error parsing cell with data:', content, e)
|
||||
return createDiv({
|
||||
text: 'Parsing error',
|
||||
cls: 'sqlseal-parse-error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/cellParser/CellFunction.ts
Normal file
7
src/cellParser/CellFunction.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { CellParserResult } from "./ModernCellParser";
|
||||
|
||||
export interface CellFunction<T = unknown> {
|
||||
get name(): string;
|
||||
get sqlFunctionArgumentsCount(): number;
|
||||
prepare(content: T): CellParserResult;
|
||||
}
|
||||
93
src/cellParser/ModernCellParser.ts
Normal file
93
src/cellParser/ModernCellParser.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { CellFunction } from "./CellFunction";
|
||||
import { parse } from 'json5'
|
||||
import { isStringifiedArray, renderStringifiedArray } from '../utils/ui'
|
||||
import { SqlSealDatabase } from "src/database/database";
|
||||
|
||||
export type UnregisterCallback = () => void;
|
||||
|
||||
export type OnRunCallback = (el: HTMLElement) => void | UnregisterCallback;
|
||||
export interface Result {
|
||||
element: HTMLElement,
|
||||
onRunCallback?: OnRunCallback
|
||||
}
|
||||
|
||||
export type CellParserResult = Result | string | HTMLElement
|
||||
|
||||
export class ModernCellParser {
|
||||
|
||||
private functions: Map<string, CellFunction> = new Map()
|
||||
|
||||
prepare(content: string): CellParserResult {
|
||||
try {
|
||||
if (!content) {
|
||||
return content
|
||||
}
|
||||
if (typeof content !== 'string') {
|
||||
return content
|
||||
}
|
||||
if (content.startsWith('SQLSEALCUSTOM')) {
|
||||
const parsedData = parse(content.slice('SQLSEALCUSTOM('.length, -1))
|
||||
// FIXME: zod or arctype here.
|
||||
return this.renderCustomElement(parsedData)
|
||||
}
|
||||
|
||||
// If it's an array, decode it and render each individual elements
|
||||
if (isStringifiedArray(content)) {
|
||||
try {
|
||||
return renderStringifiedArray(content)
|
||||
} catch (e) {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
} catch (e) {
|
||||
console.error('Error parsing cell with data:', content, e)
|
||||
return createDiv({
|
||||
text: 'Parsing error',
|
||||
cls: 'sqlseal-parse-error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render(content: string) {
|
||||
const res = this.prepare(content)
|
||||
if (typeof res === 'string' || !res) {
|
||||
return res
|
||||
} else if (res instanceof HTMLElement) {
|
||||
return res
|
||||
} else {
|
||||
if (res.onRunCallback) {
|
||||
res.onRunCallback(res.element)
|
||||
}
|
||||
return res.element
|
||||
}
|
||||
}
|
||||
|
||||
register(fn: CellFunction) {
|
||||
this.functions.set(fn.name, fn)
|
||||
}
|
||||
|
||||
unregister(fn: string | CellFunction) {
|
||||
if (typeof fn === 'string') {
|
||||
this.functions.delete(fn)
|
||||
} else {
|
||||
this.functions.delete(fn.name)
|
||||
}
|
||||
}
|
||||
|
||||
private renderCustomElement(el: any) {
|
||||
if (this.functions.has(el.type)) {
|
||||
return this.functions.get(el.type)!.prepare(el.values)
|
||||
} else {
|
||||
throw new Error(`Custom function processor ${el.type} is not registered`)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: this should be extracted to separate class / function but for now it's fine.
|
||||
registerDbFunctions(db: SqlSealDatabase) {
|
||||
this.functions.forEach(funct => {
|
||||
db.registerCustomFunction(funct.name, funct.sqlFunctionArgumentsCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
62
src/cellParser/RenderContext.ts
Normal file
62
src/cellParser/RenderContext.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// This is render context which collects all the parsed elements and maintains overvability of them.
|
||||
|
||||
import { ModernCellParser, OnRunCallback, UnregisterCallback } from "./ModernCellParser";
|
||||
|
||||
class RenderContext {
|
||||
|
||||
private isContextActive: boolean = false;
|
||||
private contextActivateCalbacks: (() => ReturnType<OnRunCallback>)[] = []
|
||||
private contextDeactivateCallbacks: UnregisterCallback[] = []
|
||||
|
||||
constructor(private readonly cellParser: ModernCellParser) {
|
||||
|
||||
}
|
||||
|
||||
render(context: string) {
|
||||
const res = this.cellParser.prepare(context)
|
||||
if (typeof res === 'string') {
|
||||
return res
|
||||
}
|
||||
if (res instanceof HTMLElement) {
|
||||
return res
|
||||
}
|
||||
|
||||
if (res.onRunCallback) {
|
||||
// FIXME: not sure if this is the proper element, this might get recreated inside the handlebars. To check.
|
||||
this.contextActivateCalbacks.push(res.onRunCallback.apply(res.element, [res.element]))
|
||||
}
|
||||
return res.element
|
||||
}
|
||||
|
||||
activate() {
|
||||
if (this.isContextActive) {
|
||||
throw new Error('Context already active')
|
||||
}
|
||||
this.isContextActive = true
|
||||
for (const cb of this.contextActivateCalbacks) {
|
||||
const res = cb()
|
||||
if (res) {
|
||||
this.contextDeactivateCallbacks.push(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
if (!this.isContextActive) {
|
||||
throw new Error('Context not active')
|
||||
}
|
||||
this.isContextActive = false
|
||||
for (const cb of this.contextDeactivateCallbacks) {
|
||||
cb()
|
||||
}
|
||||
this.contextDeactivateCallbacks = []
|
||||
}
|
||||
|
||||
clear() {
|
||||
try {
|
||||
this.deactivate()
|
||||
} catch (e) { }
|
||||
|
||||
this.contextActivateCalbacks = []
|
||||
}
|
||||
}
|
||||
13
src/cellParser/factory.ts
Normal file
13
src/cellParser/factory.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { App } from "obsidian"
|
||||
import { ModernCellParser } from "./ModernCellParser"
|
||||
import { LinkParser } from "./parser/link"
|
||||
import { ImageParser } from "./parser/image"
|
||||
import { CheckboxParser } from "./parser/checkbox"
|
||||
|
||||
export const getCellParser = (app: App, create = createEl) => {
|
||||
const cellParser = new ModernCellParser()
|
||||
cellParser.register(new LinkParser(app, create))
|
||||
cellParser.register(new ImageParser(app, create))
|
||||
cellParser.register(new CheckboxParser(app, create))
|
||||
return cellParser
|
||||
}
|
||||
63
src/cellParser/parseResults.ts
Normal file
63
src/cellParser/parseResults.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { identity, uniqueId } from "lodash";
|
||||
import { ModernCellParser } from "./ModernCellParser";
|
||||
|
||||
type OnSerialisation = (el: HTMLElement) => any;
|
||||
|
||||
export class ParseResults {
|
||||
|
||||
functions: ((parentEl: HTMLElement) => void)[] = []
|
||||
|
||||
constructor(
|
||||
private readonly cellParser: ModernCellParser,
|
||||
private readonly serialise?: OnSerialisation
|
||||
) { }
|
||||
|
||||
parse(data: Record<string, any>[], columns: string[]) {
|
||||
this.functions = []
|
||||
return data.map(d => {
|
||||
const res: any = {}
|
||||
for (const col of columns) {
|
||||
const data = this.cellParser.prepare(d[col])
|
||||
if (data instanceof Element) {
|
||||
if (this.serialise) {
|
||||
res[col] = this.serialise(data)
|
||||
} else {
|
||||
res[col] = data
|
||||
}
|
||||
} else if (typeof data === 'string') {
|
||||
res[col] = data
|
||||
} else if (!data) {
|
||||
res[col] = ''
|
||||
} else {
|
||||
if (!this.serialise) {
|
||||
res[col] = data.element
|
||||
this.functions.push((_parentEl) => {
|
||||
if (data.onRunCallback) {
|
||||
data.onRunCallback(data.element)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const id = uniqueId('sqlseal__')
|
||||
data.element.id = id
|
||||
res[col] = this.serialise(data.element)
|
||||
this.functions.push((parentEl: HTMLElement) => {
|
||||
const resultingElement = parentEl.find('#' + id)
|
||||
if (!resultingElement) {
|
||||
return
|
||||
}
|
||||
if (data.onRunCallback) {
|
||||
data.onRunCallback(resultingElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
initialise(parentEl: HTMLElement) {
|
||||
this.functions.forEach(fn => fn(parentEl))
|
||||
}
|
||||
}
|
||||
93
src/cellParser/parser/checkbox.ts
Normal file
93
src/cellParser/parser/checkbox.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { CellFunction } from "../CellFunction";
|
||||
import { CellParserResult } from "../ModernCellParser";
|
||||
import { App } from "obsidian";
|
||||
|
||||
type Args = number[] | CheckboxProps
|
||||
|
||||
interface CheckboxProps {
|
||||
checked: boolean,
|
||||
path: string,
|
||||
position: {
|
||||
line: number,
|
||||
lineContent: string
|
||||
},
|
||||
task: string
|
||||
}
|
||||
|
||||
const isCheckboxProp = (arg: any): arg is CheckboxProps => {
|
||||
return arg && arg['position'] && arg['path']
|
||||
}
|
||||
|
||||
export class CheckboxParser implements CellFunction<Args> {
|
||||
|
||||
constructor(private readonly app: App, private readonly create = createEl) { }
|
||||
|
||||
get name(): string {
|
||||
return 'checkbox'
|
||||
}
|
||||
|
||||
get sqlFunctionArgumentsCount() {
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare(values: Args): CellParserResult {
|
||||
if (!isCheckboxProp(values)) {
|
||||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
attr: {
|
||||
disabled: true,
|
||||
checked: !!values[0] ? true : null
|
||||
}
|
||||
});
|
||||
return el
|
||||
}
|
||||
|
||||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
attr: {
|
||||
checked: !!(values && values.checked) ? true : null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
element: el,
|
||||
onRunCallback: (el: HTMLInputElement) => {
|
||||
el.addEventListener('change', async () => {
|
||||
try {
|
||||
// Values should contain the data we need
|
||||
if (!values || !values.path) {
|
||||
console.error('Missing required checkbox data')
|
||||
return
|
||||
}
|
||||
|
||||
const file = this.app.vault.getFileByPath(values.path)
|
||||
if (!file) {
|
||||
console.error('File not found:', values.path)
|
||||
return
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = await this.app.vault.read(file)
|
||||
const lines = content.split('\n')
|
||||
|
||||
// Get the line containing the task
|
||||
const lineIndex = values.position?.line
|
||||
if (lineIndex >= 0 && lineIndex < lines.length) {
|
||||
// Update the task status
|
||||
if (el.checked) {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [ ]', '- [x]')
|
||||
} else {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [x]', '- [ ]')
|
||||
}
|
||||
|
||||
// Write back to the file
|
||||
await this.app.vault.modify(file, lines.join('\n'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/cellParser/parser/image.ts
Normal file
42
src/cellParser/parser/image.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { CellFunction } from "../CellFunction";
|
||||
import { CellParserResult } from "../ModernCellParser";
|
||||
import { App } from "obsidian";
|
||||
import { isLinkLocal } from "src/utils/ui/helperFunctions";
|
||||
|
||||
type Args = [string] | [string, string]
|
||||
|
||||
export class ImageParser implements CellFunction<Args> {
|
||||
|
||||
constructor(private readonly app: App, private readonly create = createEl) { }
|
||||
|
||||
get name(): string {
|
||||
return 'img'
|
||||
}
|
||||
|
||||
get sqlFunctionArgumentsCount() {
|
||||
return 2 // FIXME: should it be 1 or 2?
|
||||
}
|
||||
|
||||
prepare([href, path]: Args): CellParserResult {
|
||||
if (!isLinkLocal(href)) {
|
||||
return createEl('img', { attr: { src: href } });
|
||||
}
|
||||
href = (href ?? '').trim()
|
||||
if (href.startsWith('![[')) {
|
||||
href = href.slice(3, -2)
|
||||
}
|
||||
let parentPath = ''
|
||||
if (path) {
|
||||
const parent = this.app.vault.getFileByPath(path)
|
||||
if (parent) {
|
||||
parentPath = parent.parent?.path ?? ''
|
||||
}
|
||||
}
|
||||
path = (path ? parentPath + '/' : '') + href
|
||||
const file = this.app.vault.getFileByPath(path);
|
||||
if (!file) {
|
||||
return 'File does not exist'
|
||||
}
|
||||
return this.create('img', { attr: { src: this.app.vault.getResourcePath(file) } });
|
||||
}
|
||||
}
|
||||
68
src/cellParser/parser/link.ts
Normal file
68
src/cellParser/parser/link.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { isStringifiedArray, renderStringifiedArray } from "../../utils/ui";
|
||||
import { CellFunction } from "../CellFunction";
|
||||
import { CellParserResult, Result } from "../ModernCellParser";
|
||||
import { App } from "obsidian";
|
||||
import { isLinkLocal, isLinktext, removeExtension } from "src/utils/ui/helperFunctions";
|
||||
|
||||
type Args = [string] | [string, string]
|
||||
|
||||
export class LinkParser implements CellFunction<Args> {
|
||||
|
||||
constructor(private readonly app: App, private readonly create = createEl) { }
|
||||
|
||||
get name(): string {
|
||||
return 'a'
|
||||
}
|
||||
|
||||
get sqlFunctionArgumentsCount() {
|
||||
return 2 // FIXME: should it be 1 or 2?
|
||||
}
|
||||
|
||||
prepare([href, name]: Args) {
|
||||
if (!href) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isStringifiedArray(href)) {
|
||||
try {
|
||||
renderStringifiedArray(href, href => this.prepare([href]))
|
||||
} catch (e) {
|
||||
return href
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = href
|
||||
}
|
||||
href = href.trim()
|
||||
let cls = ''
|
||||
if (isLinktext(href)) {
|
||||
const [path, linkName] = href.slice(2, -2).split('|')
|
||||
const link = this.app.metadataCache.getFirstLinkpathDest(path, '')
|
||||
|
||||
if (name.trim() === href) {
|
||||
name = linkName ?? path
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
href = path
|
||||
} else {
|
||||
href = link.path
|
||||
}
|
||||
cls = 'internal-link'
|
||||
|
||||
} else if (isLinkLocal(href)) {
|
||||
let fileHref = href
|
||||
if (fileHref.endsWith('.md')) {
|
||||
fileHref = fileHref.slice(0, -3)
|
||||
}
|
||||
if (name === href) {
|
||||
const components = href.split('/')
|
||||
name = removeExtension(components[components.length - 1])
|
||||
}
|
||||
cls = 'internal-link'
|
||||
}
|
||||
const el = this.create('a', { text: name, href, cls })
|
||||
return el
|
||||
}
|
||||
}
|
||||
32
src/main.ts
32
src/main.ts
|
|
@ -10,24 +10,30 @@ import { CSV_VIEW_EXTENSIONS, CSV_VIEW_TYPE, CSVView } from './view/CSVView';
|
|||
import { createSqlSealEditorExtension } from './editorExtension/inlineCodeBlock';
|
||||
import { ListRenderer } from './renderer/ListRenderer';
|
||||
import { JSON_VIEW_EXTENSIONS, JSON_VIEW_TYPE, JsonView } from './view/JsonView';
|
||||
import { CellParserRegistar } from './cellParser';
|
||||
import { registerAPI } from '@vanakat/plugin-api';
|
||||
import { SQLSealRegisterApi } from './pluginApi/sqlSealApi';
|
||||
import { registerDefaultFunctions } from './utils/ui';
|
||||
import { DatabaseTable } from './database/table';
|
||||
import { FilepathHasher } from './utils/hasher';
|
||||
import { SQLSealViewPlugin } from './editorExtension/syntaxHighlight';
|
||||
import { EditorView, ViewPlugin } from '@codemirror/view';
|
||||
import { TemplateRenderer } from './renderer/TemplateRenderer';
|
||||
import { ModernCellParser } from './cellParser/ModernCellParser';
|
||||
import { getCellParser } from './cellParser/factory';
|
||||
import { CellFunction } from './cellParser/CellFunction';
|
||||
|
||||
export default class SqlSealPlugin extends Plugin {
|
||||
settings: SQLSealSettings;
|
||||
fileSync: SealFileSync;
|
||||
sqlSeal: SqlSeal;
|
||||
rendererRegistry: RendererRegistry = new RendererRegistry();
|
||||
cellParserRegistar: CellParserRegistar;
|
||||
|
||||
cellParser: ModernCellParser;
|
||||
|
||||
async onload() {
|
||||
// Preparing cell parser
|
||||
this.cellParser = getCellParser(this.app, createEl)
|
||||
|
||||
|
||||
await this.loadSettings();
|
||||
const settingsTab = new SQLSealSettingsTab(this.app, this, this.settings)
|
||||
this.addSettingTab(settingsTab);
|
||||
|
|
@ -47,16 +53,16 @@ export default class SqlSealPlugin extends Plugin {
|
|||
this.sqlSeal = sqlSeal
|
||||
|
||||
await this.sqlSeal.db.connect()
|
||||
this.cellParserRegistar = new CellParserRegistar(this.sqlSeal.db)
|
||||
registerDefaultFunctions(this.cellParserRegistar, this.app)
|
||||
|
||||
this.cellParser.registerDbFunctions(this.sqlSeal.db)
|
||||
|
||||
// REGISTERING VIEWS
|
||||
|
||||
this.rendererRegistry.register('sql-seal-internal-table', new TableRenderer(this.app, this.cellParserRegistar))
|
||||
this.rendererRegistry.register('sql-seal-internal-grid', new GridRenderer(this.app, this, this.cellParserRegistar))
|
||||
this.rendererRegistry.register('sql-seal-internal-table', new TableRenderer(this.app, this.cellParser))
|
||||
this.rendererRegistry.register('sql-seal-internal-grid', new GridRenderer(this.app, this, this.cellParser))
|
||||
this.rendererRegistry.register('sql-seal-internal-markdown', new MarkdownRenderer(this.app))
|
||||
this.rendererRegistry.register('sql-seal-internal-list', new ListRenderer(this.app, this.cellParserRegistar))
|
||||
this.rendererRegistry.register('sql-seal-internal-template', new TemplateRenderer(this.app, this.cellParserRegistar))
|
||||
this.rendererRegistry.register('sql-seal-internal-list', new ListRenderer(this.app, this.cellParser))
|
||||
this.rendererRegistry.register('sql-seal-internal-template', new TemplateRenderer(this.app, this.cellParser))
|
||||
|
||||
// start syncing when files are loaded
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
|
|
@ -184,7 +190,7 @@ export default class SqlSealPlugin extends Plugin {
|
|||
async registerViews() {
|
||||
this.registerView(
|
||||
CSV_VIEW_TYPE,
|
||||
(leaf) => new CSVView(leaf, this.settings.enableEditing, this.cellParserRegistar)
|
||||
(leaf) => new CSVView(leaf, this.settings.enableEditing, this.cellParser)
|
||||
);
|
||||
this.registerView(
|
||||
JSON_VIEW_TYPE,
|
||||
|
|
@ -200,12 +206,12 @@ export default class SqlSealPlugin extends Plugin {
|
|||
this.rendererRegistry.unregister(name)
|
||||
}
|
||||
|
||||
registerSQLSealFunction(name: string, fn: any, argumentsCount: number = 1) {
|
||||
this.cellParserRegistar.register(name, fn, argumentsCount)
|
||||
registerSQLSealFunction(fn: CellFunction) {
|
||||
this.cellParser.register(fn)
|
||||
}
|
||||
|
||||
unregisterSQLSealFunction(name: string) {
|
||||
this.cellParserRegistar.unregister(name)
|
||||
this.cellParser.unregister(name)
|
||||
}
|
||||
|
||||
registerSQLSealFlag(flag: Flag) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import { Plugin } from "obsidian"
|
|||
import SqlSealPlugin from "../main"
|
||||
import { version } from '../../package.json'
|
||||
import { RendererConfig } from "../renderer/rendererRegistry";
|
||||
import { CellFunction } from "../cellParser/CellFunction";
|
||||
|
||||
const API_VERSION = 2;
|
||||
const API_VERSION = 3;
|
||||
|
||||
export class SQLSealRegisterApi {
|
||||
registeredApis: Array<SQLSealApi> = []
|
||||
|
|
@ -36,11 +37,6 @@ interface RegisteredView {
|
|||
viewClass: any;
|
||||
}
|
||||
|
||||
interface RegisteredFunction {
|
||||
name: string,
|
||||
fn: CallableFunction
|
||||
}
|
||||
|
||||
// TODO: use the type from registrator
|
||||
export interface RegisteredFlag {
|
||||
name: string,
|
||||
|
|
@ -49,7 +45,7 @@ export interface RegisteredFlag {
|
|||
|
||||
export class SQLSealApi {
|
||||
private views: Array<RegisteredView> = []
|
||||
private functions: Array<RegisteredFunction> = []
|
||||
private functions: Array<CellFunction> = []
|
||||
private flags: Array<RegisteredFlag> = []
|
||||
|
||||
constructor(private readonly plugin: Plugin, private sqlSealPlugin: SqlSealPlugin) {
|
||||
|
|
@ -66,12 +62,9 @@ export class SQLSealApi {
|
|||
this.sqlSealPlugin.registerSQLSealView(name, viewClass)
|
||||
}
|
||||
|
||||
registerCustomFunction(name: string, fn: CallableFunction, argumentsCount: number = 1) {
|
||||
this.functions.push({
|
||||
name,
|
||||
fn,
|
||||
})
|
||||
this.sqlSealPlugin.registerSQLSealFunction(name, fn, argumentsCount)
|
||||
registerCustomFunction(fn: CellFunction) {
|
||||
this.functions.push(fn)
|
||||
this.sqlSealPlugin.registerSQLSealFunction(fn)
|
||||
}
|
||||
|
||||
registerTable<const columns extends string[]>(tableName: string, columns: columns) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { merge } from "lodash";
|
|||
import { App } from "obsidian";
|
||||
import { RendererConfig } from "./rendererRegistry";
|
||||
import { parse } from 'json5'
|
||||
import { CellParser } from "../cellParser";
|
||||
import { ViewDefinition } from "../grammar/parser";
|
||||
import SqlSealPlugin from "../main";
|
||||
import { ModernCellParser } from "src/cellParser/ModernCellParser";
|
||||
|
||||
const getCurrentTheme = () => {
|
||||
return document.body.classList.contains('theme-dark') ? 'dark' : 'light';
|
||||
|
|
@ -31,7 +31,7 @@ class GridRendererCommunicator {
|
|||
private config: Partial<GridOptions>,
|
||||
private plugin: SqlSealPlugin | null,
|
||||
private app: App,
|
||||
private cellParser: CellParser
|
||||
private cellParser: ModernCellParser
|
||||
) {
|
||||
this.initialize()
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ class GridRendererCommunicator {
|
|||
}
|
||||
|
||||
export class GridRenderer implements RendererConfig {
|
||||
constructor(private app: App, private readonly plugin: SqlSealPlugin | null, private readonly cellParser: CellParser) { }
|
||||
constructor(private app: App, private readonly plugin: SqlSealPlugin | null, private readonly cellParser: ModernCellParser) { }
|
||||
get viewDefinition(): ViewDefinition {
|
||||
return {
|
||||
name: this.rendererKey,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// This is renderer for a very basic List view.
|
||||
import { App } from "obsidian";
|
||||
import { CellParser } from "../cellParser";
|
||||
import { RendererConfig } from "../renderer/rendererRegistry";
|
||||
import { displayError } from "../utils/ui";
|
||||
import { ViewDefinition } from "../grammar/parser";
|
||||
import { ModernCellParser } from "src/cellParser/ModernCellParser";
|
||||
|
||||
interface ListRendererConfig {
|
||||
classNames: string[]
|
||||
|
|
@ -11,7 +11,7 @@ interface ListRendererConfig {
|
|||
|
||||
export class ListRenderer implements RendererConfig {
|
||||
|
||||
constructor(private readonly app: App, private readonly cellParser: CellParser) { }
|
||||
constructor(private readonly app: App, private readonly cellParser: ModernCellParser) { }
|
||||
|
||||
get rendererKey() {
|
||||
return 'list'
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// This is renderer for a very basic Table view.
|
||||
import { App } from "obsidian";
|
||||
import { CellParser } from "../cellParser";
|
||||
import { RendererConfig } from "../renderer/rendererRegistry";
|
||||
import { displayError } from "../utils/ui";
|
||||
import { ViewDefinition } from "../grammar/parser";
|
||||
import { ModernCellParser } from "../cellParser/ModernCellParser";
|
||||
|
||||
interface HTMLRendererConfig {
|
||||
classNames: string[]
|
||||
|
|
@ -11,7 +11,7 @@ interface HTMLRendererConfig {
|
|||
|
||||
export class TableRenderer implements RendererConfig {
|
||||
|
||||
constructor(private readonly app: App, private cellParser: CellParser) { }
|
||||
constructor(private readonly app: App, private cellParser: ModernCellParser) { }
|
||||
|
||||
get rendererKey() {
|
||||
return 'html'
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// This is renderer for a very basic List view.
|
||||
import { App } from "obsidian";
|
||||
import { CellParser } from "../cellParser";
|
||||
import { RendererConfig } from "./rendererRegistry";
|
||||
import { displayError } from "../utils/ui";
|
||||
import { ViewDefinition } from "../grammar/parser";
|
||||
import Handlebars from "handlebars";
|
||||
import { ParseResults } from "src/cellParser/parseResults";
|
||||
import { ModernCellParser } from "src/cellParser/ModernCellParser";
|
||||
|
||||
interface TemplateRendererConfig {
|
||||
template: HandlebarsTemplateDelegate
|
||||
|
|
@ -12,7 +13,11 @@ interface TemplateRendererConfig {
|
|||
|
||||
export class TemplateRenderer implements RendererConfig {
|
||||
|
||||
constructor(private readonly app: App, private readonly cellParser: CellParser) { }
|
||||
parseResult: ParseResults;
|
||||
|
||||
constructor(private readonly app: App, private readonly cellParser: ModernCellParser) {
|
||||
this.parseResult = new ParseResults(cellParser, (el) => new Handlebars.SafeString(el.outerHTML))
|
||||
}
|
||||
|
||||
get rendererKey() {
|
||||
return 'template'
|
||||
|
|
@ -43,7 +48,12 @@ export class TemplateRenderer implements RendererConfig {
|
|||
el.empty()
|
||||
|
||||
// Seems to be the only way to render handlebars into DOM. Don't like it but what can we do.
|
||||
el.innerHTML = config.template({ data, columns, properties: frontmatter })
|
||||
el.innerHTML = config.template({
|
||||
data: this.parseResult.parse(data, columns),
|
||||
columns,
|
||||
properties: frontmatter
|
||||
})
|
||||
this.parseResult.initialise(el)
|
||||
},
|
||||
error: (error: string) => {
|
||||
displayError(el, error)
|
||||
|
|
|
|||
|
|
@ -122,7 +122,6 @@ export class SQLSealSettingsTab extends PluginSettingTab {
|
|||
.addOption('markdown', 'Markdown Table')
|
||||
.setValue(this.settings.defaultView)
|
||||
.onChange(async (value) => {
|
||||
console.log(value)
|
||||
this.settings.defaultView = value as 'grid' | 'html' | 'markdown';
|
||||
if (!value) {
|
||||
this.settings.defaultView = DEFAULT_SETTINGS.defaultView
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import { renderLink } from "./ui/link"
|
||||
|
||||
const makeApp = () => ({ metadataCache: { getFirstLinkpathDest: jest.fn(p => ({ path: p })) } })
|
||||
|
||||
describe('UI', () => {
|
||||
it('should properly render links', () => {
|
||||
const app = makeApp()
|
||||
const createEl = jest.fn()
|
||||
renderLink(app as any, createEl)(['a/b/c.md'])
|
||||
expect(createEl).toHaveBeenCalledWith('a', {
|
||||
cls: 'internal-link',
|
||||
href: 'a/b/c.md',
|
||||
text: 'c'
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly render link with name provided', () => {
|
||||
const app = makeApp()
|
||||
const createEl = jest.fn()
|
||||
renderLink(app as any, createEl)(['a/b/c.md', 'Name'])
|
||||
expect(createEl).toHaveBeenCalledWith('a', {
|
||||
cls: 'internal-link',
|
||||
href: 'a/b/c.md',
|
||||
text: 'Name'
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly render link with external url', () => {
|
||||
const app = makeApp()
|
||||
const createEl = jest.fn()
|
||||
renderLink(app as any, createEl)(['https://wikipedia.org', 'Wikipedia'])
|
||||
expect(createEl).toHaveBeenCalledWith('a', {
|
||||
cls: '',
|
||||
href: 'https://wikipedia.org',
|
||||
text: 'Wikipedia'
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly render link using wikilink tag', () => {
|
||||
const app = makeApp()
|
||||
const createEl = jest.fn()
|
||||
renderLink(app as any, createEl)(['[[a/b/c.md|Alias]]'])
|
||||
expect(createEl).toHaveBeenCalledWith('a', {
|
||||
cls: 'internal-link',
|
||||
href: 'a/b/c.md',
|
||||
text: 'Alias'
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly return empty string when link is empty', () => {
|
||||
const app = makeApp()
|
||||
const createEl = jest.fn()
|
||||
const res = renderLink(app as any, createEl)([undefined as any])
|
||||
expect(createEl).not.toHaveBeenCalled()
|
||||
expect(res).toEqual('')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,9 +1,4 @@
|
|||
import { identity } from "lodash"
|
||||
import { App } from "obsidian"
|
||||
import { CellParserRegistar } from "../cellParser"
|
||||
import { renderCheckbox } from "./ui/checkbox"
|
||||
import { renderLink } from "./ui/link"
|
||||
import { renderImage } from "./ui/image"
|
||||
|
||||
export const displayNotice = (el: HTMLElement, text: string) => {
|
||||
el.empty()
|
||||
|
|
@ -31,9 +26,3 @@ export const renderStringifiedArray = (input: string, transformer: (f: string) =
|
|||
el.append(...links)
|
||||
return el
|
||||
}
|
||||
|
||||
export const registerDefaultFunctions = (registar: CellParserRegistar, app: App) => {
|
||||
registar.register('checkbox', renderCheckbox(app), 1)
|
||||
registar.register('a', renderLink(app), 2)
|
||||
registar.register('img', renderImage(app), 2)
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import { App } from "obsidian";
|
||||
|
||||
interface CheckboxProps {
|
||||
checked: boolean,
|
||||
path: string,
|
||||
position: {
|
||||
line: number,
|
||||
lineContent: string
|
||||
},
|
||||
task: string
|
||||
}
|
||||
|
||||
const isCheckboxProp = (arg: any): arg is CheckboxProps => {
|
||||
return arg && arg['position'] && arg['path']
|
||||
}
|
||||
|
||||
export const renderCheckbox = (app: App) => (values: number[] | CheckboxProps) => {
|
||||
|
||||
if (!isCheckboxProp(values)) {
|
||||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
attr: {
|
||||
disabled: true
|
||||
}
|
||||
});
|
||||
el.checked = !!values[0]
|
||||
return el
|
||||
}
|
||||
|
||||
const el = createEl('input', {
|
||||
type: 'checkbox',
|
||||
})
|
||||
|
||||
if (values && values.checked) {
|
||||
el.checked = true
|
||||
}
|
||||
|
||||
el.addEventListener('change', async () => {
|
||||
try {
|
||||
// Values should contain the data we need
|
||||
if (!values || !values.path) {
|
||||
console.error('Missing required checkbox data')
|
||||
return
|
||||
}
|
||||
|
||||
const file = app.vault.getFileByPath(values.path)
|
||||
if (!file) {
|
||||
console.error('File not found:', values.path)
|
||||
return
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = await app.vault.read(file)
|
||||
const lines = content.split('\n')
|
||||
|
||||
// Get the line containing the task
|
||||
const lineIndex = values.position?.line
|
||||
if (lineIndex >= 0 && lineIndex < lines.length) {
|
||||
// Update the task status
|
||||
if (el.checked) {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [ ]', '- [x]')
|
||||
} else {
|
||||
lines[lineIndex] = lines[lineIndex].replace('- [x]', '- [ ]')
|
||||
}
|
||||
|
||||
// Write back to the file
|
||||
await app.vault.modify(file, lines.join('\n'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error)
|
||||
}
|
||||
})
|
||||
|
||||
return el
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { App } from "obsidian";
|
||||
import { isLinkLocal } from "./helperFunctions";
|
||||
|
||||
export const renderImage = (app: App) => ([href, path]: [string, string]) => {
|
||||
if (!isLinkLocal(href)) {
|
||||
return createEl('img', { attr: { src: href } });
|
||||
}
|
||||
href = (href ?? '').trim()
|
||||
if (href.startsWith('![[')) {
|
||||
href = href.slice(3, -2)
|
||||
}
|
||||
let parentPath = ''
|
||||
if (path) {
|
||||
const parent = app.vault.getFileByPath(path)
|
||||
if (parent) {
|
||||
parentPath = parent.parent?.path ?? ''
|
||||
}
|
||||
}
|
||||
path = (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) } });
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import { App } from "obsidian"
|
||||
import { isLinkLocal, isLinktext, removeExtension } from "./helperFunctions"
|
||||
import { isStringifiedArray, renderStringifiedArray } from "../ui"
|
||||
|
||||
type LinkType = [string] | [string, string]
|
||||
|
||||
export const renderLink = (app: App, create = createEl) => ([href, name]: LinkType): string | Node => {
|
||||
if (!href) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isStringifiedArray(href)) {
|
||||
try {
|
||||
return renderStringifiedArray(href, href => renderLink(app, create)([href]))
|
||||
} catch (e) {
|
||||
return href
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = href
|
||||
}
|
||||
href = href.trim()
|
||||
let cls = ''
|
||||
if (isLinktext(href)) {
|
||||
const [path, linkName] = href.slice(2, -2).split('|')
|
||||
const link = app.metadataCache.getFirstLinkpathDest(path, '')
|
||||
|
||||
if (name.trim() === href) {
|
||||
name = linkName ?? path
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
href = path
|
||||
} else {
|
||||
href = link.path
|
||||
}
|
||||
cls = 'internal-link'
|
||||
|
||||
} else if (isLinkLocal(href)) {
|
||||
let fileHref = href
|
||||
if (fileHref.endsWith('.md')) {
|
||||
fileHref = fileHref.slice(0, -3)
|
||||
}
|
||||
if (name === href) {
|
||||
const components = href.split('/')
|
||||
name = removeExtension(components[components.length - 1])
|
||||
}
|
||||
cls = 'internal-link'
|
||||
}
|
||||
const el = create('a', { text: name, href, cls })
|
||||
return el
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ import { DeleteConfirmationModal } from '../modal/deleteConfirmationModal';
|
|||
import { RenameColumnModal } from '../modal/renameColumnModal';
|
||||
import { CodeSampleModal } from '../modal/showCodeSample';
|
||||
import { GridRenderer } from '../renderer/GridRenderer';
|
||||
import { CellParser } from '../cellParser';
|
||||
import { errorNotice } from '../utils/notice';
|
||||
import { ModernCellParser } from 'src/cellParser/ModernCellParser';
|
||||
|
||||
export const CSV_VIEW_TYPE = "csv-viewer" as const;
|
||||
export const CSV_VIEW_EXTENSIONS = ['csv'];
|
||||
|
|
@ -17,7 +17,7 @@ export class CSVView extends TextFileView {
|
|||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
private enableEditing: boolean,
|
||||
private cellParser: CellParser
|
||||
private cellParser: ModernCellParser
|
||||
) {
|
||||
super(leaf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,5 +56,6 @@
|
|||
"0.28.4": "0.15.0",
|
||||
"0.29.0": "0.15.0",
|
||||
"0.30.0": "0.15.0",
|
||||
"0.30.1": "0.15.0"
|
||||
"0.30.1": "0.15.0",
|
||||
"0.31.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue