246 lines
9.8 KiB
JavaScript
246 lines
9.8 KiB
JavaScript
// ── hyprcrd Plugin API ──
|
|
//
|
|
// Loaded synchronously BEFORE any module script. Plugins and core both
|
|
// register against window.hyprcrd. Plugins MUST NOT import from core
|
|
// modules — use this global API instead so they remain decoupled.
|
|
//
|
|
// Available namespaces:
|
|
// hyprcrd.hooks — generic event bus (on/emit)
|
|
// hyprcrd.editor — toolbar command registry
|
|
// hyprcrd.markdown — fenced block + post-processing registry
|
|
// hyprcrd.util — DOM helpers exposed for plugin use
|
|
//
|
|
// Plugins are loaded as <script type="module"> tags after this script
|
|
// but before app.js. They self-register on module evaluation; the core
|
|
// then queries the registries when needed.
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// ── Generic event bus ───────────────────────────────────────────
|
|
const hookListeners = Object.create(null);
|
|
const hooks = {
|
|
on(name, fn) {
|
|
if (typeof fn !== 'function') return;
|
|
(hookListeners[name] ||= []).push(fn);
|
|
},
|
|
off(name, fn) {
|
|
const arr = hookListeners[name];
|
|
if (!arr) return;
|
|
const idx = arr.indexOf(fn);
|
|
if (idx >= 0) arr.splice(idx, 1);
|
|
},
|
|
emit(name, data) {
|
|
const arr = hookListeners[name];
|
|
if (!arr) return data;
|
|
for (const fn of arr.slice()) {
|
|
try {
|
|
const r = fn(data);
|
|
if (r !== undefined) data = r;
|
|
} catch (err) {
|
|
console.error(`[hyprcrd hook ${name}]`, err);
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// ── Editor command registry ─────────────────────────────────────
|
|
// Toolbar buttons with `data-md="<name>"` dispatch to handlers
|
|
// registered here. Plugins inject buttons + register handlers.
|
|
const editorCommands = Object.create(null);
|
|
const editor = {
|
|
registerCommand(name, fn) {
|
|
if (typeof fn !== 'function') return;
|
|
editorCommands[name] = fn;
|
|
},
|
|
unregisterCommand(name) {
|
|
delete editorCommands[name];
|
|
},
|
|
getCommand(name) {
|
|
return editorCommands[name];
|
|
},
|
|
// Set by editor.js after init — gives plugins access to
|
|
// insertMd(before, after, placeholder) and the textarea body
|
|
// without forcing an import dependency.
|
|
api: null,
|
|
};
|
|
|
|
// ── Markdown fenced block registry ──────────────────────────────
|
|
// Each entry: { lang: string, handler: (content) => string }
|
|
// The handler receives the raw text inside ```<lang> ... ``` and
|
|
// must return safe HTML (it will pass through DOMPurify after).
|
|
const fencedBlocks = [];
|
|
// Pre/post processors run on the full source text, before/after
|
|
// marked.parse(). Useful for non-fenced syntax extensions.
|
|
const preProcessors = [];
|
|
const postProcessors = [];
|
|
const markdown = {
|
|
registerFencedBlock(lang, handler) {
|
|
if (typeof handler !== 'function') return;
|
|
fencedBlocks.push({ lang, handler });
|
|
},
|
|
getFencedBlocks() {
|
|
return fencedBlocks.slice();
|
|
},
|
|
registerPreProcessor(fn) {
|
|
if (typeof fn === 'function') preProcessors.push(fn);
|
|
},
|
|
getPreProcessors() {
|
|
return preProcessors.slice();
|
|
},
|
|
registerPostProcessor(fn) {
|
|
if (typeof fn === 'function') postProcessors.push(fn);
|
|
},
|
|
getPostProcessors() {
|
|
return postProcessors.slice();
|
|
},
|
|
// Filled in by core markdown.js after load
|
|
render: null,
|
|
};
|
|
|
|
// ── Util helpers (mirror of internal util.js) ───────────────────
|
|
let csrfToken = '';
|
|
const util = {
|
|
$(idOrSel) {
|
|
if (typeof idOrSel !== 'string') return null;
|
|
if (idOrSel.startsWith('#') || idOrSel.includes(' ') || idOrSel.includes('.') || idOrSel.includes('[')) {
|
|
return document.querySelector(idOrSel);
|
|
}
|
|
return document.getElementById(idOrSel);
|
|
},
|
|
$$(sel) {
|
|
return Array.from(document.querySelectorAll(sel));
|
|
},
|
|
esc(s) {
|
|
return String(s ?? '').replace(/[&<>"']/g, c => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[c]));
|
|
},
|
|
// CSRF: core calls setCsrf() after auth so plugins can read it.
|
|
// Emits 'csrf.ready' so plugins that need to make state-changing
|
|
// requests on init can defer until auth has happened.
|
|
setCsrf(token) {
|
|
csrfToken = String(token || '');
|
|
if (csrfToken) hooks.emit('csrf.ready', csrfToken);
|
|
},
|
|
csrf() { return csrfToken; },
|
|
};
|
|
|
|
/** Generic JSON API call with automatic CSRF header. */
|
|
async function api(path, opts = {}) {
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
|
|
const res = await fetch(path, {
|
|
method: opts.method || 'GET',
|
|
headers,
|
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
});
|
|
return res.json();
|
|
}
|
|
|
|
// ── Settings registry ───────────────────────────────────────────
|
|
// Plugins can inject their own tabs into the main settings overlay.
|
|
// The plugin's onShow callback is called whenever the tab becomes visible.
|
|
const settings = {
|
|
registerTab({ id, label, icon, onShow }) {
|
|
if (!id) return;
|
|
const nav = document.getElementById('settings-nav');
|
|
const panelsContainer = document.querySelector('#settings-overlay .overflow-y-auto.p-5');
|
|
if (!nav || !panelsContainer) {
|
|
// DOM not ready — defer to next tick
|
|
requestAnimationFrame(() => settings.registerTab({ id, label, icon, onShow }));
|
|
return;
|
|
}
|
|
if (nav.querySelector(`[data-tab="${id}"]`)) return; // already registered
|
|
|
|
// Create tab button
|
|
const btn = document.createElement('button');
|
|
btn.className = 'settings-tab';
|
|
btn.dataset.tab = id;
|
|
const iconClass = icon || 'ph-puzzle-piece';
|
|
btn.innerHTML = `<i class="ph ${iconClass} text-sm"></i><span class="font-pixel text-[10px]">${label || id}</span>`;
|
|
nav.appendChild(btn);
|
|
|
|
// Create empty panel container
|
|
let panel = document.getElementById(`settings-panel-${id}`);
|
|
if (!panel) {
|
|
panel = document.createElement('div');
|
|
panel.id = `settings-panel-${id}`;
|
|
panel.className = 'hidden';
|
|
panelsContainer.appendChild(panel);
|
|
}
|
|
|
|
// Click handler — duplicates core tab logic (theme.js may also
|
|
// attach its own handler later, but identical work is harmless).
|
|
btn.addEventListener('click', () => {
|
|
nav.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.querySelectorAll('[id^="settings-panel-"]').forEach(p => p.classList.add('hidden'));
|
|
panel.classList.remove('hidden');
|
|
if (typeof onShow === 'function') {
|
|
try { onShow(panel); } catch (err) { console.error(`[settings tab ${id}]`, err); }
|
|
}
|
|
hooks.emit('settings.tab.shown', { tab: id, panel });
|
|
});
|
|
},
|
|
|
|
/** Open the main settings overlay. */
|
|
open() {
|
|
document.getElementById('settings-btn')?.click();
|
|
},
|
|
|
|
/** Open the settings overlay and switch to a specific tab. */
|
|
openTab(id) {
|
|
this.open();
|
|
// Settings opens via click; the tab might need a frame to render
|
|
requestAnimationFrame(() => {
|
|
document.querySelector(`#settings-nav [data-tab="${id}"]`)?.click();
|
|
});
|
|
},
|
|
};
|
|
|
|
// ── Dialog API (filled in by core dialog.js after load) ─────────
|
|
// Plugins call hyprcrd.dialog.confirm(...) / hyprcrd.dialog.show(...)
|
|
// instead of native browser confirm()/prompt() so the UI stays themed
|
|
// and consistent.
|
|
const dialog = {
|
|
show: null, // (opts) => Promise<value|null> — set by core dialog.js
|
|
/** Convenience confirm wrapper. Returns Promise<true|null>. */
|
|
async confirm(message, opts = {}) {
|
|
if (typeof dialog.show !== 'function') {
|
|
// Core dialog not loaded yet — fall back to native
|
|
return window.confirm(message) ? true : null;
|
|
}
|
|
return dialog.show({
|
|
title: opts.title || 'confirm',
|
|
message,
|
|
input: false,
|
|
danger: opts.danger ?? true,
|
|
okLabel: opts.okLabel || 'ok',
|
|
cancelLabel: opts.cancelLabel || 'cancel',
|
|
});
|
|
},
|
|
};
|
|
|
|
// ── Plugin metadata registry ────────────────────────────────────
|
|
// Plugins can register themselves here for introspection / debug.
|
|
const plugins = [];
|
|
const registerPlugin = (meta) => {
|
|
if (meta && meta.name) plugins.push(meta);
|
|
};
|
|
|
|
window.hyprcrd = {
|
|
hooks,
|
|
editor,
|
|
markdown,
|
|
util,
|
|
api,
|
|
settings,
|
|
dialog,
|
|
plugins,
|
|
registerPlugin,
|
|
version: '1.0',
|
|
};
|
|
})();
|