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" }];
- },
- };
-}