Skip to content

Commit e78833b

Browse files
committed
create a mdbook mdbook_preprocessor
1 parent 77e2f2a commit e78833b

File tree

10 files changed

+2569
-529
lines changed

10 files changed

+2569
-529
lines changed

Cargo.lock

+2,092-114
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

book.toml

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ multilingual = false
55
src = "src"
66
title = "Rust Project Goals"
77

8+
[preprocessor.goals]
9+
command = "cargo run -p mdbook-goals --"
10+
811
[output.html]
912
git-repository-url = "https://github.com/rust-lang/rust-project-goals"
1013
edit-url-template = "https://github.com/rust-lang/rust-project-goals/edit/main/{path}"

mdbook-goals/Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@ edition = "2021"
55

66
[dependencies]
77
anyhow = "1.0.86"
8+
mdbook = "0.4.40"
89
regex = "1.10.5"
10+
semver = "1.0.23"
11+
serde = "1.0.204"
12+
serde_json = "1.0.120"
913
structopt = "0.3.26"
14+
walkdir = "2.5.0"

mdbook-goals/src/goal.rs

+269
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
use std::collections::BTreeSet;
2+
use std::fmt::Write;
3+
use std::path::Path;
4+
5+
use regex::Regex;
6+
7+
use crate::{
8+
markwaydown::{self, Section, Table},
9+
util::{self, ARROW},
10+
};
11+
12+
/// Process the input file `input` and return a list of team asks.
13+
/// Ignores goals that are marked as "not accepted".
14+
///
15+
/// # Parameters
16+
///
17+
/// * `input`, path on disk
18+
/// * `link_path`, path to insert into any links in the output
19+
pub fn team_asks_in_input<'i>(
20+
input: &'i Path,
21+
link_path: &'i Path,
22+
) -> anyhow::Result<Vec<TeamAsk<'i>>> {
23+
let sections = markwaydown::parse(input)?;
24+
25+
let Some(metadata) = extract_metadata(&sections)? else {
26+
return Ok(vec![]);
27+
};
28+
29+
match metadata.status {
30+
Status::Flagship | Status::Proposed => extract_team_asks(link_path, &metadata, &sections),
31+
Status::NotAccepted => Ok(vec![]),
32+
}
33+
}
34+
35+
pub fn format_team_asks(asks_of_any_team: &[TeamAsk]) -> anyhow::Result<String> {
36+
let mut output = String::new();
37+
38+
let all_teams: BTreeSet<&String> = asks_of_any_team.iter().flat_map(|a| &a.teams).collect();
39+
40+
for team in all_teams {
41+
let asks_of_this_team: Vec<&TeamAsk> = asks_of_any_team
42+
.iter()
43+
.filter(|a| a.teams.contains(team))
44+
.collect();
45+
46+
if team != "LC" {
47+
write!(output, "\n### {} team\n", team)?;
48+
} else {
49+
write!(output, "\n### Leadership Council\n")?;
50+
}
51+
52+
let subgoals: BTreeSet<&String> = asks_of_this_team.iter().map(|a| &a.subgoal).collect();
53+
54+
let mut table = vec![vec![
55+
"Goal".to_string(),
56+
"Owner".to_string(),
57+
"Notes".to_string(),
58+
]];
59+
60+
for subgoal in subgoals {
61+
table.push(vec![
62+
format!("*{}*", subgoal),
63+
"".to_string(),
64+
"".to_string(),
65+
]);
66+
67+
for ask in asks_of_this_team.iter().filter(|a| a.subgoal == *subgoal) {
68+
table.push(vec![
69+
format!(
70+
"{} [{}]({}#ownership-and-team-asks)",
71+
ARROW,
72+
ask.heading,
73+
ask.link_path.display()
74+
),
75+
ask.owners.to_string(),
76+
ask.notes.to_string(),
77+
]);
78+
}
79+
}
80+
81+
write!(output, "{}", util::format_table(&table))?;
82+
}
83+
84+
Ok(output)
85+
}
86+
87+
#[derive(Debug)]
88+
struct Metadata<'a> {
89+
#[allow(unused)]
90+
title: &'a str,
91+
short_title: &'a str,
92+
owners: &'a str,
93+
status: Status,
94+
}
95+
96+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
97+
pub enum Status {
98+
Flagship,
99+
Proposed,
100+
NotAccepted,
101+
}
102+
103+
fn extract_metadata(sections: &[Section]) -> anyhow::Result<Option<Metadata<'_>>> {
104+
let Some(first_section) = sections.first() else {
105+
anyhow::bail!("no markdown sections found in input")
106+
};
107+
108+
if first_section.title.is_empty() {
109+
anyhow::bail!("first section has no title");
110+
}
111+
112+
let title = &first_section.title;
113+
114+
let Some(first_table) = first_section.tables.first() else {
115+
return Ok(None);
116+
};
117+
118+
expect_headers(first_table, &["Metadata", ""])?;
119+
120+
let short_title_row = first_table.rows.iter().find(|row| row[0] == "Short title");
121+
122+
let Some(owners_row) = first_table
123+
.rows
124+
.iter()
125+
.find(|row| row[0] == "Owner" || row[0] == "Owner(s)" || row[0] == "Owners")
126+
else {
127+
anyhow::bail!("metadata table has no `Owner(s)` row")
128+
};
129+
130+
let Some(status_row) = first_table.rows.iter().find(|row| row[0] == "Status") else {
131+
anyhow::bail!("metadata table has no `Status` row")
132+
};
133+
134+
let status_values = &[
135+
("Flagship", Status::Flagship),
136+
("Proposed", Status::Proposed),
137+
("Not accepted", Status::NotAccepted),
138+
];
139+
140+
let Some(status) = status_values
141+
.iter()
142+
.find(|(s, _)| status_row[1] == *s)
143+
.map(|s| s.1)
144+
else {
145+
anyhow::bail!(
146+
"unrecognized status `{}`, expected one of: {}",
147+
status_row[1],
148+
status_values
149+
.iter()
150+
.map(|s| s.0)
151+
.collect::<Vec<_>>()
152+
.join(", ")
153+
)
154+
};
155+
156+
Ok(Some(Metadata {
157+
title,
158+
short_title: if let Some(row) = short_title_row {
159+
&row[1]
160+
} else {
161+
title
162+
},
163+
owners: &owners_row[1],
164+
status,
165+
}))
166+
}
167+
168+
#[derive(Debug)]
169+
pub struct TeamAsk<'i> {
170+
link_path: &'i Path,
171+
subgoal: String,
172+
heading: String,
173+
teams: Vec<String>,
174+
owners: String,
175+
notes: String,
176+
}
177+
178+
fn extract_team_asks<'i>(
179+
link_path: &'i Path,
180+
metadata: &Metadata<'_>,
181+
sections: &[Section],
182+
) -> anyhow::Result<Vec<TeamAsk<'i>>> {
183+
let Some(ownership_section) = sections
184+
.iter()
185+
.find(|section| section.title == "Ownership and team asks")
186+
else {
187+
anyhow::bail!("no `Ownership and team asks` section found")
188+
};
189+
190+
let Some(table) = ownership_section.tables.first() else {
191+
anyhow::bail!(
192+
"on line {}, no table found in `Ownership and team asks` section",
193+
ownership_section.line_num
194+
)
195+
};
196+
197+
expect_headers(table, &["Subgoal", "Owner(s) or team(s)", "Notes"])?;
198+
199+
let mut heading = "";
200+
let mut owners: &str = metadata.owners;
201+
202+
let mut tasks = vec![];
203+
for row in &table.rows {
204+
let subgoal;
205+
if row[0].starts_with(ARROW) {
206+
// e.g., "↳ stabilization" is a subtask of the metagoal
207+
subgoal = row[0][ARROW.len()..].trim().to_string();
208+
} else {
209+
// remember the last heading
210+
heading = &row[0];
211+
subgoal = heading.to_string();
212+
};
213+
214+
if !row[1].contains("![Team]") {
215+
if !row[1].is_empty() {
216+
owners = &row[1];
217+
}
218+
219+
continue;
220+
}
221+
222+
let teams = extract_teams(&row[1]);
223+
224+
tasks.push(TeamAsk {
225+
link_path,
226+
heading: if subgoal == heading {
227+
metadata.short_title.to_string()
228+
} else {
229+
heading.to_string()
230+
},
231+
subgoal,
232+
teams,
233+
owners: if owners == "Owner" {
234+
metadata.owners.to_string()
235+
} else {
236+
owners.to_string()
237+
},
238+
notes: row[2].to_string(),
239+
});
240+
}
241+
242+
Ok(tasks)
243+
}
244+
245+
fn expect_headers(table: &Table, expected: &[&str]) -> anyhow::Result<()> {
246+
if table.header != expected {
247+
anyhow::bail!(
248+
"on line {}, unexpected table header, expected `{:?}`, found `{:?}`",
249+
table.line_num,
250+
expected,
251+
table.header
252+
);
253+
}
254+
255+
Ok(())
256+
}
257+
258+
fn extract_teams(s: &str) -> Vec<String> {
259+
extract_identifiers(s)
260+
.into_iter()
261+
.filter(|&s| s != "Team")
262+
.map(|s| s.to_string())
263+
.collect()
264+
}
265+
266+
fn extract_identifiers(s: &str) -> Vec<&str> {
267+
let regex = Regex::new("[-.A-Za-z]+").unwrap();
268+
regex.find_iter(s).map(|m| m.as_str()).collect()
269+
}

0 commit comments

Comments
 (0)