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.
97 lines
3.9 KiB
TypeScript
97 lines
3.9 KiB
TypeScript
/**
|
|
* Configured extra markdown extensions (#1307).
|
|
*
|
|
* A user can teach annotate about additional plain-text document extensions
|
|
* (for example `.livemd`, Livebook notebooks) with `markdownExtensions` in
|
|
* `~/.plannotator/config.json`:
|
|
*
|
|
* { "markdownExtensions": [".livemd"] }
|
|
*
|
|
* Listed extensions are accepted everywhere `.md` is accepted on the annotate
|
|
* path — CLI target resolution, the folder file browser, `/api/doc`
|
|
* (relative/wiki-link navigation between sibling docs), the 2MB size cap and
|
|
* the per-file version history — and are rendered as MARKDOWN (frontmatter
|
|
* stripped), never as HTML.
|
|
*
|
|
* The extension predicates themselves live in `@plannotator/core/annotatable`,
|
|
* which is browser-safe and zero-dep and therefore cannot read a config file.
|
|
* This module is the node-side seam: it reads `config.json` ONCE per process
|
|
* through the same `loadConfig()` every other setting uses, normalizes the
|
|
* value, and threads it into those pure functions. Everything here degrades to
|
|
* the built-in behavior when the key is absent or invalid.
|
|
*/
|
|
|
|
import {
|
|
buildAnnotatableDocRegex,
|
|
buildAnnotatableExtensionsHint,
|
|
buildAnnotatableTextRegex,
|
|
isAnnotatableDocPath as isAnnotatableDocPathWith,
|
|
isAnnotatableTextPath as isAnnotatableTextPathWith,
|
|
normalizeMarkdownExtensions,
|
|
shouldStripFrontmatter as shouldStripFrontmatterWith,
|
|
} from "./annotatable";
|
|
import { loadConfig, type PlannotatorConfig } from "./config";
|
|
|
|
export { normalizeMarkdownExtensions };
|
|
|
|
/**
|
|
* Resolve the configured extra markdown extensions from an explicit config
|
|
* object. Pure: invalid entries are dropped, `.env` is denylisted, and
|
|
* built-in extensions are deduplicated (see `normalizeMarkdownExtensions`).
|
|
*/
|
|
export function resolveMarkdownExtensions(config: PlannotatorConfig): string[] {
|
|
return normalizeMarkdownExtensions(config.markdownExtensions);
|
|
}
|
|
|
|
let cached: string[] | null = null;
|
|
|
|
/**
|
|
* The extra extensions for this process. Read from `config.json` on first use
|
|
* and memoized: a session's accepted set must not change halfway through a
|
|
* directory walk. Pass an explicit config to bypass the memo entirely.
|
|
*/
|
|
export function getExtraMarkdownExtensions(config?: PlannotatorConfig): string[] {
|
|
if (config) return resolveMarkdownExtensions(config);
|
|
if (cached === null) cached = resolveMarkdownExtensions(loadConfig());
|
|
return cached;
|
|
}
|
|
|
|
/** Drop the memo so the next read re-reads `config.json`. Tests only. */
|
|
export function resetMarkdownExtensionsCache(): void {
|
|
cached = null;
|
|
}
|
|
|
|
/** Plain-text (markdown-rendered) matcher including the configured extras. */
|
|
export function getAnnotatableTextRegex(): RegExp {
|
|
return buildAnnotatableTextRegex(getExtraMarkdownExtensions());
|
|
}
|
|
|
|
/** Plain-text + raw-HTML matcher including the configured extras. */
|
|
export function getAnnotatableDocRegex(): RegExp {
|
|
return buildAnnotatableDocRegex(getExtraMarkdownExtensions());
|
|
}
|
|
|
|
/** Accepted-set hint for error messages, including the configured extras. */
|
|
export function getAnnotatableExtensionsHint(): string {
|
|
return buildAnnotatableExtensionsHint(getExtraMarkdownExtensions());
|
|
}
|
|
|
|
/**
|
|
* True when annotate can open `input` as a plain-text (markdown-rendered)
|
|
* document, honoring the configured extras. Drop-in replacement for the pure
|
|
* core predicate of the same name — server code should import this one.
|
|
*/
|
|
export function isAnnotatableTextPath(input: string): boolean {
|
|
return isAnnotatableTextPathWith(input, getExtraMarkdownExtensions());
|
|
}
|
|
|
|
/** True when annotate can open `input` at all, honoring the configured extras. */
|
|
export function isAnnotatableDocPath(input: string): boolean {
|
|
return isAnnotatableDocPathWith(input, getExtraMarkdownExtensions());
|
|
}
|
|
|
|
/** Frontmatter stripping decision honoring the configured extras (extras are markdown). */
|
|
export function shouldStripFrontmatter(path: string | null | undefined): boolean {
|
|
return shouldStripFrontmatterWith(path, getExtraMarkdownExtensions());
|
|
}
|