/**
* The pages, rendered on the server: the first paint is the whole vault —
* your notes on the left, the note in the middle, what it links to and
* what links back on the right — and `static/app.js` makes it an editor
* afterwards (autosave, live preview, the quick switcher, the keys).
* Without JavaScript every page still reads, and the editor still saves.
*/
import { isAccount, type Account } from "./accounts.ts";
import { canDelete, type Note } from "./actions.ts";
import { commit, description, env, publicOrigin, title, where } from "./env.ts";
import { asset, escape } from "./http.ts";
import { token, tokensHref } from "./tokens.ts";
import {
backlinks,
excerpt,
graph,
headings,
hrefOf,
outgoing,
renderNote,
tagCounts,
tags,
type Hit,
} from "./vault.ts";
import type { Who } from "./who.ts";
/** Data for the browser, inside a page: JSON that can never close its `;
const CHANGES = "note.added note.removed note.changed";
/**
* The frame every page shares: the tokens and the look, and what a link
* to the page says when it is shared — a title, a sentence and the
* address, for Open Graph and the card a chat app unfurls.
*/
export function layout(
body: string,
path = "/",
options: { name?: string; kind?: string; scripts?: string[] } = {},
): string {
const name = options.name === undefined ? title : `${options.name} · ${title}`;
return `
${escape(name)}
${(options.scripts ?? []).map((s) => ``).join("\n")}
${body}
`;
}
/** Signed out: what this is, and the ways in. */
export function landing(notice?: string): string {
return layout(
`
${glyph}
${escape(title)}
A private vault for your thinking. Markdown notes, linked with [[double brackets]], with backlinks, tags and a graph of how it all connects.
${s.active === undefined ? "" : ``}
`;
/** One note, open: the editor and the reading view side by side. */
export function notePage(s: Shell & { active: Note }): string {
const note = s.active;
const tools = `
Saved${icon.download}
${canDelete(s.who, note) ? `` : ""}`;
const center = `${toolbar(s, `Vault/${escape(note.title)}`, tools)}
${graphData(s.vault)}`;
return layout(shell(s, center, right(note, s.vault)), hrefOf(note), {
name: note.title,
kind: "page app-page",
scripts: ["app.js", "graph.js"],
});
}
/** An empty vault: two ways to begin. */
export function emptyPage(s: Shell): string {
const center = `${toolbar(s, `Vault`)}
${glyph}
Your vault is empty
Every note you write here is markdown, saved as you type, and private to you.
`;
return layout(shell(s, center), "/", { kind: "page app-page", scripts: ["app.js"] });
}
/** A link to a note that isn't written yet. */
export function missingPage(s: Shell & { title: string }): string {
const center = `${toolbar(s, `Vault/${escape(s.title)}`)}
“${escape(s.title)}” doesn't exist yet
A link points here, but nothing's been written.
`;
return layout(shell(s, center), "/open", { name: s.title, kind: "page app-page", scripts: ["app.js"] });
}
/** What a search found. */
export function searchPage(s: Shell & { query: string; hits: readonly Hit[] }): string {
const words = s.query
.toLowerCase()
.match(/"[^"]+"|\S+/g)
?.map((w) => w.replace(/^"|"$/g, ""))
.filter((w) => !/^(#|tag:)/.test(w)) ?? [];
const mark = (text: string): string => {
let html = escape(text);
for (const w of words) {
const e = escape(w).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (e !== "") html = html.replace(new RegExp(`(${e})`, "gi"), "$1");
}
return html;
};
const center = `${toolbar(s, `Vault/Search`)}
${s.hits.length} ${s.hits.length === 1 ? "note" : "notes"} for ${escape(s.query)}