Skip to content

Commit 0e23b14

Browse files
nikomatsakisNiko Matsakis
andauthored
restructure the formatting of team asks (#245)
* restructure the formatting of team asks Break down by goal and have a table with the kind of asks. * sort by title, shorten link * elaborate more in the RFC itself --------- Co-authored-by: Niko Matsakis <nikomat@amazon.com>
1 parent 7653471 commit 0e23b14

18 files changed

+269
-108
lines changed

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/mdbook-goals/src/mdbook_preprocessor.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use mdbook::book::{Book, Chapter};
88
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
99
use mdbook::BookItem;
1010
use regex::{Captures, Regex};
11-
use rust_project_goals::util::GithubUserInfo;
11+
use rust_project_goals::config::Configuration;
12+
use rust_project_goals::format_team_ask::format_team_asks;
13+
use rust_project_goals::util::{self, GithubUserInfo};
1214

1315
use rust_project_goals::{
14-
goal::{self, format_team_asks, GoalDocument, Status, TeamAsk},
16+
goal::{self, GoalDocument, Status, TeamAsk},
1517
re, team,
1618
};
1719

@@ -129,6 +131,7 @@ impl<'c> GoalPreprocessorWithContext<'c> {
129131
BookItem::Chapter(chapter) => {
130132
self.replace_metadata_placeholders(chapter)?;
131133
self.replace_team_asks(chapter)?;
134+
self.replace_valid_team_asks(chapter)?;
132135
self.replace_goal_lists(chapter)?;
133136
self.replace_goal_count(chapter)?;
134137
self.link_teams(chapter)?;
@@ -261,10 +264,30 @@ impl<'c> GoalPreprocessorWithContext<'c> {
261264
Ok(())
262265
}
263266

267+
/// Look for `<!-- TEAM ASKS -->` in the chapter content and replace it with the team asks.
268+
fn replace_valid_team_asks(&mut self, chapter: &mut Chapter) -> anyhow::Result<()> {
269+
if !re::VALID_TEAM_ASKS.is_match(&chapter.content) {
270+
return Ok(());
271+
}
272+
let config = Configuration::get();
273+
let rows = std::iter::once(vec!["Ask".to_string(), "aka".to_string(), "Description".to_string()])
274+
.chain(config.team_asks.iter().map(|(name, details)| {
275+
vec![
276+
format!("{name:?}"),
277+
details.short.to_string(),
278+
details.about.to_string(),
279+
]
280+
}))
281+
.collect::<Vec<Vec<String>>>();
282+
let table = util::format_table(&rows);
283+
let new_content = re::VALID_TEAM_ASKS.replace_all(&chapter.content, table);
284+
chapter.content = new_content.to_string();
285+
Ok(())
286+
}
287+
264288
/// Find the goal documents for the milestone in which this `chapter_path` resides.
265289
/// e.g., if invoked with `2024h2/xxx.md`, will find all goal documents in `2024h2`.
266290
fn goal_documents(&mut self, chapter_path: &Path) -> anyhow::Result<Arc<Vec<GoalDocument>>> {
267-
268291
let Some(milestone_path) = chapter_path.parent() else {
269292
anyhow::bail!("cannot get goal documents from `{chapter_path:?}`")
270293
};

crates/rust-project-goals/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ walkdir = "2.5.0"
1616
rust_team_data = { git = "https://github.com/rust-lang/team" }
1717
rust-project-goals-json = { version = "0.1.0", path = "../rust-project-goals-json" }
1818
toml = "0.8.19"
19+
indexmap = "2.7.1"

crates/rust-project-goals/src/config.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
use std::{collections::BTreeMap, path::PathBuf};
1+
use std::path::PathBuf;
22

33
use anyhow::Context;
4+
use indexmap::IndexMap;
45
use serde::Deserialize;
56

67
#[derive(Deserialize)]
78
pub struct Configuration {
8-
pub team_asks: BTreeMap<String, String>,
9+
/// Defines the valid "asks" of teams. The key is the ask, the value is an extended description.
10+
/// IndexMap is used to preserve the ordering as defined in the TOML file.
11+
pub team_asks: IndexMap<String, TeamAskDetails>,
12+
}
13+
14+
#[derive(Deserialize)]
15+
pub struct TeamAskDetails {
16+
/// A short descriptor of the team ask suitable for inclusion in a table
17+
pub short: String,
18+
19+
/// Longer description
20+
pub about: String,
921
}
1022

1123
impl Configuration {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use std::collections::BTreeSet;
2+
3+
use indexmap::IndexMap;
4+
5+
use crate::{config::Configuration, goal::TeamAsk, team::TeamName, util};
6+
7+
/// Format a set of team asks into a table, with asks separated by team and grouped by kind.
8+
///
9+
/// output looks like
10+
///
11+
/// ```ignore
12+
/// | Goal | [DMS](#discussion-and-moral-support) | [SR](#standard reviews) |
13+
/// | :--- | :-- | :-- |
14+
/// | Foo | ✅ | ✅ (notes) |
15+
/// | Bar (Baz) | ✅ | ✅ (\*1) |
16+
///
17+
/// \*1: ... longer notes that would not fit ...
18+
/// ```
19+
pub fn format_team_asks(asks_of_any_team: &[&TeamAsk]) -> anyhow::Result<String> {
20+
use std::fmt::Write;
21+
22+
const CHECK: &str = "✅";
23+
24+
/// Arbitrary: max length of text before we insert a footnote
25+
const FOOTNOTE_LEN: usize = 22;
26+
27+
let mut output = String::new();
28+
29+
let all_teams: BTreeSet<&TeamName> = asks_of_any_team
30+
.iter()
31+
.flat_map(|a| &a.teams)
32+
.copied()
33+
.collect();
34+
35+
// The set of configured team asks
36+
let config = Configuration::get();
37+
38+
for team_name in all_teams {
39+
let asks_of_this_team: Vec<_> = asks_of_any_team
40+
.iter()
41+
.filter(|a| a.teams.contains(&team_name))
42+
.collect();
43+
44+
let team_data = team_name.data();
45+
write!(output, "\n### {} team\n", team_data.name)?;
46+
47+
// We will accumulate footnotes when we encounter comments that are too long.
48+
let mut footnotes = vec![];
49+
50+
// These are things like "discussion and moral support". They are extracted from
51+
// the configuration. We prune out the ones that do not appear in the asks for a particular team.
52+
let ask_headings = config
53+
.team_asks
54+
.keys()
55+
.filter(|&ask_kind| {
56+
asks_of_this_team
57+
.iter()
58+
.any(|a| &a.ask_description == ask_kind)
59+
})
60+
.collect::<Vec<_>>();
61+
let empty_row = || {
62+
(0..ask_headings.len())
63+
.map(|_| "".to_string())
64+
.collect::<Vec<_>>()
65+
};
66+
67+
// Collect the asks by goal. The `rows` map goes from goal title to a row with entries
68+
let mut goal_rows: IndexMap<String, Vec<String>> = IndexMap::default();
69+
for ask in &asks_of_this_team {
70+
let link = format!("{}", ask.link_path.display());
71+
72+
let goal_title = match &ask.goal_titles[..] {
73+
[goal_title] => format!("[{goal_title}]({link}#ownership-and-team-asks)"),
74+
[goal_title, subgoal_title] => {
75+
format!("[{subgoal_title}]({link}#ownership-and-team-asks) (part of [{goal_title}]({link}))")
76+
}
77+
_ => anyhow::bail!(
78+
"expected either 1 or 2 goal titles, not {:?}",
79+
ask.goal_titles
80+
),
81+
};
82+
83+
let row = goal_rows.entry(goal_title).or_insert_with(empty_row);
84+
85+
let index = ask_headings
86+
.iter()
87+
.position(|&h| h == &ask.ask_description)
88+
.unwrap();
89+
90+
let text = if !ask.notes.is_empty() {
91+
&ask.notes
92+
} else {
93+
CHECK
94+
};
95+
96+
let mut maybe_footnote = |text: &str| -> String {
97+
if text.len() > FOOTNOTE_LEN {
98+
let footnote_index = footnotes.len() + 1;
99+
footnotes.push(format!("\\*{footnote_index}: {text} ([from here]({link}))", link = ask.link_path.display()));
100+
format!("\\*{footnote_index}")
101+
} else {
102+
text.to_string()
103+
}
104+
};
105+
106+
if !row[index].is_empty() {
107+
row[index] = format!("{} {}", row[index], maybe_footnote(text));
108+
} else {
109+
row[index] = maybe_footnote(text);
110+
}
111+
}
112+
113+
// Sort the goal rows by name (ignoring case).
114+
goal_rows.sort_by_cached_key(|ask_names, _ask_rows| {
115+
ask_names.clone().to_uppercase()
116+
});
117+
118+
// Create the table itself.
119+
let table = {
120+
let headings = std::iter::once("Goal".to_string())
121+
.chain(
122+
ask_headings
123+
.iter()
124+
.map(|&ask_kind| format!(
125+
"[{team_ask_short}][valid_team_asks]", // HACK: This should not be hardcoded in the code.
126+
team_ask_short = config.team_asks[ask_kind].short,
127+
))
128+
) // e.g. "discussion and moral support"
129+
.collect::<Vec<String>>();
130+
131+
let rows = goal_rows.into_iter().map(|(goal_title, goal_columns)| {
132+
std::iter::once(goal_title)
133+
.chain(goal_columns)
134+
.collect::<Vec<String>>()
135+
});
136+
137+
std::iter::once(headings).chain(rows).collect::<Vec<_>>()
138+
};
139+
140+
write!(output, "{}", util::format_table(&table))?;
141+
142+
for footnote in footnotes {
143+
write!(output, "\n\n{}\n", footnote)?;
144+
}
145+
}
146+
147+
Ok(output)
148+
}

0 commit comments

Comments
 (0)