//! `cargo xtask site [--out DIR] [--app DIR]`: the project's web site for //! GitHub Pages (https://fabianhassler.github.io/rustfig/): a landing page, //! the manual (`docs/manual/*.md`) as HTML pages, the screenshot, a //! sitemap, and with `--app` the web app built by `cargo xtask web` under //! `app/`. Static HTML and CSS only (no scripts besides the app itself), so //! search engines read every page. The output is deterministic. use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag}; use std::fmt::Write as _; use std::path::{Path, PathBuf}; /// Where the site is served. pub const BASE: &str = "https://fabianhassler.github.io/rustfig/"; const REPO: &str = "https://github.com/FabianHassler/rustfig"; /// One manual page: file stem, title (its `# ` heading) and HTML body. pub struct Page { pub stem: String, pub title: String, pub description: String, pub body: String, } /// Escapes text for HTML. fn esc(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) } /// The manual's per-system blocks (`` … ``, /// which the program shows only on that system) as labelled boxes. fn os_blocks(md: &str) -> String { let mut out = String::with_capacity(md.len()); let mut open = false; for line in md.lines() { let t = line.trim(); if let Some(rest) = t.strip_prefix("")) { if open { out.push_str("\n\n\n"); open = false; } let systems: Vec<&str> = rest .split_whitespace() .filter(|s| *s != "end") .map(|s| match s { "mac" => "macOS", "windows" => "Windows", "linux" => "Linux", other => other, }) .collect(); if !systems.is_empty() { let _ = write!( out, "

On {}

\n\n", systems.join(" and ") ); open = true; } continue; } out.push_str(line); out.push('\n'); } if open { out.push_str("\n
\n"); } out } /// A link to another topic (`keys.md#ctrl`) becomes its page (`keys.html#ctrl`). fn page_link(dest: &str) -> Option { if dest.contains("://") { return None; } let (file, frag) = dest.split_once('#').map_or((dest, None), |(f, h)| (f, Some(h))); let stem = file.strip_suffix(".md")?; Some(match frag { Some(h) => format!("{stem}.html#{h}"), None => format!("{stem}.html"), }) } /// One topic's Markdown as a page. pub fn page(stem: &str, md: &str) -> Page { // The program's manual window has a search box; the site doesn't. let md = md.replace( " This manual is part of the program: it works offline and you can search it with the box above.", " The same manual is built into the program (Help > RustFig Manual), works offline and can be searched there.", ); let title = md .lines() .find_map(|l| l.strip_prefix("# ")) .unwrap_or(stem) .trim() .to_string(); // The first paragraph, for the search result snippet. let description = md .split("\n\n") .map(str::trim) .find(|p| !p.is_empty() && !p.starts_with('#') && !p.starts_with("\nm\n\nw\n\nb\n"); assert!(s.contains("On macOS

") && s.contains("On Linux and Windows

")); assert_eq!(s.matches("
").count(), s.matches("
").count()); assert!(!s.contains("