From 16242a2f440fc6c779e821b8191534235022dc47 Mon Sep 17 00:00:00 2001 From: Victor Wiebe Date: Fri, 12 Jun 2026 21:52:41 +0000 Subject: [PATCH] Phase 1: gtd-calendar fence, webview round trip, drilldown proof - New markdown-it content script intercepts ```gtd-calendar fences and emits a placeholder div (config URI-encoded in a data attribute) - New webview asset (gtd-calendar-webview.js) finds placeholders, requests data via webviewApi.postMessage, renders a debug view, and wires click-to-open drilldown (openNote command) - index.ts registers the content script and handles getEvents/openNote; getEvents parses the YAML config for real but returns hardcoded events, including one pointing at the current note to prove drilldown - Old event-calendar content script removed; Calendar/ classes retained for reuse in Phase 3 - Fork CSS section appended (placeholder, error, debug styles) --- package-lock.json | 8 +- plugin.config.json | 2 +- src/event-calendar.css | 34 ++++++++ src/gtd-calendar-renderer.ts | 59 +++++++++++++ src/gtd-calendar-webview.js | 130 ++++++++++++++++++++++++++++ src/index.ts | 112 +++++++++++++++++++++++- src/joplin-plugin-event-calendar.ts | 46 ---------- 7 files changed, 338 insertions(+), 53 deletions(-) create mode 100644 src/gtd-calendar-renderer.ts create mode 100644 src/gtd-calendar-webview.js delete mode 100644 src/joplin-plugin-event-calendar.ts diff --git a/package-lock.json b/package-lock.json index b6cec58..d9522c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "joplin-plugin-event-calendar", - "version": "0.3.0", + "name": "joplin-plugin-gtd-calendar", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "joplin-plugin-event-calendar", - "version": "0.3.0", + "name": "joplin-plugin-gtd-calendar", + "version": "0.1.0", "license": "MIT", "dependencies": { "date-fns": "^2.29.3", diff --git a/plugin.config.json b/plugin.config.json index bce1cf1..ce57b90 100644 --- a/plugin.config.json +++ b/plugin.config.json @@ -1,3 +1,3 @@ { - "extraScripts": ["joplin-plugin-event-calendar.ts"] + "extraScripts": ["gtd-calendar-renderer.ts"] } \ No newline at end of file diff --git a/src/event-calendar.css b/src/event-calendar.css index c8aedb0..67603cb 100644 --- a/src/event-calendar.css +++ b/src/event-calendar.css @@ -127,3 +127,37 @@ top: 5vh; right: 5vw; } + +/* --- GTD Calendar (fork) --- */ + +.gtd-calendar-placeholder .gtd-calendar-loading { + font-style: italic; + opacity: 0.7; +} + +.gtd-calendar-error { + border: 1px solid #c0392b; + border-radius: 4px; + padding: 0.5em 0.75em; + color: #c0392b; +} + +.gtd-calendar-debug { + border: 1px dashed currentColor; + border-radius: 6px; + padding: 0.75em 1em; +} + +.gtd-calendar-debug-meta { + font-size: 0.85em; + opacity: 0.75; +} + +.gtd-calendar-debug-events li { + margin: 0.25em 0; +} + +.gtd-calendar-clickable { + cursor: pointer; + text-decoration: underline; +} diff --git a/src/gtd-calendar-renderer.ts b/src/gtd-calendar-renderer.ts new file mode 100644 index 0000000..c8043e2 --- /dev/null +++ b/src/gtd-calendar-renderer.ts @@ -0,0 +1,59 @@ +const FENCE_INFO = "gtd-calendar"; + +/** + * Markdown-it content script. + * + * Markdown-it renders synchronously and has no access to the Joplin data + * API, so this script does the minimum possible: it intercepts + * ```gtd-calendar fences and emits a placeholder
carrying the raw + * (URI-encoded) fence content and our content script id. + * + * The companion webview asset (gtd-calendar-webview.js) finds these + * placeholders in the rendered note, requests event data from the main + * plugin process via webviewApi.postMessage, and builds the calendar DOM. + */ +export default function (context: { contentScriptId: string }) { + return { + plugin: function (markdownIt: any) { + const defaultRender: Function = + markdownIt.renderer.rules.fence || + function ( + tokens: any[], + idx: number, + options: any, + env: any, + self: any + ) { + return self.renderToken(tokens, idx, options, env, self); + }; + + markdownIt.renderer.rules.fence = function ( + tokens: any[], + idx: number, + options: {}, + env: any, + self: any + ) { + const token = tokens[idx]; + if (token.info !== FENCE_INFO) + return defaultRender(tokens, idx, options, env, self); + + const encodedConfig = encodeURIComponent(token.content); + + return ( + `
` + + `

Loading GTD Calendar…

` + + `
` + ); + }; + }, + assets: function () { + return [ + { name: "event-calendar.css" }, + { name: "gtd-calendar-webview.js" }, + ]; + }, + }; +} diff --git a/src/gtd-calendar-webview.js b/src/gtd-calendar-webview.js new file mode 100644 index 0000000..0cb90ac --- /dev/null +++ b/src/gtd-calendar-webview.js @@ -0,0 +1,130 @@ +/* + * GTD Calendar — webview script. + * + * Runs inside Joplin's rendered-note webview. Finds gtd-calendar + * placeholders emitted by the markdown-it content script, asks the main + * plugin process for event data, and renders the result. + * + * Phase 1: the plugin returns a hardcoded payload; this script renders a + * simple debug view proving the full round trip, including click-to-open + * drilldown. + */ +(function () { + "use strict"; + + function processPlaceholders() { + const placeholders = document.querySelectorAll( + ".gtd-calendar-placeholder:not([data-gtd-processed])" + ); + + placeholders.forEach(function (el) { + el.setAttribute("data-gtd-processed", "true"); + + const contentScriptId = el.getAttribute("data-content-script-id"); + const rawConfig = decodeURIComponent( + el.getAttribute("data-config") || "" + ); + + if ( + typeof webviewApi === "undefined" || + typeof webviewApi.postMessage !== "function" + ) { + renderError( + el, + "GTD Calendar: webview messaging is unavailable. " + + "This plugin requires Joplin 2.7 or newer (desktop)." + ); + return; + } + + webviewApi + .postMessage(contentScriptId, { + type: "getEvents", + rawConfig: rawConfig, + }) + .then(function (payload) { + if (!payload) { + renderError(el, "GTD Calendar: empty response from plugin."); + return; + } + renderPayload(el, payload, contentScriptId); + }) + .catch(function (error) { + renderError(el, "GTD Calendar: " + String(error)); + }); + }); + } + + function renderError(el, message) { + el.innerHTML = ""; + const box = document.createElement("div"); + box.className = "gtd-calendar-error"; + box.textContent = message; + el.appendChild(box); + } + + function renderPayload(el, payload, contentScriptId) { + el.innerHTML = ""; + + const box = document.createElement("div"); + box.className = "gtd-calendar-debug"; + + const heading = document.createElement("h3"); + heading.textContent = + (payload.title || "GTD Calendar") + " — Phase 1 plumbing test"; + box.appendChild(heading); + + const meta = document.createElement("p"); + meta.className = "gtd-calendar-debug-meta"; + const noteInfo = payload.sourceNote + ? '"' + payload.sourceNote.title + '" (' + payload.sourceNote.id + ")" + : "unknown"; + meta.textContent = + "view: " + + (payload.view || "?") + + " · rendered in note: " + + noteInfo; + box.appendChild(meta); + + if (payload.configError) { + const warn = document.createElement("p"); + warn.className = "gtd-calendar-error"; + warn.textContent = "Config problem: " + payload.configError; + box.appendChild(warn); + } + + const list = document.createElement("ul"); + list.className = "gtd-calendar-debug-events"; + + (payload.events || []).forEach(function (event) { + const item = document.createElement("li"); + + const glyph = event.isTodo ? (event.completed ? "☑ " : "☐ ") : "📄 "; + const dateLabel = event.date ? event.date : "unscheduled"; + item.textContent = glyph + event.title + " — " + dateLabel; + + if (event.id) { + item.classList.add("gtd-calendar-clickable"); + item.title = "Click to open this note"; + item.addEventListener("click", function () { + webviewApi.postMessage(contentScriptId, { + type: "openNote", + noteId: event.id, + }); + }); + } + + list.appendChild(item); + }); + + box.appendChild(list); + el.appendChild(box); + } + + processPlaceholders(); + + // Joplin may replace rendered content without re-running asset scripts; + // watch for new placeholders arriving in the DOM. + const observer = new MutationObserver(processPlaceholders); + observer.observe(document.body, { childList: true, subtree: true }); +})(); diff --git a/src/index.ts b/src/index.ts index fc83f0d..8cb7b57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,120 @@ import joplin from "api"; import { ContentScriptType } from "api/types"; +const YAML = require("yaml"); + +const CONTENT_SCRIPT_ID = "gtd-calendar-renderer"; + +function todayISO(): string { + return new Date().toISOString().slice(0, 10); +} + joplin.plugins.register({ onStart: async function () { await joplin.contentScripts.register( ContentScriptType.MarkdownItPlugin, - "joplin-plugin-event-calendar", - "./joplin-plugin-event-calendar.js" + CONTENT_SCRIPT_ID, + "./gtd-calendar-renderer.js" + ); + + await joplin.contentScripts.onMessage( + CONTENT_SCRIPT_ID, + async (message: any) => { + if (!message || !message.type) return null; + + switch (message.type) { + case "getEvents": + return handleGetEvents(message); + case "openNote": + return handleOpenNote(message); + default: + return null; + } + } ); }, }); + +/** + * Phase 1: parse the gtd-calendar config for real, but return hardcoded + * events. Proves: config transit, YAML parsing in the main process, data + * API access (selectedNote), and the response path back to the webview. + * + * Phase 2 replaces the hardcoded events with folder-scoped note/todo + * collection per SPEC.md. + */ +async function handleGetEvents(message: { rawConfig?: string }) { + let config: any = {}; + let configError: string | null = null; + + try { + config = YAML.parse(message.rawConfig || "") || {}; + } catch (error) { + configError = String(error); + } + + let sourceNote: { id: string; title: string; parentId: string } | null = + null; + try { + const note = await joplin.workspace.selectedNote(); + if (note) { + sourceNote = { + id: note.id, + title: note.title, + parentId: note.parent_id, + }; + } + } catch (error) { + // Non-fatal in Phase 1; the debug view reports "unknown". + console.warn("gtd-calendar: could not resolve selected note", error); + } + + return { + phase: 1, + title: typeof config.title === "string" ? config.title : "GTD Calendar", + view: typeof config.view === "string" ? config.view : "day", + config, + configError, + sourceNote, + events: [ + { + id: sourceNote ? sourceNote.id : null, + title: "Drilldown test — opens this very note", + date: todayISO(), + isTodo: false, + completed: false, + }, + { + id: null, + title: "Hardcoded todo", + date: todayISO(), + isTodo: true, + completed: false, + }, + { + id: null, + title: "Hardcoded completed todo", + date: todayISO(), + isTodo: true, + completed: true, + }, + { + id: null, + title: "Hardcoded unscheduled note", + date: null, + isTodo: false, + completed: false, + }, + ], + }; +} + +async function handleOpenNote(message: { noteId?: string }) { + if (!message.noteId) return { ok: false, error: "No noteId provided" }; + try { + await joplin.commands.execute("openNote", message.noteId); + return { ok: true }; + } catch (error) { + return { ok: false, error: String(error) }; + } +} diff --git a/src/joplin-plugin-event-calendar.ts b/src/joplin-plugin-event-calendar.ts deleted file mode 100644 index 30fb5eb..0000000 --- a/src/joplin-plugin-event-calendar.ts +++ /dev/null @@ -1,46 +0,0 @@ -const YAML = require("yaml"); - -import Calendar from "./Calendar/"; - -export default function () { - return { - plugin: function (markdownIt: any) { - const defaultRender: Function = - markdownIt.renderer.rules.fence || - function ( - tokens: any[], - idx: number, - options: any, - env: any, - self: any - ) { - return self.renderToken(tokens, idx, options, env, self); - }; - - markdownIt.renderer.rules.fence = function ( - tokens: any[], - idx: number, - options: {}, - env: any, - self: any - ) { - const token = tokens[idx]; - if (token.info !== "joplin-plugin-event-calendar") - return defaultRender(tokens, idx, options, env, self); - try { - const jsonContent = YAML.parse( - markdownIt.utils.escapeHtml(token.content) - ); - const calendar = new Calendar(jsonContent); - const contentHtml = calendar.render(); - return `
${contentHtml.outerHTML}
`; - } catch (error) { - console.log(error); - } - }; - }, - assets: function () { - return [{ name: "event-calendar.css" }]; - }, - }; -}