import * as cheerio from 'cheerio' import sanitizeHtml from 'sanitize-html' import { eq, inArray, sql } from 'drizzle-orm' import { jobs as jobsTable, pageRenderQueue as renderQueueTable } from '../db/schema.ts' import { CustomError } from '../helpers/common.ts' /** * Rendering model * * Markdown becomes HTML in the browser, not here: the editor renders as you type, and what it shows * in its preview is what gets sent up and stored. One renderer, one result — the preview cannot drift * from the saved page because they are the same render. * * What this model does is everything that has to happen *after* that, and cannot be left to the * client: * * - **Sanitizing.** The HTML arrived from a browser, so it is a user input like any other. What * survives depends on what the author is allowed to do — scripts and styles are permissions. * - **Normalizing.** The editor leaves scaffolding in its output (line markers for preview scroll * sync) that has no business being stored, and headings arrive without the anchors a table of * contents needs. * - **Extracting.** The table of contents and the plain text the search index is built from are both * derived from the final HTML, once it is settled. * * Re-rendering an existing page from its source — which the server needs when the content is there * but the render is stale — goes back through the very same frontend pipeline, driven in a headless * browser. That is a job rather than part of a request: see `queuePage` and `drainQueue`. */ /** How long the renderer bundle gets to load itself in the headless browser, in milliseconds. */ const RENDER_READY_TIMEOUT = 30000 /** How long a single render gets once the bundle is up, in milliseconds. */ const RENDER_TIMEOUT = 30000 /** The task that drains the render queue. One browser, one page at a time. */ const DRAIN_TASK = 'renderPages' /** A heading in the table of contents, shaped for the Quasar tree the page sidebar draws. */ export interface TocNode { key: string label: string /** * The heading's own level, 1 to 6. * * Kept alongside the nesting because the two say different things: a contents list is asked to show * "H1 to H2", which is about the tag an author reached for, and an `h3` written under an `h1` is * still an `h3` however few levels sit above it. */ level: number children: TocNode[] } export interface PostProcessResult { /** The HTML to store and serve. */ render: string /** The table of contents, derived from the headings. */ toc: TocNode[] /** Plain text, for the search index. */ text: string } /** * A headless browser standing by on the renderer bundle, good for any number of pages. * * Opening one is the expensive part of rendering, so it is handed out as a handle to be reused and * closed by whoever asked for it rather than opened per page. */ interface PageRenderer { /** * Markdown in, the editor's own HTML out — before `postProcess` gets to it. * * `context` carries what the source cannot say about itself, currently the page's own path: a * relative image in a page resolves against the folder it sits in, as it would in a repository. */ render( content: string, config: Record, context: Record ): Promise close(): Promise } /** What the author is allowed to put in a page, beyond ordinary content. */ export interface RenderPermissions { /** `write:scripts` — may embed `