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.
58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
/**
|
|
* Extra markdown extensions, renderer side (#1307).
|
|
*
|
|
* The server resolves `markdownExtensions` from `~/.plannotator/config.json`
|
|
* and ships the normalized list with the annotate payload. The renderer needs
|
|
* it for one job: deciding whether a relative link or a wiki-link target names
|
|
* a local document it should open in the linked-doc overlay (`/api/doc`) or a
|
|
* plain external link. Without it, `[notes](notes.livemd)` renders as a dead
|
|
* external link even though the server would happily serve it.
|
|
*
|
|
* Module-level registry seam, like `skillReferences.ts`: a host (or the app's
|
|
* own boot code) registers the list once, everything else reads it. Empty by
|
|
* default, so nothing changes for a user with no config.
|
|
*
|
|
* The built-in set here is deliberately NARROWER than the annotatable set on
|
|
* the server (`.md`/`.mdx`/`.txt`/`.html`/`.htm` only) — widening it is a
|
|
* separate decision. Extras are added on top of it.
|
|
*/
|
|
|
|
import { normalizeMarkdownExtensions } from "@plannotator/core/annotatable";
|
|
|
|
/** Built-in extensions the renderer treats as openable local documents. */
|
|
const BUILTIN_LINKED_DOC_REGEX = /\.(mdx?|txt|html?)$/i;
|
|
|
|
let extraExtensions: string[] = [];
|
|
|
|
/**
|
|
* Register the extra markdown extensions for this page. Values are normalized
|
|
* with the same rules the server applies (dot-led, lowercased, `.env` denied),
|
|
* so a hostile or malformed payload cannot inject regex or path fragments.
|
|
*/
|
|
export function setExtraMarkdownExtensions(value: unknown): void {
|
|
extraExtensions = normalizeMarkdownExtensions(value);
|
|
}
|
|
|
|
/** The registered extra extensions (normalized, possibly empty). */
|
|
export function getExtraMarkdownExtensions(): string[] {
|
|
return extraExtensions;
|
|
}
|
|
|
|
/**
|
|
* Does this link target name a local document the linked-doc overlay can open?
|
|
*
|
|
* `allowFragment` mirrors the two call sites this replaced: markdown links
|
|
* accept a trailing `#fragment` (stripped by the caller before navigating),
|
|
* wiki-link targets do not.
|
|
*/
|
|
export function hasLinkedDocExtension(
|
|
target: string,
|
|
options?: { allowFragment?: boolean },
|
|
): boolean {
|
|
const trimmed = target.trim();
|
|
const path = options?.allowFragment ? trimmed.replace(/#.*$/, "") : trimmed;
|
|
if (BUILTIN_LINKED_DOC_REGEX.test(path)) return true;
|
|
const lower = path.toLowerCase();
|
|
return extraExtensions.some((ext) => lower.endsWith(ext));
|
|
}
|