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)
This commit is contained in:
parent
5791ee5ec2
commit
16242a2f44
8
package-lock.json
generated
8
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "joplin-plugin-event-calendar",
|
"name": "joplin-plugin-gtd-calendar",
|
||||||
"version": "0.3.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "joplin-plugin-event-calendar",
|
"name": "joplin-plugin-gtd-calendar",
|
||||||
"version": "0.3.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extraScripts": ["joplin-plugin-event-calendar.ts"]
|
"extraScripts": ["gtd-calendar-renderer.ts"]
|
||||||
}
|
}
|
||||||
@ -127,3 +127,37 @@
|
|||||||
top: 5vh;
|
top: 5vh;
|
||||||
right: 5vw;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
59
src/gtd-calendar-renderer.ts
Normal file
59
src/gtd-calendar-renderer.ts
Normal file
@ -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 <div> 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 (
|
||||||
|
`<div class="gtd-calendar-placeholder"` +
|
||||||
|
` data-content-script-id="${context.contentScriptId}"` +
|
||||||
|
` data-config="${encodedConfig}">` +
|
||||||
|
`<p class="gtd-calendar-loading">Loading GTD Calendar…</p>` +
|
||||||
|
`</div>`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
assets: function () {
|
||||||
|
return [
|
||||||
|
{ name: "event-calendar.css" },
|
||||||
|
{ name: "gtd-calendar-webview.js" },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
130
src/gtd-calendar-webview.js
Normal file
130
src/gtd-calendar-webview.js
Normal file
@ -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 });
|
||||||
|
})();
|
||||||
112
src/index.ts
112
src/index.ts
@ -1,12 +1,120 @@
|
|||||||
import joplin from "api";
|
import joplin from "api";
|
||||||
import { ContentScriptType } from "api/types";
|
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({
|
joplin.plugins.register({
|
||||||
onStart: async function () {
|
onStart: async function () {
|
||||||
await joplin.contentScripts.register(
|
await joplin.contentScripts.register(
|
||||||
ContentScriptType.MarkdownItPlugin,
|
ContentScriptType.MarkdownItPlugin,
|
||||||
"joplin-plugin-event-calendar",
|
CONTENT_SCRIPT_ID,
|
||||||
"./joplin-plugin-event-calendar.js"
|
"./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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 `<div class="joplin-editable">${contentHtml.outerHTML}</div>`;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
assets: function () {
|
|
||||||
return [{ name: "event-calendar.css" }];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user