Let a gesture name the manual section that shows it
The help sheet says which move does a thing, and the manual has a picture of the thing being done, but nothing joined the two: a user reading "Pinch it with two fingers" had no way from there to the GIF of it. A GESTURE tag takes an optional `manual:` field naming a heading of docs/manual/README.md by its anchor. The scan checks every one against the anchors the bundled page is rendered with and fails when the manual has no such heading, so renaming a section cannot leave the sheet linking to the top of the page; gestures-check carries the same failure into CI. The anchor goes into gesture_book.rs as a new field, and into docs/gestures.md as a "See it" link to manual/README.md#anchor. The help sheet draws a "See it" button beside the title of each gesture that has one, which opens the bundled manual at that section. The field is additive: a tag without it is unchanged, and no gesture carries one yet.
This commit is contained in:
@@ -57,6 +57,15 @@
|
||||
//! field before it, which is what lets `why` run to a sentence. An unrecognised
|
||||
//! key is an error rather than a silently dropped line: `pointr:` should not
|
||||
//! quietly cost a gesture its desktop half.
|
||||
//!
|
||||
//! # `manual:`, the section that shows it
|
||||
//!
|
||||
//! Optional. It names a heading of `docs/manual/README.md` by its anchor —
|
||||
//! `manual: looking-closer`, with or without the `#` — and the help sheet then
|
||||
//! offers "See it", opening the bundled manual there, where a picture shows
|
||||
//! the move being made. The anchor must exist: [`check_manual`] fails the scan
|
||||
//! when the manual has no such heading, so renaming a section cannot leave a
|
||||
//! link on the sheet that opens the manual at the top.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -79,6 +88,8 @@ pub struct Gesture {
|
||||
/// a user consulting the help sheet wants the gesture, and a reader of the
|
||||
/// document wants the argument.
|
||||
pub why: Option<String>,
|
||||
/// The manual section that shows it, as a heading anchor without `#`.
|
||||
pub manual: Option<String>,
|
||||
pub file: String,
|
||||
pub line: usize,
|
||||
}
|
||||
@@ -105,7 +116,7 @@ impl Gesture {
|
||||
}
|
||||
|
||||
/// A key that may appear in a gesture block.
|
||||
const KEYS: &[&str] = &["where", "touch", "pointer", "keys", "why"];
|
||||
const KEYS: &[&str] = &["where", "touch", "pointer", "keys", "why", "manual"];
|
||||
|
||||
/// Something wrong with a tag, reported rather than dropped.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -211,6 +222,7 @@ pub fn extract_from_text(text: &str, path: &str) -> (Vec<Gesture>, Vec<GesturePr
|
||||
pointer: take("pointer"),
|
||||
keys: take("keys"),
|
||||
why: take("why"),
|
||||
manual: take("manual").map(|m| m.trim_start_matches('#').to_string()),
|
||||
file: path.to_string(),
|
||||
line: start + 1,
|
||||
};
|
||||
@@ -285,6 +297,29 @@ fn split_key(body: &str) -> Option<(String, String)> {
|
||||
Some((key.to_ascii_lowercase(), tail.to_string()))
|
||||
}
|
||||
|
||||
/// Every `manual:` that names a section the manual does not have.
|
||||
///
|
||||
/// `anchors` is [`crate::manual::anchors`] of the README — the ids the bundled
|
||||
/// page gives its headings — so this and the page cannot disagree about what
|
||||
/// exists.
|
||||
pub fn check_manual(gestures: &[Gesture], anchors: &[String]) -> Vec<GestureProblem> {
|
||||
gestures
|
||||
.iter()
|
||||
.filter_map(|g| {
|
||||
let m = g.manual.as_ref()?;
|
||||
(!anchors.iter().any(|a| a == m)).then(|| GestureProblem {
|
||||
file: g.file.clone(),
|
||||
line: g.line,
|
||||
what: format!(
|
||||
"gesture `{}` names manual section `#{m}`, which docs/manual/README.md \
|
||||
has no heading for",
|
||||
g.title
|
||||
),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Group gestures under their `where:`, keeping the order the sections were
|
||||
/// first seen so the document reads in the order a user meets them.
|
||||
pub fn by_section(gestures: &[Gesture]) -> Vec<(String, Vec<&Gesture>)> {
|
||||
@@ -339,6 +374,11 @@ pub fn render_markdown(gestures: &[Gesture]) -> String {
|
||||
for (modality, how) in g.routes() {
|
||||
m.push_str(&format!("- **{modality}** — {how}\n"));
|
||||
}
|
||||
if let Some(anchor) = &g.manual {
|
||||
m.push_str(&format!(
|
||||
"- **See it** — [in the manual](manual/README.md#{anchor})\n"
|
||||
));
|
||||
}
|
||||
if let Some(why) = &g.why {
|
||||
m.push_str(&format!("\n{why}\n"));
|
||||
}
|
||||
@@ -381,6 +421,8 @@ pub fn render_rust(gestures: &[Gesture]) -> String {
|
||||
r.push_str(" pub touch: &'static str,\n");
|
||||
r.push_str(" pub pointer: &'static str,\n");
|
||||
r.push_str(" pub keys: &'static str,\n");
|
||||
r.push_str(" /// The manual section that shows it, a heading anchor; empty for none.\n");
|
||||
r.push_str(" pub manual: &'static str,\n");
|
||||
r.push_str("}\n\n");
|
||||
|
||||
r.push_str("/// Every gesture, in the order the sections were first seen in the source.\n");
|
||||
@@ -402,6 +444,10 @@ pub fn render_rust(gestures: &[Gesture]) -> String {
|
||||
" keys: {},\n",
|
||||
quote(g.keys.as_deref().unwrap_or(""))
|
||||
));
|
||||
r.push_str(&format!(
|
||||
" manual: {},\n",
|
||||
quote(g.manual.as_deref().unwrap_or(""))
|
||||
));
|
||||
r.push_str(" },\n");
|
||||
}
|
||||
}
|
||||
@@ -590,6 +636,38 @@ pub fn zoom() {}
|
||||
assert_eq!(g[1].title, "Second");
|
||||
}
|
||||
|
||||
/// `manual:` is read, with or without its `#`, and a section the manual
|
||||
/// does not have is a problem rather than a link to the top of the page.
|
||||
#[test]
|
||||
fn a_manual_anchor_must_name_a_section_the_manual_has() {
|
||||
let text = "\
|
||||
// GESTURE: Zoom
|
||||
// where: Develop
|
||||
// touch: Pinch
|
||||
// manual: #looking-closer
|
||||
// GESTURE: Pan
|
||||
// where: Develop
|
||||
// touch: Drag
|
||||
// manual: looking-further
|
||||
";
|
||||
let (g, p) = extract_from_text(text, "x.slint");
|
||||
assert!(p.is_empty(), "{p:?}");
|
||||
assert_eq!(g[0].manual.as_deref(), Some("looking-closer"));
|
||||
let anchors = crate::manual::anchors("## Looking closer\n");
|
||||
let problems = check_manual(&g, &anchors);
|
||||
assert_eq!(problems.len(), 1, "{problems:?}");
|
||||
assert!(problems[0].what.contains("looking-further"));
|
||||
assert_eq!(problems[0].line, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_manual_anchor_is_linked_from_the_document_and_carried_in_the_table() {
|
||||
let text = "// GESTURE: Zoom\n// where: Develop\n// touch: Pinch\n// manual: looking-closer\n";
|
||||
let (g, _) = extract_from_text(text, "x.slint");
|
||||
assert!(render_markdown(&g).contains("(manual/README.md#looking-closer)"));
|
||||
assert!(render_rust(&g).contains("manual: \"looking-closer\","));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sections_keep_the_order_they_were_first_seen() {
|
||||
let text = "\
|
||||
|
||||
@@ -161,6 +161,15 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
|
||||
problems.extend(p);
|
||||
}
|
||||
|
||||
// A `manual:` must name a section the manual has, checked against the
|
||||
// same anchors the bundled page is rendered with.
|
||||
let manual_source = std::fs::read_to_string(base.join(MANUAL_SOURCE))
|
||||
.with_context(|| format!("reading {MANUAL_SOURCE}"))?;
|
||||
problems.extend(gestures::check_manual(
|
||||
&found,
|
||||
&manual::anchors(&manual_source),
|
||||
));
|
||||
|
||||
println!("files scanned {}", files.len());
|
||||
println!("gestures found {}", found.len());
|
||||
println!("places {}", gestures::by_section(&found).len());
|
||||
|
||||
Reference in New Issue
Block a user