//! The field of DNA as markup, for the `
` the post-login page hangs it
//! behind.
//!
//! [`crate::art`] draws it; this writes the finished grid out as HTML. The
//! two renderings walk the same [`Canvas::rows`](crate::art::Canvas::rows),
//! so the browser gets the terminal's art glyph for glyph — which is a claim
//! the tests below hold rather than a hope.
use crate::art::{self, Canvas, Ink};
/// The field prerendered into the post-login page, in cells. This one does
/// not follow anything the way `about`'s frame follows a terminal: the page
/// is drawn once, by the process handling the OAuth callback, and has no idea
/// what it will be opened on. So it is cut generously — wider and taller than
/// a laptop screen holds at the type size the page asks for — and the page
/// fades it out at its own edges, which makes a screen too large to cover
/// look like art floating in space rather than a texture that ran out.
///
/// 79 rows is a whole number of half turns past the first, the property
/// [`art::snap_rows`] exists to preserve, so the bottom edge is as wide as
/// the top one. Barely visible under the fade, and free to keep.
const PAGE_COLUMNS: i32 = 240;
const PAGE_ROWS: i32 = 79;
/// The CSS class for a cell's ink, given what a caller wants done with
/// [`Ink::Muted`].
///
/// The bases always get one. Muted is the choice: in the field it is every
/// bond glyph — most of the grid — and wrapping each run of `-` and `=` in
/// an element to say "this is the colour the `` is already painted in"
/// would multiply the page's largest asset for nothing. On the panel it is
/// the border and the labels, which have to be *dimmer* than the text beside
/// them, so there it is worth an element.
///
/// [`Ink::Default`] never gets one either way. That is what the word means:
/// whatever colour the block it landed in is set in.
fn class_for(ink: Ink, muted: &'static str) -> &'static str {
match ink {
Ink::A => "a",
Ink::T => "t",
Ink::G => "g",
Ink::C => "c",
Ink::Muted => muted,
Ink::Default => "",
}
}
/// One canvas as markup: a run of one color becomes one `` with a
/// one-letter class instead of one SGR escape. Uncolored runs are emitted
/// bare, so a field's bonds and blanks cost nothing but their own glyphs.
///
/// Written for a ``, where the newlines and the leading blanks are the
/// layout. The glyphs are escaped even though the field draws none of the
/// three that need it — the canvas is general, and a page assembled from it
/// should not depend on what happens to be drawn on it.
fn render(canvas: &Canvas, muted: &'static str) -> String {
let mut out = String::new();
for row in canvas.rows() {
let mut open = "";
for cell in row {
let class = class_for(cell.ink, muted);
if class != open {
if !open.is_empty() {
out.push_str("");
}
if !class.is_empty() {
out.push_str(" out.push_str("<"),
'>' => out.push_str(">"),
ch => out.push(ch),
}
}
if !open.is_empty() {
out.push_str("");
}
out.push('\n');
}
out
}
/// The field, with nothing punched out of it, ready for a ``.
///
/// Prerendered here rather than drawn in the browser — it is the same art
/// `atgc about` shows, from the same code, and a page that arrives finished
/// has nothing to lay out twice or to get wrong with a font that loads late.
pub(crate) fn field() -> String {
let mut canvas = Canvas::new(PAGE_COLUMNS, PAGE_ROWS);
art::draw_field(&mut canvas);
render(&canvas, "")
}
/// Any canvas as markup, with the muted ink marked up too: for a block whose
/// text is the subject rather than the texture, where a border has to read
/// as quieter than the line inside it.
///
/// Only `site` wants this, so it is compiled where that is — it is
/// behind the `www` feature, which is why this is not a doc link.
#[cfg(any(feature = "www", test))]
pub(crate) fn panel(canvas: &Canvas) -> String {
render(canvas, "m")
}
#[cfg(test)]
mod tests {
use super::*;
/// CSS class per base, in [`art::STRAND`] order. The mapping lives in
/// [`class_for`]; this is the list the tests below check it against.
const CLASSES: [&str; 4] = ["a", "t", "g", "c"];
/// The inverse of what `render_html` adds: drop the elements, put the
/// three escaped characters back, and the glyphs are all that is left.
fn strip_tags(html: &str) -> String {
let mut out = String::new();
let mut rest = html;
while let Some(open) = rest.find('<') {
out.push_str(&rest[..open]);
let close = open + rest[open..].find('>').expect("unclosed element");
rest = &rest[close + 1..];
}
out.push_str(rest);
out.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
}
// -- the field in HTML -----------------------------------------------
/// The browser gets the same art as the terminal, from the same code:
/// strip the markup back out of the HTML and what is left is the plain
/// render of the same canvas, glyph for glyph. This is the claim the
/// whole prerendering rests on — everything else about the page is CSS.
#[test]
fn the_html_field_is_the_terminal_field_with_markup_around_it() {
let mut canvas = Canvas::new(PAGE_COLUMNS, PAGE_ROWS);
art::draw_field(&mut canvas);
let stripped = strip_tags(&render(&canvas, ""));
assert_eq!(stripped, canvas.render(false));
assert_eq!(stripped.lines().count(), PAGE_ROWS as usize);
// And `field` draws that canvas rather than one of its own.
assert_eq!(field(), render(&canvas, ""));
}
/// Only the bases are wrapped. The bonds are the bulk of the field's
/// glyphs and they are left bare for the `` to color, which is what
/// keeps the page to tens of kilobytes instead of hundreds.
#[test]
fn only_the_bases_are_wrapped_in_an_element() {
let html = field();
let bases = html
.chars()
.filter(|c| matches!(c, 'A' | 'T' | 'G' | 'C'))
.count();
assert_eq!(html.matches("").count(), bases);
// Every class is one of the four, and every one of the four is used.
for class in CLASSES {
assert!(
html.contains(&format!("")),
"no {class} in the field"
);
}
let classes: Vec<&str> = html
.split("", Ink::Default);
canvas.put(3, 0, '<', Ink::of('A'));
assert_eq!(render(&canvas, ""), "<&><\n");
assert!(!field().contains("<"), "the field draws no markup");
}
}