//! Rendering a creator's markdown back to them before it is saved. //! //! The described markdown field (`crate::quasi::rich_field`) emits a Preview //! pane and nothing that fills it: `makeover-webview` never turns a value into //! markup, on the stated grounds that a host with its own sanitiser and its own //! content-security posture still owns both. This server has both, so this is //! where the pane is filled. //! //! # Why the server and not the browser //! //! The four hand-written editors this replaces had one preview between them, //! and it was a regex pass over h1-h3, bold, italic and inline code //! (`static/partial-item-text-editor.js`, before this landed). It was safe -- //! it escaped first and substituted after -- and it was not what publishes. A //! creator reading it learned what that regex did, not what //! `render_creator_markdown` does, so tables, links, lists and media rewriting //! all appeared for the first time after save. //! //! Rendering here answers with the pipeline that actually publishes, ammonia //! and media-host restriction included, which is both the honest preview and //! the one place the sanitising stays. See `crate::markdown` for the policy. use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; use crate::{ auth::AuthUser, config::Config, error::Result, validation::{self, limits}, }; /// What the editor is holding right now. #[derive(Debug, Deserialize)] pub(super) struct PreviewRequest { /// The markdown source, unsaved. pub markdown: String, } /// The pane's contents. #[derive(Debug, Serialize)] pub(super) struct PreviewResponse { /// Sanitised markup, ready for the pane. Named `html` rather than `body` /// because nothing here is a resource and there is nothing else to return. html: String, } /// Render unsaved markdown the way saving it would. /// /// Authenticated because the media rewriting is per-creator: relative image /// paths resolve under the asking user's CDN prefix, which is the same thing /// `render_creator_markdown` does at save time. Stores nothing. #[tracing::instrument(skip_all, name = "preview::markdown")] pub(super) async fn preview_markdown( State(config): State, AuthUser(user): AuthUser, Json(req): Json, ) -> Result> { // The same bound the save path applies, so a body too long to store is // refused here rather than previewed and then rejected. `ITEM_TEXT_BODY_MAX` // is the largest of the four surfaces' limits; a shorter one is caught by // its own endpoint on save. if req.markdown.chars().count() > limits::ITEM_TEXT_BODY_MAX { return Err(crate::error::AppError::validation(format!( "Content must be {} characters or less", limits::ITEM_TEXT_BODY_MAX ))); } validation::reject_control_chars_multiline("Content", &req.markdown)?; let html = crate::markdown::render_creator_markdown( &req.markdown, user.id, config.cdn_base_url.as_str(), ); Ok(Json(PreviewResponse { html })) }