//! Notes on the object a git page is showing, described. //! //! `templates/partials/git_notes_panel.html`'s contents. The commit page and //! the file view both carry it -- a note on a commit and a note on a blob are //! the same note drawn the same way -- so it stays one thing here for the //! reason it was one partial there. //! //! [`super::carousel`]'s shape: [`region`] is the description and [`html`] is //! the Askama half. //! //! # The body is a handover, and the header is not //! //! `CommitNote::html` has already been through docengine and the sanitiser, and //! neither pass is derivable from the note's markdown by anything downstream. //! That is `src/quasi/project.rs`'s split exactly: markup a host pipeline owns //! goes in a [`RegionKind::Handover`], and the frame over it is described. //! //! The alternative was [`Node::Rich`], which carries the source and lets each //! renderer make its own markup. It is the right member for a note *written* in //! this session and the wrong one here: `source` is kept for the edit box and //! rendering it a second way would draw a different document from the one the //! notes index, the feed and the search results all show. //! //! # A namespace is monospace because it is a ref path //! //! `commits`, `mnw/builds`: a name git will accept, not prose. It is said as an //! inline [`Node::Code`], which is the member for machine text in a line of //! reading, and the shipped rule set `font-family: var(--font-mono)` on it for //! the same reason without being able to say so. use quasi_declare::declare; use quasi_router::RegionKind; use quasi_webview::Webview; use crate::routes::git::notes_view::CommitNote; /// The panel's own region, and what a link to the notes on a page aims at. /// /// `notes` rather than a longer name because the shipped markup used it as the /// fragment target and links off the notes index point at it. pub const REGION: &str = "notes"; /// Where one note's rendered markdown lands. /// /// By namespace, which is unique within an object: a note is one blob per /// namespace, so two notes on a page cannot share this. Slugged first, because /// a namespace carries slashes (`mnw/builds`) and this becomes an element id: /// a slash there is legal HTML and is an escape every selector and every URL /// fragment that reaches for it has to remember. #[must_use] pub fn body_region(namespace: &str) -> String { format!("note-body-{}", slug(namespace)) } /// A namespace as an element id can carry it. See [`body_region`]. fn slug(namespace: &str) -> String { namespace .chars() .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) .collect() } declare! { /// The panel, for a screen that owns its whole document. /// /// `None` when there is nothing to show. The shipped partial wrapped itself /// in `{{% if !notes.is_empty() %}}`, and an empty panel here would be a /// bordered box with nothing in it rather than the absence the page wants. #[must_use] pub shape region(notes: &[CommitNote]) -> Option; region REGION as Group unless notes.is_empty() { for note in notes { include one(note); } } } /// The markup, for an Askama template to drop in. /// /// Pays its own handovers, which is what separates this from [`region`]: a /// described screen fills them from its own renderer ([`fill`]), and a template /// has no renderer to fill from. #[must_use] pub fn html(notes: &[CommitNote]) -> String { use quasi_axum::Serves as _; let Some(node) = region(notes) else { return String::new(); }; // No shell: a fragment landing inside a document Askama already built. fill(Webview::new(), notes).fragment(&node) } /// Pay every note's handover on a renderer. /// /// Separate from [`region`] because a screen builds its description once and /// its renderer once, and the two are not the same call. `src/quasi/project.rs` /// makes the same pair. #[must_use] pub fn fill(mut webview: Webview, notes: &[CommitNote]) -> Webview { for note in notes { // The app's own classes, which is what a handover is: the region is a // place and the markup in it is this host's. `git-note-body` and // `git-readme-body` are the rules the shipped panel wrote and the // annotations panel still writes, so a note and an annotation stay one // typography rather than two. webview = webview.with_fill( body_region(¬e.namespace), format!( "
{}
", note.html ), ); } webview } /// Who last edited the note, or the bound the walk stopped at. /// /// A supplier because the line is decided by `exact`, which is a field inside /// an `Option` the form has no binding pattern to reach. Empty when there is no /// attribution, which is what R9 asks of a supplier: the value is built whether /// or not the guard places it. /// /// `exact` is the load-bearing field. When the walk hit its budget the commit /// it stopped at is not the one that made the edit, so the line names a bound /// and never a person. fn attribution(note: &CommitNote) -> String { let Some(attribution) = ¬e.attribution else { return String::new(); }; if attribution.exact { format!("{} - {}", attribution.by, attribution.when) } else { format!( "edited since {} - {}", attribution.short_commit, attribution.when ) } } declare! { /// One note: what it is, where it came from, and what it says. /// /// The header is one line: a name on the left and a provenance on the /// right. Wrapping keeps both when there is no room for one line, which is /// what the shipped rule's `flex-wrap` did. A note nobody on the repo wrote /// says so, so it does not read as one of theirs they have forgotten. shape one(note: &CommitNote) -> Slot; region "note-{slug(¬e.namespace)}" as Group { region "note-header-{slug(¬e.namespace)}" as Group { across Wrap { beside Essential literal ¬e.namespace; beside Secondary text "written by makenot.work" when note.read_only; beside Secondary text attribution(note) when note.attribution.is_some(); } } region body_region(¬e.namespace) as RegionKind::handover("a rendered note") {} } } #[cfg(test)] mod tests { use super::*; use crate::routes::git::notes_view::NoteAttribution; fn note(namespace: &str, read_only: bool, attribution: Option) -> CommitNote { CommitNote { namespace: namespace.into(), html: format!("

the note in {namespace}

"), source: format!("the note in {namespace}"), attribution, read_only, } } fn attributed(exact: bool) -> NoteAttribution { NoteAttribution { short_commit: "abc1234".into(), by: "ada".into(), when: "3 days ago".into(), exact, } } /// The panel is the absence when there is nothing in it, which is what the /// shipped `{% if !notes.is_empty() %}` said. #[test] fn no_notes_is_no_panel() { assert!(region(&[]).is_none()); assert_eq!(html(&[]), ""); } /// The rendered markdown reaches the page, through the handover rather than /// through the description. #[test] fn the_rendered_note_is_what_the_reader_gets() { let html = html(&[note("commits", false, None)]); assert!(html.contains("

the note in commits

"), "{html}"); assert!(html.contains("git-note-body"), "{html}"); } /// A namespace carries slashes and lands in an element id, so it is slugged /// on the way. #[test] fn a_namespace_with_a_slash_is_still_an_id() { let html = html(&[note("mnw/builds", true, None)]); assert!(html.contains("id=\"note-body-mnw-builds\""), "{html}"); assert!(!html.contains("id=\"note-body-mnw/builds\""), "{html}"); // The name itself is unchanged where it is read. assert!(html.contains("mnw/builds"), "{html}"); } /// A note nobody on the repo wrote says so, and one they did does not. #[test] fn a_note_written_here_says_where_it_came_from() { assert!(html(&[note("mnw/builds", true, None)]).contains("written by makenot.work")); assert!(!html(&[note("commits", false, None)]).contains("written by makenot.work")); } /// When the walk hit its budget the commit it stopped at is not the one /// that made the edit, so the line names a bound and never a person. #[test] fn an_inexact_attribution_names_no_one() { let exact = html(&[note("commits", false, Some(attributed(true)))]); assert!(exact.contains("ada"), "{exact}"); let inexact = html(&[note("commits", false, Some(attributed(false)))]); assert!(inexact.contains("edited since abc1234"), "{inexact}"); assert!(!inexact.contains("ada"), "{inexact}"); } /// Each note is its own region, under the id the panel builds from the /// namespace. Two notes on one page are two boxes, and the header is a box /// inside each of them. #[test] fn every_note_is_its_own_region() { let html = html(&[ note("commits", false, None), note("mnw/builds", false, None), ]); assert!(html.contains("id=\"note-commits\""), "{html}"); assert!(html.contains("id=\"note-header-commits\""), "{html}"); assert!(html.contains("id=\"note-mnw-builds\""), "{html}"); assert!(html.contains("id=\"note-header-mnw-builds\""), "{html}"); } /// The namespace is machine text in a line of reading, which is what the /// inline code member is for. #[test] fn the_namespace_is_monospace() { let html = html(&[note("commits", false, None)]); assert!( html.contains("commits"), "{html}" ); } }