2025-08-10 08:34:54 +00:00
|
|
|
import { OmnibusRegistrator } from "@hypersphere/omnibus";
|
|
|
|
|
import {
|
|
|
|
|
App,
|
|
|
|
|
MarkdownPostProcessorContext,
|
|
|
|
|
MarkdownRenderChild,
|
|
|
|
|
} from "obsidian";
|
|
|
|
|
import { Sync } from "../../sync/sync/sync";
|
|
|
|
|
import { RendererRegistry, RenderReturn } from "../renderer/rendererRegistry";
|
|
|
|
|
import { ParserResult, parseWithDefaults, TableDefinition } from "../parser";
|
2026-03-22 18:01:03 +00:00
|
|
|
import { SqlocalDatabaseProxy } from "../../database/sqlocal/sqlocalDatabaseProxy";
|
2025-08-10 08:34:54 +00:00
|
|
|
import { displayError, displayNotice } from "../../../utils/ui";
|
|
|
|
|
import { transformQuery } from "../sql/sqlTransformer";
|
|
|
|
|
import { registerObservers } from "../../../utils/registerObservers";
|
|
|
|
|
import { Settings } from "../../settings/Settings";
|
|
|
|
|
import { ModernCellParser } from "../../syntaxHighlight/cellParser/ModernCellParser";
|
|
|
|
|
|
|
|
|
|
export class CodeblockProcessor extends MarkdownRenderChild {
|
|
|
|
|
registrator: OmnibusRegistrator;
|
|
|
|
|
renderer: RenderReturn;
|
|
|
|
|
private flags: ParserResult["flags"];
|
|
|
|
|
private extrasEl: HTMLElement;
|
|
|
|
|
private explainEl: HTMLElement;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private el: HTMLElement,
|
|
|
|
|
private source: string,
|
|
|
|
|
private ctx: MarkdownPostProcessorContext,
|
|
|
|
|
private rendererRegistry: RendererRegistry,
|
2026-03-22 18:01:03 +00:00
|
|
|
private db: Pick<SqlocalDatabaseProxy, 'select' | 'explain'>,
|
2025-08-10 08:34:54 +00:00
|
|
|
private cellParser: ModernCellParser,
|
|
|
|
|
private settings: Settings,
|
|
|
|
|
private app: App,
|
|
|
|
|
private sync: Sync,
|
2025-08-10 09:08:13 +00:00
|
|
|
private tq: typeof transformQuery = transformQuery
|
2025-08-10 08:34:54 +00:00
|
|
|
) {
|
|
|
|
|
super(el);
|
|
|
|
|
|
|
|
|
|
this.registrator = this.sync.getRegistrator();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query: string;
|
|
|
|
|
|
|
|
|
|
async onload() {
|
|
|
|
|
try {
|
|
|
|
|
const defaults: ParserResult = {
|
|
|
|
|
flags: {
|
|
|
|
|
refresh: this.settings.get("enableDynamicUpdates"),
|
|
|
|
|
explain: false,
|
|
|
|
|
},
|
|
|
|
|
query: "",
|
|
|
|
|
renderer: {
|
|
|
|
|
options: "",
|
|
|
|
|
type: this.settings.get("defaultView").toUpperCase(),
|
|
|
|
|
},
|
|
|
|
|
tables: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const results = parseWithDefaults(
|
|
|
|
|
this.source,
|
|
|
|
|
this.rendererRegistry.getViewDefinitions(),
|
|
|
|
|
defaults,
|
|
|
|
|
this.rendererRegistry.flags,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (results.tables) {
|
|
|
|
|
await this.registerTables(results.tables);
|
|
|
|
|
if (!results.query) {
|
|
|
|
|
displayNotice(
|
|
|
|
|
this.el,
|
|
|
|
|
`Creating tables: ${results.tables.map((t) => t.tableAlias).join(", ")}`,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.flags = results.flags;
|
|
|
|
|
let rendererEl = this.el;
|
|
|
|
|
|
|
|
|
|
if (this.flags.explain) {
|
|
|
|
|
this.extrasEl = this.el.createDiv({ cls: "sqlseal-extras-container" });
|
|
|
|
|
if (this.flags.explain) {
|
|
|
|
|
this.explainEl = this.extrasEl.createEl("pre", {
|
|
|
|
|
cls: "sqlseal-extras-explain-container",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
rendererEl = this.el.createDiv({ cls: "sqlseal-renderer-container" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IF WE'RE ON CANVAS, LETS ADD BACKGRUND
|
|
|
|
|
if (this.isOnCanvas) {
|
|
|
|
|
rendererEl.classList.add('sqlseal-renderer-on-canvas')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.renderer = this.rendererRegistry.prepareRender(
|
|
|
|
|
results.renderer.type.toLowerCase(),
|
|
|
|
|
results.renderer.options,
|
|
|
|
|
)(rendererEl, {
|
|
|
|
|
cellParser: this.cellParser,
|
|
|
|
|
sourcePath: this.sourceKey,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// FIXME: probably should save the one before transform and perform transform every time we execute it.
|
|
|
|
|
this.query = results.query;
|
|
|
|
|
await this.render();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
displayError(this.el, e.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onunload() {
|
|
|
|
|
this.registrator.offAll();
|
|
|
|
|
if (this.renderer?.cleanup) {
|
|
|
|
|
this.renderer.cleanup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async render() {
|
|
|
|
|
try {
|
|
|
|
|
const registeredTablesForContext =
|
|
|
|
|
await this.sync.getTablesMappingForContext(this.sourceKey);
|
|
|
|
|
|
2025-08-10 09:08:13 +00:00
|
|
|
// Transforming Query
|
2026-04-06 11:50:00 +00:00
|
|
|
const res = this.tq(this.query, registeredTablesForContext, {
|
|
|
|
|
disableTagAutoDetection: this.settings.get('disableTagAutoDetection')
|
|
|
|
|
});
|
2025-08-10 08:34:54 +00:00
|
|
|
const transformedQuery = res.sql;
|
|
|
|
|
|
|
|
|
|
if (this.flags.refresh) {
|
|
|
|
|
registerObservers({
|
|
|
|
|
bus: this.registrator,
|
|
|
|
|
callback: () => this.render(),
|
|
|
|
|
fileName: this.sourceKey,
|
|
|
|
|
tables: res.mappedTables,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let variables = {};
|
|
|
|
|
const file = this.app.vault.getFileByPath(this.sourceKey);
|
|
|
|
|
if (file) {
|
|
|
|
|
const fileCache = this.app.metadataCache.getFileCache(file);
|
|
|
|
|
variables = {
|
|
|
|
|
...(fileCache?.frontmatter ?? {}),
|
|
|
|
|
path: file.path,
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
basename: file.basename,
|
|
|
|
|
parent: file.parent?.path,
|
|
|
|
|
extension: file.extension,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-15 13:57:27 +00:00
|
|
|
// Merge with context frontmatter (used for Explorer variables)
|
|
|
|
|
variables = {
|
|
|
|
|
...variables,
|
|
|
|
|
...this.ctx.frontmatter,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2025-08-10 08:34:54 +00:00
|
|
|
if (this.flags.explain) {
|
|
|
|
|
// Rendering explain
|
|
|
|
|
const result = await this.db.explain(transformedQuery, variables);
|
|
|
|
|
this.explainEl.textContent = result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-15 13:57:27 +00:00
|
|
|
|
2025-08-10 08:34:54 +00:00
|
|
|
const { data, columns } = (await this.db.select(
|
|
|
|
|
transformedQuery,
|
|
|
|
|
variables,
|
|
|
|
|
))!; // FIXME: check this
|
|
|
|
|
this.renderer.render({
|
|
|
|
|
data,
|
|
|
|
|
columns,
|
|
|
|
|
flags: this.flags,
|
|
|
|
|
frontmatter: variables,
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.renderer.error(e.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cachedName: string | null = null
|
|
|
|
|
|
|
|
|
|
get isOnCanvas() {
|
|
|
|
|
return !this.ctx.sourcePath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get canvasName() {
|
|
|
|
|
// This is hack to detect what name has current canvas.
|
|
|
|
|
// It's not fool proof but should work for majority of use-cases for now.
|
|
|
|
|
// We need to find proper way of getting it or ask Obsidian devs to expose some info.
|
|
|
|
|
|
|
|
|
|
if (this.cachedName !== null) {
|
|
|
|
|
return this.cachedName
|
|
|
|
|
}
|
|
|
|
|
const canvasViews = this.app.workspace.getLeavesOfType("canvas");
|
|
|
|
|
|
|
|
|
|
for (const leaf of canvasViews) {
|
2025-08-10 09:01:08 +00:00
|
|
|
const canvasView = leaf.view as any; // Canvas view has data and file properties not exposed in base View type
|
|
|
|
|
const nodes = JSON.parse(canvasView.data).nodes
|
|
|
|
|
const node = nodes.filter((n: any) => n.text).find((n: any) => n.text.contains(this.source))
|
2025-08-10 08:34:54 +00:00
|
|
|
if (node) {
|
2025-08-10 09:01:08 +00:00
|
|
|
this.cachedName = canvasView.file.path
|
2025-08-10 08:34:54 +00:00
|
|
|
return this.cachedName as string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.cachedName = ''
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get sourceKey() {
|
|
|
|
|
return this.ctx.sourcePath.trim() ? this.ctx.sourcePath.trim() : this.canvasName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async registerTables(tables: TableDefinition[]) {
|
|
|
|
|
await Promise.all(
|
|
|
|
|
tables.map((table) =>
|
|
|
|
|
this.sync.registerTable({
|
|
|
|
|
...table,
|
|
|
|
|
sourceFile: this.sourceKey,
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|