diff --git a/src/atproto/client.ts b/src/atproto/client.ts index 24a3a0f..6b13d72 100644 --- a/src/atproto/client.ts +++ b/src/atproto/client.ts @@ -15,7 +15,7 @@ import { type StoredSession, type StoredState, } from "@atcute/oauth-node-client"; -import { OAUTH_SCOPE } from "../lib/constants.ts"; +import { OAUTH_SCOPE } from "../lib/collections.ts"; import { getDb } from "../server/db/index.ts"; import type { AtprotoEnv } from "./env.ts"; diff --git a/src/atproto/pds.ts b/src/atproto/pds.ts index 8aeb71d..816ec52 100644 --- a/src/atproto/pds.ts +++ b/src/atproto/pds.ts @@ -1,7 +1,7 @@ import type { Client } from "@atcute/client"; import { ok } from "@atcute/client"; import type { Did, Nsid } from "@atcute/lexicons/syntax"; -import { COLLECTIONS } from "../lib/constants.ts"; +import { COLLECTIONS } from "../lib/collections.ts"; interface PdsWriteResult { uri: string; diff --git a/src/firehose/handlers.ts b/src/firehose/handlers.ts index 09e915e..b259445 100644 --- a/src/firehose/handlers.ts +++ b/src/firehose/handlers.ts @@ -1,4 +1,4 @@ -import { COLLECTIONS, normalizeRole } from "../lib/constants.ts"; +import { COLLECTIONS, normalizeRole } from "../lib/collections.ts"; import { LIMITS } from "../lib/limits.ts"; import { applyRevisionFromFirehose, diff --git a/src/firehose/index.ts b/src/firehose/index.ts index ad16357..0c14f9d 100644 --- a/src/firehose/index.ts +++ b/src/firehose/index.ts @@ -1,6 +1,6 @@ import { JetstreamSubscription } from "@atcute/jetstream"; import { getJetstreamUrl, isAuthEnabled } from "../atproto/env.ts"; -import { COLLECTIONS } from "../lib/constants.ts"; +import { COLLECTIONS } from "../lib/collections.ts"; import { getCursor, setCursor } from "../server/db/queries/index.ts"; import { type FirehoseCommit, handleCommitEvent } from "./handlers.ts"; diff --git a/src/lib/access-resolve.ts b/src/lib/access-resolve.ts new file mode 100644 index 0000000..8bf7154 --- /dev/null +++ b/src/lib/access-resolve.ts @@ -0,0 +1,127 @@ +import { getSessionFromRequest } from "../atproto/session.ts"; +import type { WikiRow } from "../server/db/queries/index.ts"; +import { + getMemberRole, + getRequest, + getWiki, +} from "../server/db/queries/index.ts"; +import { resolveUserTheme } from "../views/theme/index.ts"; +import { + canEdit, + canManage, + canRead, + getAccessLevel, + type RequestContext, + type WikiRequestContext, +} from "./access.ts"; +import { csrfTokenFor } from "./csrf.ts"; +import { ForbiddenError, NotFoundError } from "./errors.ts"; +import { resolveLocale } from "./i18n/index.ts"; +import { resolveHandleToDid, resolveProfile } from "./profile.ts"; + +export type { RequestContext, WikiRequestContext } from "./access.ts"; + +function narrowWikiContext( + ctx: RequestContext & { ownerHandle?: string | null }, + urlHandle: string, +): WikiRequestContext { + return { + ...ctx, + wiki: ctx.wiki as WikiRow, + ownerHandle: ctx.ownerHandle ?? urlHandle, + }; +} + +export async function resolveWikiContext( + request: Request, + urlHandle: string, + wikiSlug: string, + requiredAccess: "read" | "edit" | "admin", +): Promise { + const ctx = await resolveRequestContext(request, urlHandle, wikiSlug); + if (!ctx.wiki) { + throw new NotFoundError("Wiki not found", { i18nKey: "wikiNotFound" }); + } + if (requiredAccess !== "read" && !ctx.session) { + throw new ForbiddenError(); + } + const check = { read: canRead, edit: canEdit, admin: canManage }; + if (!check[requiredAccess](ctx.access)) { + throw new ForbiddenError(); + } + return narrowWikiContext(ctx, urlHandle); +} + +export async function resolveWikiContextSoft( + request: Request, + urlHandle: string, + wikiSlug: string, +): Promise { + const ctx = await resolveRequestContext(request, urlHandle, wikiSlug); + if (!ctx.wiki) { + throw new NotFoundError("Wiki not found", { i18nKey: "wikiNotFound" }); + } + return narrowWikiContext(ctx, urlHandle); +} + +export async function resolveRequestContext( + request: Request, + ownerHandle?: string, + wikiSlug?: string, +): Promise { + const session = await getSessionFromRequest(request); + const did = session?.did ?? null; + const csrfToken = did ? csrfTokenFor(did) : null; + const cookieHeader = request.headers.get("cookie"); + const locale = resolveLocale( + cookieHeader, + request.headers.get("accept-language"), + ); + const userTheme = resolveUserTheme(cookieHeader); + + const baseShared = { session, did, locale, userTheme, csrfToken } as const; + + if (!ownerHandle || !wikiSlug) { + return { + ...baseShared, + wiki: null, + access: "none", + hasPendingRequest: false, + }; + } + + const ownerDid = await resolveHandleToDid(ownerHandle); + if (!ownerDid) { + return { + ...baseShared, + wiki: null, + access: "none", + hasPendingRequest: false, + }; + } + + const wiki = getWiki(ownerDid, wikiSlug); + if (!wiki) { + return { + ...baseShared, + wiki: null, + access: "none", + hasPendingRequest: false, + }; + } + + const role = did ? getMemberRole(wiki.at_uri, did) : null; + const access = getAccessLevel(wiki, did, role); + const hasPendingRequest = + access === "none" && did ? getRequest(wiki.at_uri, did) !== null : false; + + const profile = await resolveProfile(wiki.did); + + return { + ...baseShared, + wiki, + ownerHandle: profile.handle, + access, + hasPendingRequest, + }; +} diff --git a/src/lib/access.ts b/src/lib/access.ts index 45d1cac..6f6fd31 100644 --- a/src/lib/access.ts +++ b/src/lib/access.ts @@ -1,15 +1,7 @@ -import { getSessionFromRequest, type Session } from "../atproto/session.ts"; -import type { WikiRow } from "../server/db/queries/index.ts"; -import { - getMemberRole, - getRequest, - getWiki, -} from "../server/db/queries/index.ts"; -import { resolveUserTheme, type UserTheme } from "../views/theme/index.ts"; -import { csrfTokenFor } from "./csrf.ts"; -import { ForbiddenError, NotFoundError } from "./errors.ts"; -import { type Locale, resolveLocale } from "./i18n/index.ts"; -import { resolveHandleToDid, resolveProfile } from "./profile.ts"; +import type { Session } from "../atproto/session.ts"; +import type { WikiRow } from "../server/db/types.ts"; +import type { UserTheme } from "../views/theme/index.ts"; +import type { Locale } from "./i18n/index.ts"; export type AccessLevel = "none" | "read" | "edit" | "admin"; @@ -18,17 +10,14 @@ export function getAccessLevel( userDid: string | null, memberRole: string | null, ): AccessLevel { - // Owner always gets admin if (userDid && wiki.did === userDid) { return "admin"; } - // Admin members get admin on any wiki if (memberRole === "admin") { return "admin"; } - // Contributors can edit on any wiki if (memberRole === "contributor") { return "edit"; } @@ -37,7 +26,6 @@ export function getAccessLevel( return "read"; } - // Private wiki: viewer members get read, everyone else gets none if (memberRole === "viewer") { return "read"; } @@ -64,133 +52,11 @@ export interface RequestContext { access: AccessLevel; locale: Locale; userTheme: UserTheme; - /** true only when access is "none" and the user has a pending access request */ hasPendingRequest: boolean; - /** CSRF token for authenticated sessions; null when the user is logged out. */ csrfToken: string | null; } -/** - * RequestContext with wiki guaranteed non-null and ownerHandle resolved. - * `ownerHandle` is non-null — it falls back to the URL-supplied handle when - * profile resolution returns nothing, so routes never have to repeat the - * `?? urlHandle` fallback. - */ export interface WikiRequestContext extends RequestContext { wiki: WikiRow; ownerHandle: string; } - -function narrowWikiContext( - ctx: RequestContext & { ownerHandle?: string | null }, - urlHandle: string, -): WikiRequestContext { - return { - ...ctx, - wiki: ctx.wiki as WikiRow, - ownerHandle: ctx.ownerHandle ?? urlHandle, - }; -} - -/** - * Resolve context, check wiki exists, and verify access level in one call. - * Returns a narrowed WikiRequestContext on success, throws on failure. - */ -export async function resolveWikiContext( - request: Request, - urlHandle: string, - wikiSlug: string, - requiredAccess: "read" | "edit" | "admin", -): Promise { - const ctx = await resolveRequestContext(request, urlHandle, wikiSlug); - if (!ctx.wiki) { - throw new NotFoundError("Wiki not found", { i18nKey: "wikiNotFound" }); - } - if (requiredAccess !== "read" && !ctx.session) { - throw new ForbiddenError(); - } - const check = { read: canRead, edit: canEdit, admin: canManage }; - if (!check[requiredAccess](ctx.access)) { - throw new ForbiddenError(); - } - return narrowWikiContext(ctx, urlHandle); -} - -/** - * Like resolveWikiContext but never throws -- returns the full context even when - * access is insufficient. Throws NotFoundError if wiki doesn't exist. - * Used for pages that render a custom access-denied view. - */ -export async function resolveWikiContextSoft( - request: Request, - urlHandle: string, - wikiSlug: string, -): Promise { - const ctx = await resolveRequestContext(request, urlHandle, wikiSlug); - if (!ctx.wiki) { - throw new NotFoundError("Wiki not found", { i18nKey: "wikiNotFound" }); - } - return narrowWikiContext(ctx, urlHandle); -} - -export async function resolveRequestContext( - request: Request, - ownerHandle?: string, - wikiSlug?: string, -): Promise { - const session = await getSessionFromRequest(request); - const did = session?.did ?? null; - const csrfToken = did ? csrfTokenFor(did) : null; - const cookieHeader = request.headers.get("cookie"); - const locale = resolveLocale( - cookieHeader, - request.headers.get("accept-language"), - ); - const userTheme = resolveUserTheme(cookieHeader); - - const baseShared = { session, did, locale, userTheme, csrfToken } as const; - - if (!ownerHandle || !wikiSlug) { - return { - ...baseShared, - wiki: null, - access: "none", - hasPendingRequest: false, - }; - } - - const ownerDid = await resolveHandleToDid(ownerHandle); - if (!ownerDid) { - return { - ...baseShared, - wiki: null, - access: "none", - hasPendingRequest: false, - }; - } - - const wiki = getWiki(ownerDid, wikiSlug); - if (!wiki) { - return { - ...baseShared, - wiki: null, - access: "none", - hasPendingRequest: false, - }; - } - - const role = did ? getMemberRole(wiki.at_uri, did) : null; - const access = getAccessLevel(wiki, did, role); - const hasPendingRequest = - access === "none" && did ? getRequest(wiki.at_uri, did) !== null : false; - - const profile = await resolveProfile(wiki.did); - - return { - ...baseShared, - wiki, - ownerHandle: profile.handle, - access, - hasPendingRequest, - }; -} diff --git a/src/lib/assets.ts b/src/lib/assets.ts index 05344d8..56d7399 100644 --- a/src/lib/assets.ts +++ b/src/lib/assets.ts @@ -33,6 +33,15 @@ function computeHash(localPath: string): string | null { * assetUrl("/public/dist.css") → "/public/dist.css?v=abcd1234ef" * assetUrl("https://cdn.example/foo.js") → "https://cdn.example/foo.js" (unchanged) */ +export const VIZ_SCRIPTS = [ + "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js", + "/public/viz/dist.js", +]; + +export const EDITOR_SCRIPTS = ["/public/editor/dist.js"]; + +export const KATEX_STYLESHEETS = ["/public/katex.css"]; + export function assetUrl(path: string): string { if (/^https?:\/\//.test(path)) return path; if (path.includes("?")) return path; diff --git a/src/lib/collections.ts b/src/lib/collections.ts new file mode 100644 index 0000000..9df2d83 --- /dev/null +++ b/src/lib/collections.ts @@ -0,0 +1,17 @@ +export const COLLECTIONS = { + wiki: "wiki.lichen.wiki", + note: "wiki.lichen.note", + noteRevision: "wiki.lichen.noteRevision", + membership: "wiki.lichen.membership", + memberRequest: "wiki.lichen.memberRequest", + bookmark: "wiki.lichen.bookmark", +} as const; + +export const OAUTH_SCOPE = `atproto include:wiki.lichen.permissions blob:*/*`; + +export type MemberRole = "admin" | "contributor" | "viewer"; + +export function normalizeRole(raw: string | null | undefined): MemberRole { + if (raw === "admin" || raw === "viewer") return raw; + return "contributor"; +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts deleted file mode 100644 index 96c652b..0000000 --- a/src/lib/constants.ts +++ /dev/null @@ -1,54 +0,0 @@ -export const COLLECTIONS = { - wiki: "wiki.lichen.wiki", - note: "wiki.lichen.note", - noteRevision: "wiki.lichen.noteRevision", - membership: "wiki.lichen.membership", - memberRequest: "wiki.lichen.memberRequest", - bookmark: "wiki.lichen.bookmark", -} as const; - -export const OAUTH_SCOPE = `atproto include:wiki.lichen.permissions blob:*/*`; - -export type MemberRole = "admin" | "contributor" | "viewer"; - -export function normalizeRole(raw: string | null | undefined): MemberRole { - if (raw === "admin" || raw === "viewer") return raw; - return "contributor"; -} - -export const VIZ_SCRIPTS = [ - "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js", - "/public/viz/dist.js", -]; - -export const EDITOR_SCRIPTS = ["/public/editor/dist.js"]; - -export const KATEX_STYLESHEETS = ["/public/katex.css"]; - -export const MIME_TO_EXT: Record = { - "image/jpeg": "jpg", - "image/png": "png", - "image/gif": "gif", - "image/webp": "webp", -}; - -export const IMAGE_EXTENSIONS = new Set([ - ".jpg", - ".jpeg", - ".png", - ".gif", - ".webp", -]); - -export const WIKI_LANGUAGES = [ - { code: "en", label: "English" }, - { code: "fr", label: "Francais" }, - { code: "es", label: "Espanol" }, - { code: "de", label: "Deutsch" }, - { code: "pt", label: "Portugues" }, - { code: "ja", label: "Japanese" }, - { code: "zh", label: "Chinese" }, - { code: "ko", label: "Korean" }, - { code: "ar", label: "Arabic" }, - { code: "eo", label: "Esperanto" }, -] as const; diff --git a/src/lib/diff-render.ts b/src/lib/diff-render.ts index b560b2b..39063b2 100644 --- a/src/lib/diff-render.ts +++ b/src/lib/diff-render.ts @@ -1,6 +1,7 @@ import type * as DiffLib from "diff"; import DiffMatchPatch from "diff-match-patch"; import { escapeHtml } from "./html.ts"; +import { normalizeLF } from "./normalize.ts"; const dmp = new DiffMatchPatch(); @@ -15,8 +16,6 @@ const INSERT = 1; const DELETE = -1; const EQUAL = 0; -const normalizeLF = (s: string): string => s.replace(/\r\n/g, "\n"); - function renderEqual(text: string): string { return `${escapeHtml(text)}`; } diff --git a/src/lib/image-types.ts b/src/lib/image-types.ts new file mode 100644 index 0000000..0fda174 --- /dev/null +++ b/src/lib/image-types.ts @@ -0,0 +1,25 @@ +const IMAGE_TYPES = [ + { mime: "image/jpeg", ext: ".jpg", aliases: [".jpeg"] }, + { mime: "image/png", ext: ".png" }, + { mime: "image/gif", ext: ".gif" }, + { mime: "image/webp", ext: ".webp" }, +] as const; + +export const MIME_TO_EXT: Record = Object.fromEntries( + IMAGE_TYPES.map((t) => [t.mime, t.ext.slice(1)]), +); + +export const IMAGE_EXTENSIONS: Set = new Set( + IMAGE_TYPES.flatMap((t) => + "aliases" in t ? [t.ext, ...t.aliases] : [t.ext], + ), +); + +export const MIME_BY_EXT: Record = Object.fromEntries( + IMAGE_TYPES.flatMap((t) => { + const entries: [string, string][] = [[t.ext, t.mime]]; + if ("aliases" in t) + for (const alias of t.aliases) entries.push([alias, t.mime]); + return entries; + }), +); diff --git a/src/lib/import-export/export.ts b/src/lib/import-export/export.ts index 5ad27ed..03ef088 100644 --- a/src/lib/import-export/export.ts +++ b/src/lib/import-export/export.ts @@ -5,7 +5,7 @@ import { getSidebarNotes, listNotesWithContent, } from "../../server/db/queries/index.ts"; -import { MIME_TO_EXT } from "../constants.ts"; +import { MIME_TO_EXT } from "../image-types.ts"; import { fetchVerifiedBlob } from "../pds-fetch.ts"; import { rewriteForExport } from "./markdown-transform.ts"; diff --git a/src/lib/import-export/markdown-transform.ts b/src/lib/import-export/markdown-transform.ts index 19ab9e8..d9e1c8a 100644 --- a/src/lib/import-export/markdown-transform.ts +++ b/src/lib/import-export/markdown-transform.ts @@ -1,5 +1,5 @@ import { basename } from "node:path"; -import { IMAGE_EXTENSIONS } from "../constants.ts"; +import { IMAGE_EXTENSIONS } from "../image-types.ts"; interface ExportResult { content: string; diff --git a/src/lib/import-export/zip-parse.ts b/src/lib/import-export/zip-parse.ts index 7dbde1d..f333083 100644 --- a/src/lib/import-export/zip-parse.ts +++ b/src/lib/import-export/zip-parse.ts @@ -1,23 +1,13 @@ import { basename } from "node:path"; import { unzipSync } from "fflate"; -import { IMAGE_EXTENSIONS } from "../constants.ts"; import { ImportError } from "../errors.ts"; +import { IMAGE_EXTENSIONS, MIME_BY_EXT } from "../image-types.ts"; +import { LIMITS } from "../limits.ts"; +import { normalizeLF } from "../normalize.ts"; import { isValidSlug, slugify } from "../slug.ts"; import { extractLocalImageRefs } from "./markdown-transform.ts"; import type { ImportedImage, ImportedNote, ImportResult } from "./types.ts"; -const MAX_UNCOMPRESSED_BYTES = 50 * 1024 * 1024; // 50MB -const MAX_ZIP_ENTRIES = 500; -const MAX_MD_FILES = 100; - -const MIME_BY_EXT: Record = { - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".webp": "image/webp", -}; - /** * Parse an import zip file, extracting markdown notes and referenced images. * Throws ImportError on validation failures. @@ -36,14 +26,14 @@ export function parseImportZip(buffer: ArrayBuffer): ImportResult { if (paths.length === 0) { throw new ImportError("Zip file is empty.", { i18nKey: "zipEmpty" }); } - if (paths.length > MAX_ZIP_ENTRIES) { + if (paths.length > LIMITS.zipImport.maxZipEntries) { throw new ImportError( - `Zip contains too many entries (${paths.length}, max ${MAX_ZIP_ENTRIES}).`, + `Zip contains too many entries (${paths.length}, max ${LIMITS.zipImport.maxZipEntries}).`, { i18nKey: "tooManyEntries", i18nVars: { count: String(paths.length), - max: String(MAX_ZIP_ENTRIES), + max: String(LIMITS.zipImport.maxZipEntries), }, }, ); @@ -53,7 +43,7 @@ export function parseImportZip(buffer: ArrayBuffer): ImportResult { let totalSize = 0; for (const data of Object.values(entries)) { totalSize += data.length; - if (totalSize > MAX_UNCOMPRESSED_BYTES) { + if (totalSize > LIMITS.zipImport.maxUncompressedBytes) { throw new ImportError("Zip content exceeds 50MB uncompressed limit.", { i18nKey: "zipTooLarge", }); @@ -81,11 +71,9 @@ export function parseImportZip(buffer: ArrayBuffer): ImportResult { const ext = name.slice(name.lastIndexOf(".")).toLowerCase(); if (ext === ".md" || ext === ".markdown") { - // Normalize CRLF (Windows-authored files) to LF so diffs against - // future edits don't churn on line-ending differences. mdFiles.push({ name, - content: new TextDecoder().decode(data).replace(/\r\n/g, "\n"), + content: normalizeLF(new TextDecoder().decode(data)), }); } else if (IMAGE_EXTENSIONS.has(ext)) { imageFiles.set(name.toLowerCase(), data); @@ -98,14 +86,14 @@ export function parseImportZip(buffer: ArrayBuffer): ImportResult { i18nKey: "noMarkdownFiles", }); } - if (mdFiles.length > MAX_MD_FILES) { + if (mdFiles.length > LIMITS.zipImport.maxMdFiles) { throw new ImportError( - `Too many markdown files (${mdFiles.length}, max ${MAX_MD_FILES}).`, + `Too many markdown files (${mdFiles.length}, max ${LIMITS.zipImport.maxMdFiles}).`, { i18nKey: "tooManyMarkdownFiles", i18nVars: { count: String(mdFiles.length), - max: String(MAX_MD_FILES), + max: String(LIMITS.zipImport.maxMdFiles), }, }, ); diff --git a/src/lib/languages.ts b/src/lib/languages.ts new file mode 100644 index 0000000..760452e --- /dev/null +++ b/src/lib/languages.ts @@ -0,0 +1,12 @@ +export const WIKI_LANGUAGES = [ + { code: "en", label: "English" }, + { code: "fr", label: "Francais" }, + { code: "es", label: "Espanol" }, + { code: "de", label: "Deutsch" }, + { code: "pt", label: "Portugues" }, + { code: "ja", label: "Japanese" }, + { code: "zh", label: "Chinese" }, + { code: "ko", label: "Korean" }, + { code: "ar", label: "Arabic" }, + { code: "eo", label: "Esperanto" }, +] as const; diff --git a/src/lib/limits.ts b/src/lib/limits.ts index e8d516b..2995136 100644 --- a/src/lib/limits.ts +++ b/src/lib/limits.ts @@ -57,4 +57,19 @@ export const LIMITS = { login: { limit: 5, windowMs: 60_000 }, search: { limit: 30, windowMs: 60_000 }, }, + historyPageSize: 25, + linkSuggestLimit: 20, + snapshotCap: 25, + zipImport: { + maxUncompressedBytes: 50 * 1024 * 1024, + maxZipEntries: 500, + maxMdFiles: 100, + }, + cacheTtl: { + ogCard: 3600, + sitemap: 3600, + blobLocal: 86400, + blobImmutable: 31536000, + cookie: 31536000, + }, } as const; diff --git a/src/lib/normalize.ts b/src/lib/normalize.ts new file mode 100644 index 0000000..4b63f91 --- /dev/null +++ b/src/lib/normalize.ts @@ -0,0 +1,3 @@ +export function normalizeLF(s: string): string { + return s.replace(/\r\n/g, "\n"); +} diff --git a/src/lib/orchestrators/bookmark.ts b/src/lib/orchestrators/bookmark.ts index a8facb2..67f6e52 100644 --- a/src/lib/orchestrators/bookmark.ts +++ b/src/lib/orchestrators/bookmark.ts @@ -7,7 +7,7 @@ import { upsertBookmark, } from "../../server/db/queries/index.ts"; import { parseAtUri } from "../at-uri.ts"; -import { COLLECTIONS } from "../constants.ts"; +import { COLLECTIONS } from "../collections.ts"; import { withPdsError } from "./helpers.ts"; export async function addBookmarkAction( diff --git a/src/lib/orchestrators/membership.ts b/src/lib/orchestrators/membership.ts index 281bf92..a6b4365 100644 --- a/src/lib/orchestrators/membership.ts +++ b/src/lib/orchestrators/membership.ts @@ -15,8 +15,7 @@ import { } from "../../server/db/queries/index.ts"; import type { WikiRequestContext } from "../access.ts"; import { parseAtUri } from "../at-uri.ts"; -import type { MemberRole } from "../constants.ts"; -import { COLLECTIONS } from "../constants.ts"; +import { COLLECTIONS, type MemberRole } from "../collections.ts"; import { ForbiddenError, NotFoundError, ValidationError } from "../errors.ts"; import { t } from "../i18n/index.ts"; import { withPdsError } from "./helpers.ts"; diff --git a/src/lib/orchestrators/note.ts b/src/lib/orchestrators/note.ts index 7b5f6d7..339084e 100644 --- a/src/lib/orchestrators/note.ts +++ b/src/lib/orchestrators/note.ts @@ -19,7 +19,7 @@ import { buildBlobsForContent, parseBlobMetadata, } from "../attachments.ts"; -import { COLLECTIONS } from "../constants.ts"; +import { COLLECTIONS } from "../collections.ts"; import { createDiff } from "../diff.ts"; import { ForbiddenError, @@ -29,6 +29,7 @@ import { } from "../errors.ts"; import { fmt, type Messages } from "../i18n/index.ts"; import { LIMITS } from "../limits.ts"; +import { normalizeLF } from "../normalize.ts"; import { validateNewNote } from "../note-validation.ts"; import { withPdsError } from "./helpers.ts"; @@ -43,12 +44,7 @@ export function parseNoteFormFields(formData: { get(name: string): string | File | null; }): NoteFormFields { const title = (formData.get("title") as string | null) ?? ""; - // Browsers serialize @@ -83,7 +82,7 @@ export function editNotePage( type="submit" class="${primaryButtonClass}" >${msg.editor.save} - ${msg.editor.cancel} + ${msg.editor.cancel} - + `; return layout(`${msg.wiki.edit} ${noteTitle}`, scrollFixStyle + formHtml, { ...options, - wikiName, - wikiSlug, - ownerHandle, + wikiName: wiki.name, + wikiSlug: wiki.slug, + ownerHandle: wiki.handle, noindex: true, }); } diff --git a/src/views/edit-sidebar.ts b/src/views/edit-sidebar.ts index eafaabf..846b92b 100644 --- a/src/views/edit-sidebar.ts +++ b/src/views/edit-sidebar.ts @@ -1,6 +1,7 @@ import { escapeHtml } from "../lib/html.ts"; import { t } from "../lib/i18n/index.ts"; import { noteUrl, sidebarEditUrl, wikiUrl } from "../lib/urls.ts"; +import type { WikiIdentity } from "../lib/wiki-identity.ts"; import { type LayoutOptions, layout } from "./layout.ts"; import { inputClass, @@ -10,9 +11,7 @@ import { } from "./theme/index.ts"; export function editSidebarPage( - wikiName: string, - ownerHandle: string, - wikiSlug: string, + wiki: WikiIdentity, currentSidebar: string, notes: { slug: string; title: string }[], options: LayoutOptions & { error?: string }, @@ -24,7 +23,7 @@ export function editSidebarPage( .sort((a, b) => a.title.localeCompare(b.title)) .map( (n) => - `
  • ${escapeHtml(n.title)}
  • `, + `
  • ${escapeHtml(n.title)}
  • `, ) .join("\n"); @@ -79,25 +78,25 @@ export function editSidebarPage( ${defaultColumnCard} `; - const backLink = `← ${msg.wiki.backToWikiPrefix} “${escapeHtml(wikiName)}”`; + const backLink = `← ${msg.wiki.backToWikiPrefix} “${escapeHtml(wiki.name)}”`; const body = ` ${backLink}

    ${msg.wiki.editSidebarHeading}

    ${errorBanner} ${tipsRow} - -