mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
d2d2dba7fa
* feat(annotate): configurable extra markdown extensions (#1307) Adds a config-only `markdownExtensions` key to ~/.plannotator/config.json, e.g. { "markdownExtensions": [".livemd"] } for Livebook notebooks. A listed extension is accepted everywhere .md is on the annotate path: CLI target resolution, folder discovery and the file browser, /api/doc plus relative and wiki-link navigation between sibling docs, the 2MB size cap, and per-file version history. Listed extensions render as markdown with frontmatter stripped, never as raw HTML, and they only widen the accepted set. Design: - packages/core/annotatable.ts stays browser-safe and zero-dep. Its regexes and predicates now take an optional, defaulted-empty list of extra extensions, plus a normalizer and regex builders. - packages/shared/markdown-extensions.ts is the node-side seam: it reads config.json once per process through the existing loadConfig() and threads the normalized list into those pure functions. resolve-file re-exports the config-aware predicates so both runtimes pick them up; the Bun server, the Pi mirror, the OpenCode plugin and the CLI all go through them. - The annotate /api/plan payload ships the resolved list so the renderer can linkify links to sibling documents (module-level UI registry, empty by default, so nothing changes without config). Validation: entries must be dot-led, lowercase-normalized, and free of path separators, globs and whitespace. Invalid entries are dropped silently, built-ins are deduplicated, and `.env` is denylisted so config can never register it (annotate copies file contents into the data dir). Deliberately unchanged: the Pi plan-write allowlist (ALLOWED_PLAN_EXTENSIONS in tool-scope.ts) and Edit Mode source save (SOURCE_SAVE_FILE_REGEX), which keep their own narrower allowlists. * fix(annotate): deny the dotenv family and sandbox config-aware tests Review follow-ups on #1309: - deny the whole dotenv family (.prod.env, .env.local, ...) in normalizeMarkdownExtensions, not just the exact .env name - resolve config.json path per call instead of at module scope so PLANNOTATOR_DATA_DIR sandboxing works in single-process test runs - stop resolve-file.test.ts reading the real user config: pure predicate imports plus pinned empty extras on every resolve call - add the config.json -> memo -> predicate integration test using resetMarkdownExtensionsCache under a temp data dir * test(call-flow): make the stale-read advert test self-sufficient The read-only GET only probes the node runtime while Call flow is enabled. The stale-read test relied on earlier tests' settings POSTs leaking callFlow=true through the process-frozen config path; with lazy config resolution each sandbox is genuinely isolated, so the test now enables Call flow in its own data dir. Locally the dependency was masked by an fnm-shimmed sem sidecar spawning node coincidentally.
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test";
|
|
import {
|
|
getExtraMarkdownExtensions,
|
|
hasLinkedDocExtension,
|
|
setExtraMarkdownExtensions,
|
|
} from "./markdownExtensions";
|
|
|
|
// Module-level registry: every test must leave it empty for the next one.
|
|
afterEach(() => {
|
|
setExtraMarkdownExtensions([]);
|
|
});
|
|
|
|
describe("hasLinkedDocExtension", () => {
|
|
test("built-in document extensions are recognized without registration", () => {
|
|
expect(hasLinkedDocExtension("notes.md")).toBe(true);
|
|
expect(hasLinkedDocExtension("guide.mdx")).toBe(true);
|
|
expect(hasLinkedDocExtension("page.html")).toBe(true);
|
|
// Markdown links may carry a fragment; wiki-link targets may not (a
|
|
// fragment-less `[[notes]]` is what gets `.md` appended).
|
|
expect(hasLinkedDocExtension("notes.md#section", { allowFragment: true })).toBe(true);
|
|
expect(hasLinkedDocExtension("notes.md#section")).toBe(false);
|
|
expect(hasLinkedDocExtension("app.ts")).toBe(false);
|
|
});
|
|
|
|
// #1307: without this the renderer turns `[tour](tour.livemd)` into a dead
|
|
// external link, and `[[tour.livemd]]` into a request for `tour.livemd.md`.
|
|
test("a registered extension makes sibling docs openable", () => {
|
|
expect(hasLinkedDocExtension("tour.livemd")).toBe(false);
|
|
setExtraMarkdownExtensions([".livemd"]);
|
|
expect(hasLinkedDocExtension("tour.livemd")).toBe(true);
|
|
expect(hasLinkedDocExtension("../notebooks/TOUR.LiveMD")).toBe(true);
|
|
expect(hasLinkedDocExtension("tour.livemd#setup", { allowFragment: true })).toBe(true);
|
|
expect(hasLinkedDocExtension("tour.livemd.bin")).toBe(false);
|
|
});
|
|
|
|
test("the payload is normalized, so a malformed or denied entry registers nothing", () => {
|
|
setExtraMarkdownExtensions([".env", "livemd", "*.livemd", ".livemd"]);
|
|
expect(getExtraMarkdownExtensions()).toEqual([".livemd"]);
|
|
expect(hasLinkedDocExtension(".env")).toBe(false);
|
|
});
|
|
|
|
test("a missing payload clears the registry rather than throwing", () => {
|
|
setExtraMarkdownExtensions([".livemd"]);
|
|
setExtraMarkdownExtensions(undefined);
|
|
expect(getExtraMarkdownExtensions()).toEqual([]);
|
|
expect(hasLinkedDocExtension("tour.livemd")).toBe(false);
|
|
});
|
|
});
|