syntaxai/tdd.md · main · src / b32_edit_resolve.ts

b32_edit_resolve.ts 83 lines · 2995 bytes raw
// c32 — pure logic: given a (section, slug) tuple from a /edit/<...>
// URL, resolve it to the editable resource (page URL, file path on
// disk, page title) — or return null if the combination doesn't map
// to an existing doc page. Pure: no I/O. The handler does the actual
// fs read for the current body.
//
// Editable sections are the ones that already have a registry: sama,
// guides, blog. Slug must match an entry in the corresponding registry
// — this prevents arbitrary file writes via /edit/../../etc/passwd
// style inputs.

import { ALL_SAMA } from "./a31_sama.ts";
import { ALL_GUIDES } from "./a31_guides.ts";
import { ALL_POSTS } from "./a31_blog.ts";
import { SITE_NAV } from "./a31_docs_nav.ts";

export type EditableSection = "sama" | "guides" | "blog";

export interface ResolvedEdit {
  section: EditableSection;
  slug: string;
  pageUrl: string;
  filePath: string;
  title: string;
}

const SECTIONS = new Set<EditableSection>(["sama", "guides", "blog"]);

const isValidSection = (s: string): s is EditableSection => SECTIONS.has(s as EditableSection);

const SAFE_SLUG = /^[a-z0-9][a-z0-9-]*$/;

const lookupTitle = (section: EditableSection, slug: string): string | null => {
  if (section === "sama") {
    const e = ALL_SAMA.find((d) => d.slug === slug);
    if (e) return `${e.letter} — ${e.title}`;
  } else if (section === "guides") {
    const e = ALL_GUIDES.find((g) => g.slug === slug);
    if (e) return e.title;
  } else {
    const e = ALL_POSTS.find((p) => p.slug === slug);
    if (e) return e.title;
  }
  // Fallback to SITE_NAV: nav-only editable pages (e.g. /sama/skill)
  // have a content/<...>.md backing file but no entry in the discipline
  // / guide / blog registries. They're listed in SITE_NAV with a
  // non-null editPath, which is the single source of truth for
  // "this docs page is editable".
  const navSection = SITE_NAV.find((s) => s.id === section);
  const link = navSection?.links.find(
    (l) => l.href === `/${section}/${slug}` && l.editPath !== null,
  );
  return link?.label ?? null;
};

export const resolveEdit = (section: string, slug: string): ResolvedEdit | null => {
  if (!isValidSection(section)) return null;
  if (!SAFE_SLUG.test(slug)) return null;
  const title = lookupTitle(section, slug);
  if (title === null) return null;
  // /sama discipline pages live under /sama/discipline/<slug> as of
  // PR #53. /blog pages live under /blog/<yyyy-mm>/<slug> as of PR #55.
  // Other sections keep the flat /<section>/<slug> shape.
  let pageUrl: string;
  if (section === "sama" && slug !== "skill" && slug !== "v2") {
    pageUrl = `/sama/discipline/${slug}`;
  } else if (section === "blog") {
    const post = ALL_POSTS.find((p) => p.slug === slug);
    pageUrl = post
      ? `/blog/${post.date.slice(0, 7)}/${slug}`
      : `/blog/${slug}`;
  } else {
    pageUrl = `/${section}/${slug}`;
  }
  return {
    section,
    slug,
    pageUrl,
    filePath: `content/${section}/${slug}.md`,
    title,
  };
};