···11//! Error reporting for preprocessors.
2233-use std::fmt::{self, Debug, Display, Write};
33+use std::{
44+ borrow::Borrow,
55+ fmt::{self, Debug, Display, Write},
66+};
4758use log::{Level, LevelFilter};
69use miette::{
···12151316/// Trait for Markdown diagnostics. This will eventually be printed to stderr.
1417///
1515-/// Each [`Problem`] represents a specific message, such as a warning, associated with
1818+/// Each [`IssueItem`] represents a specific message, such as a warning, associated with
1619/// an [`Issue`] (the type and severity of the issue) and a location in the Markdown
1720/// source, represented by [`LabeledSpan`].
1818-pub trait Problem: Send + Sync {
2121+pub trait IssueItem: Send + Sync {
1922 type Kind: Issue;
2023 fn issue(&self) -> Self::Kind;
2124 fn label(&self) -> LabeledSpan;
···4043impl<K, P> Diagnostics<'_, K, P>
4144where
4245 K: Title,
4343- P: Problem,
4646+ P: IssueItem,
4447{
4548 /// Render a report of the diagnostics using [miette]'s graphical reporting
4649 pub fn to_report(&self, colored: bool) -> String {
···86898790impl<'a, K, P> Diagnostics<'a, K, P>
8891where
8989- P: Problem,
9292+ P: IssueItem,
9093{
9194 pub fn new(text: &'a str, name: K, issues: Vec<P>) -> Self {
9295 Self { text, name, issues }
···105108 }
106109 }
107110111111+ pub fn name(&self) -> &K {
112112+ &self.name
113113+ }
114114+108115 fn status(&self) -> P::Kind {
109116 self.issues
110117 .iter()
···117124impl<K, P> Diagnostic for Diagnostics<'_, K, P>
118125where
119126 K: Title,
120120- P: Problem,
127127+ P: IssueItem,
121128{
122129 fn severity(&self) -> Option<Severity> {
123130 match self.status().level() {
···173180 }
174181}
175182176176-impl<K, P: Problem> Debug for Diagnostics<'_, K, P> {
183183+impl<K, P: IssueItem> Debug for Diagnostics<'_, K, P> {
177184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178185 fmt::Debug::fmt(&self.status(), f)
179186 }
180187}
181188182182-impl<K, P: Problem> Display for Diagnostics<'_, K, P> {
189189+impl<K, P: IssueItem> Display for Diagnostics<'_, K, P> {
183190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184191 fmt::Display::fmt(&self.status(), f)
185192 }
186193}
187194188188-impl<K, P: Problem> std::error::Error for Diagnostics<'_, K, P> {}
195195+impl<K, P: IssueItem> std::error::Error for Diagnostics<'_, K, P> {}
189196190197/// Builder for printing diagnostics over multiple files.
191198pub struct ReportBuilder<'a, K, P, F> {
···238245 self
239246 }
240247248248+ pub fn named<Q>(mut self, mut f: impl FnMut(&K) -> bool) -> Self
249249+ where
250250+ K: Borrow<Q>,
251251+ Q: Eq + ?Sized,
252252+ {
253253+ self.items.retain(|d| f(&d.name));
254254+ self
255255+ }
256256+241257 pub fn logging(mut self, logging: bool) -> Self {
242258 self.logging = logging;
243259 self
···246262247263impl<'a, K, P, F> ReportBuilder<'a, K, P, F>
248264where
249249- P: Problem,
265265+ P: IssueItem,
250266{
251267 pub fn build(self) -> Reporter<'a, P>
252268 where
···293309294310impl<P> Reporter<'_, P>
295311where
296296- P: Problem,
312312+ P: IssueItem,
297313{
298314 pub fn to_status(&self) -> P::Kind {
299315 self.items
···408424409425impl StyleCompat for Style {
410426 fn toggle(self, enabled: bool) -> Self {
411411- if enabled {
412412- self
413413- } else {
414414- Style::new()
415415- }
427427+ if enabled { self } else { Style::new() }
416428 }
417429}
418430
-150
crates/mdbookkit/src/env.rs
···11-use std::io::Read;
22-33-use anyhow::{anyhow, Result};
44-use serde::Deserialize;
55-use tap::Pipe;
66-77-pub fn is_ci() -> Option<String> {
88- let ci = std::env::var("CI").unwrap_or("".into());
99- if matches!(ci.as_str(), "" | "0" | "false") {
1010- None
1111- } else {
1212- Some(ci)
1313- }
1414-}
1515-1616-/// Flag indicating how the program should proceed when there are warnings.
1717-///
1818-/// Used in preprocessor options.
1919-///
2020-/// Doc comments for variants in this enum will show up in autogenerated docs.
2121-#[cfg_attr(feature = "common-cli", derive(clap::ValueEnum))]
2222-#[derive(Deserialize, Debug, Default, Clone, Copy)]
2323-#[serde(rename_all = "lowercase")]
2424-pub enum ErrorHandling {
2525- /// Fail if the environment variable `CI` is set to a value other than `0` or `false`.
2626- /// Environments like GitHub Actions configure this automatically.
2727- #[default]
2828- #[serde(rename = "ci")]
2929- #[cfg_attr(feature = "common-cli", clap(name = "ci"))]
3030- Env,
3131-3232- /// Fail as long as there are warnings, even in local use.
3333- Always,
3434-}
3535-3636-impl ErrorHandling {
3737- pub fn check(&self, level: log::Level) -> Result<()> {
3838- match level {
3939- log::Level::Error => Err(anyhow!("preprocessor has errors")),
4040- log::Level::Warn => match self {
4141- Self::Always => {
4242- anyhow!("treating warnings as errors because fail-on-unresolved is \"always\"")
4343- .context("preprocessor has errors")
4444- .pipe(Err)
4545- }
4646- Self::Env => {
4747- let Some(ci) = Self::warning_as_error() else {
4848- return Ok(());
4949- };
5050- anyhow!("treating warnings as errors because fail-on-unresolved is \"ci\" and CI={ci}")
5151- .context("preprocessor has errors")
5252- .pipe(Err)
5353- }
5454- },
5555- _ => Ok(()),
5656- }
5757- }
5858-5959- pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> {
6060- match result {
6161- Ok(Err(error)) if Self::warning_as_error().is_some() => Err(error),
6262- result => result,
6363- }
6464- }
6565-6666- fn warning_as_error() -> Option<String> {
6767- is_ci()
6868- }
6969-}
7070-7171-pub fn string_from_stdin() -> Result<String> {
7272- Vec::new()
7373- .pipe(|mut buf| std::io::stdin().read_to_end(&mut buf).and(Ok(buf)))?
7474- .pipe(|buf| Ok(String::from_utf8(buf)?))
7575-}
7676-7777-#[cfg(feature = "common-cli")]
7878-pub use book::*;
7979-#[cfg(feature = "common-cli")]
8080-mod book {
8181- use std::{
8282- io::{Read, Write},
8383- path::PathBuf,
8484- };
8585-8686- use anyhow::{Context, Result};
8787- use mdbook::{
8888- book::{Book, Chapter},
8989- preprocess::PreprocessorContext,
9090- BookItem,
9191- };
9292- use serde::de::DeserializeOwned;
9393- use tap::Pipe;
9494-9595- pub fn book_from_stdin() -> Result<(PreprocessorContext, Book)> {
9696- Ok(Vec::new()
9797- .pipe(|mut buf| std::io::stdin().read_to_end(&mut buf).and(Ok(buf)))?
9898- .pipe(String::from_utf8)?
9999- .pipe_as_ref(serde_json::from_str)?)
100100- }
101101-102102- pub fn config_from_book<T>(config: &mdbook::Config, name: &str) -> Result<T>
103103- where
104104- T: DeserializeOwned + Default,
105105- {
106106- if let Some(config) = config.get_preprocessor(name) {
107107- T::deserialize(toml::Value::Table(config.clone()))?
108108- } else {
109109- Default::default()
110110- }
111111- .pipe(Ok)
112112- }
113113-114114- pub fn smart_punctuation(config: &mdbook::Config) -> bool {
115115- config
116116- .get_deserialized_opt::<bool, _>("output.html.smart-punctuation")
117117- .unwrap_or_default()
118118- .unwrap_or(true)
119119- }
120120-121121- pub fn iter_chapters(book: &Book) -> impl Iterator<Item = (&PathBuf, &Chapter)> {
122122- book.iter().filter_map(|item| {
123123- let BookItem::Chapter(ch) = item else {
124124- return None;
125125- };
126126- let Some(path) = &ch.source_path else {
127127- return None;
128128- };
129129- Some((path, ch))
130130- })
131131- }
132132-133133- pub fn for_each_chapter_mut<F>(book: &mut Book, mut func: F)
134134- where
135135- F: FnMut(PathBuf, &mut Chapter),
136136- {
137137- book.for_each_mut(|item| {
138138- let BookItem::Chapter(ch) = item else { return };
139139- let Some(path) = &ch.source_path else { return };
140140- func(path.clone(), ch)
141141- });
142142- }
143143-144144- pub fn book_into_stdout(book: &Book) -> Result<()> {
145145- serde_json::to_string(&book)
146146- .context("failed to serialize book")
147147- .and_then(|output| Ok(std::io::stdout().write_all(output.as_bytes())?))
148148- .context("failed to write book to stdout")
149149- }
150150-}
+66
crates/mdbookkit/src/error.rs
···11+use anyhow::{Result, anyhow};
22+use serde::Deserialize;
33+use tap::Pipe;
44+55+pub fn is_ci() -> Option<String> {
66+ let ci = std::env::var("CI").unwrap_or("".into());
77+ if matches!(ci.as_str(), "" | "0" | "false") {
88+ None
99+ } else {
1010+ Some(ci)
1111+ }
1212+}
1313+1414+/// Flag indicating how the program should proceed when there are warnings.
1515+///
1616+/// Used in preprocessor options.
1717+///
1818+/// Doc comments for variants in this enum will show up in autogenerated docs.
1919+#[derive(clap::ValueEnum, Deserialize, Debug, Default, Clone, Copy)]
2020+#[serde(rename_all = "lowercase")]
2121+pub enum OnWarning {
2222+ /// Fail if the environment variable `CI` is set to a value other than `0` or `false`.
2323+ /// Environments like GitHub Actions configure this automatically.
2424+ #[default]
2525+ #[serde(rename = "ci")]
2626+ #[clap(name = "ci")]
2727+ FailInCi,
2828+2929+ /// Fail as long as there are warnings, even in local use.
3030+ AlwaysFail,
3131+}
3232+3333+impl OnWarning {
3434+ pub fn check(&self, level: log::Level) -> Result<()> {
3535+ match level {
3636+ log::Level::Error => Err(anyhow!("preprocessor has errors")),
3737+ log::Level::Warn => match self {
3838+ Self::AlwaysFail => {
3939+ anyhow!("treating warnings as errors because fail-on-unresolved is \"always\"")
4040+ .context("preprocessor has errors")
4141+ .pipe(Err)
4242+ }
4343+ Self::FailInCi => {
4444+ let Some(ci) = Self::warning_as_error() else {
4545+ return Ok(());
4646+ };
4747+ anyhow!("treating warnings as errors because fail-on-unresolved is \"ci\" and CI={ci}")
4848+ .context("preprocessor has errors")
4949+ .pipe(Err)
5050+ }
5151+ },
5252+ _ => Ok(()),
5353+ }
5454+ }
5555+5656+ pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> {
5757+ match result {
5858+ Ok(Err(error)) if Self::warning_as_error().is_some() => Err(error),
5959+ result => result,
6060+ }
6161+ }
6262+6363+ fn warning_as_error() -> Option<String> {
6464+ is_ci()
6565+ }
6666+}
+4-4
crates/mdbookkit/src/lib.rs
···88//!
99//! [preprocessors]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html
10101111-#[cfg(feature = "common-logger")]
1111+pub mod book;
1212pub mod diagnostics;
1313-pub mod env;
1313+pub mod error;
1414pub mod logging;
1515pub mod markdown;
1616-1717-pub mod bin;
1616+#[cfg(feature = "_testing")]
1717+pub mod testing;
+328-35
crates/mdbookkit/src/logging.rs
···11//! Progress reporting and logging for preprocessors.
2233-use std::{fmt, sync::mpsc};
33+use std::{
44+ collections::BTreeSet,
55+ fmt, io,
66+ sync::{OnceLock, mpsc},
77+ thread,
88+ time::{Duration, Instant},
99+};
1010+1111+use anyhow::{Context, Result};
1212+use console::{StyledObject, Term, colors_enabled_stderr, set_colors_enabled};
1313+use env_logger::Logger;
1414+use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle};
1515+use log::{Level, LevelFilter, Log};
1616+use tap::{Pipe, Tap};
417518pub fn spinner() -> SpinnerHandle {
619 SpinnerHandle
···1932 let prefix = prefix.into();
2033 let msg = Message::Create { prefix, total };
21342222- #[cfg(feature = "common-logger")]
2323- if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() {
3535+ if let Some(Spinner { tx, .. }) = SPINNER.get() {
2436 tx.send(msg).ok();
2537 } else {
2638 spinner_log!(info!("{msg}"));
2739 }
28402929- #[cfg(not(feature = "common-logger"))]
3030- spinner_log!(info!("{msg}"));
3131-3241 self
3342 }
3443···3746 let update = update.to_string();
3847 let msg = Message::Update { key, update };
39484040- #[cfg(feature = "common-logger")]
4141- if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() {
4949+ if let Some(Spinner { tx, .. }) = SPINNER.get() {
4250 tx.send(msg).ok();
4351 } else {
4452 spinner_log!(info!("{msg}"));
4553 }
4646-4747- #[cfg(not(feature = "common-logger"))]
4848- spinner_log!(info!("{msg}"));
49545055 self
5156 }
···6065 };
6166 let done = Some(Message::Done { key, task });
62676363- #[cfg(feature = "common-logger")]
6464- if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() {
6868+ if let Some(Spinner { tx, .. }) = SPINNER.get() {
6569 tx.send(open).ok();
6670 let spin = Some(tx.clone());
6771 return TaskHandle { spin, done };
···7781 let update = update.to_string();
7882 let msg = Message::Finish { key, update };
79838080- #[cfg(feature = "common-logger")]
8181- if let Some(terminal::Spinner { tx, .. }) = terminal::SPINNER.get() {
8484+ if let Some(Spinner { tx, .. }) = SPINNER.get() {
8285 tx.send(msg).ok();
8386 } else {
8487 spinner_log!(info!("{msg}"));
8588 }
8686-8787- #[cfg(not(feature = "common-logger"))]
8888- spinner_log!(info!("{msg}"));
8989 }
9090}
9191···107107}
108108109109#[derive(Debug)]
110110-#[cfg_attr(not(feature = "common-logger"), allow(unused))]
111110enum Message {
112111 Create { prefix: String, total: Option<u64> },
113112 Update { key: String, update: String },
···149148 }
150149}
151150152152-#[cfg(feature = "common-logger")]
153151pub fn styled<D>(val: D) -> console::StyledObject<D> {
154154- if let Some(terminal::Spinner { term, .. }) = terminal::SPINNER.get() {
152152+ if let Some(Spinner { term, .. }) = SPINNER.get() {
155153 term.style()
156154 } else {
157155 console::Style::new().for_stderr()
···162160#[macro_export]
163161macro_rules! styled {
164162 ( ( $($display:tt)+ ) . $($style:tt)+ ) => {{
165165- #[cfg(feature = "common-logger")]
166166- {
167167- $crate::logging::styled( $($display)* ) . $($style)*
168168- }
169169- #[cfg(not(feature = "common-logger"))]
170170- {
171171- $($display)*
172172- }
163163+ $crate::logging::styled( $($display)* ) . $($style)*
173164 }};
174165}
175166176176-#[cfg(feature = "common-logger")]
177167pub fn is_logging() -> bool {
178178- terminal::SPINNER.get().is_none()
168168+ SPINNER.get().is_none()
179169}
180170181171#[macro_export]
···208198 };
209199}
210200211211-#[cfg(feature = "common-logger")]
212212-mod terminal;
201201+/// Either a [`console::Term`] or an [`env_logger::Logger`].
202202+///
203203+/// This is automatically detected upon installation as the global logger. The logic is:
204204+///
205205+/// - If the `RUST_LOG` env var is set, this will use [`env_logger`].
206206+/// - If stderr is not "user-attended", as determined by [`console::user_attended_stderr()`],
207207+/// like if stderr is piped to a file, this will use [`env_logger`].
208208+/// - Otherwise, this will use [`console`].
209209+///
210210+/// When this is a [`console::Term`], logs are handled by the global [`indicatif`] spinner.
211211+///
212212+/// When this is an [`env_logger::Logger`], there will not be a spinner, and progress
213213+/// reports are printed as logs instead.
214214+pub enum ConsoleLogger {
215215+ Console(Term),
216216+ Logger(Logger),
217217+}
218218+219219+impl ConsoleLogger {
220220+ /// Install a [`ConsoleLogger`] as the global [`log`] logger.
221221+ pub fn install(name: &str) {
222222+ Self::try_install(name).expect("logger should not have been set");
223223+ }
224224+225225+ pub fn try_install(name: &str) -> Result<()> {
226226+ log::set_boxed_logger(Box::new(Self::new(name)))?;
227227+ log::set_max_level(LevelFilter::max());
228228+ Ok(())
229229+ }
230230+231231+ fn new(name: &str) -> Self {
232232+ match maybe_logging() {
233233+ Some(LevelFilter::Off) => SPINNER
234234+ .get_or_init(|| spawn_spinner(name))
235235+ .term
236236+ .clone()
237237+ .pipe(Self::Console),
238238+ level => env_logger::Builder::new()
239239+ .format(log_format)
240240+ .parse_default_env()
241241+ .tap_mut(|builder| {
242242+ if let Some(level) = level {
243243+ builder.filter_level(level);
244244+ }
245245+ })
246246+ .build()
247247+ .pipe(Self::Logger),
248248+ }
249249+ }
250250+}
251251+252252+fn maybe_logging() -> Option<LevelFilter> {
253253+ if std::env::var("RUST_LOG")
254254+ .map(|v| !v.is_empty())
255255+ .unwrap_or(false)
256256+ {
257257+ // RUST_LOG to be parsed by env_logger
258258+ None
259259+ } else if !console::user_attended_stderr() {
260260+ // RUST_LOG not set but stderr isn't a terminal
261261+ // log info and above
262262+ Some(LevelFilter::Info)
263263+ } else {
264264+ // use spinner instead
265265+ Some(LevelFilter::Off)
266266+ }
267267+}
268268+269269+impl Log for ConsoleLogger {
270270+ fn enabled(&self, metadata: &log::Metadata) -> bool {
271271+ match self {
272272+ ConsoleLogger::Logger(logger) => logger.enabled(metadata),
273273+ ConsoleLogger::Console(_) => {
274274+ if metadata.target().starts_with(env!("CARGO_CRATE_NAME")) {
275275+ metadata.level() <= Level::Info
276276+ } else {
277277+ metadata.level() <= Level::Warn
278278+ }
279279+ }
280280+ }
281281+ }
282282+283283+ fn log(&self, record: &log::Record) {
284284+ match self {
285285+ ConsoleLogger::Logger(logger) => logger.log(record),
286286+ ConsoleLogger::Console(term) => {
287287+ if !self.enabled(record.metadata()) {
288288+ return;
289289+ }
290290+ let Ok(message) = Vec::<u8>::new()
291291+ .pipe(|mut buf| log_format(&mut buf, record).and(Ok(buf)))
292292+ .context("failed to emit log message")
293293+ .and_then(|buf| Ok(String::from_utf8(buf)?))
294294+ else {
295295+ return;
296296+ };
297297+ let message = styled_log(message.trim_end(), record);
298298+ term.write_line(&message.to_string()).ok();
299299+ }
300300+ }
301301+ }
302302+303303+ fn flush(&self) {
304304+ match self {
305305+ ConsoleLogger::Console(term) => {
306306+ term.flush().ok();
307307+ }
308308+ ConsoleLogger::Logger(logger) => {
309309+ logger.flush();
310310+ }
311311+ }
312312+ }
313313+}
314314+315315+pub static SPINNER: OnceLock<Spinner> = OnceLock::new();
316316+317317+pub struct Spinner {
318318+ tx: mpsc::Sender<Message>,
319319+ term: Term,
320320+}
321321+322322+fn spawn_spinner(name: &str) -> Spinner {
323323+ // https://github.com/console-rs/indicatif/issues/698
324324+ set_colors_enabled(colors_enabled_stderr());
325325+326326+ let (tx, rx) = mpsc::channel();
327327+328328+ let term = Term::stderr();
329329+330330+ let target = term.clone();
331331+ let template = format!("{{spinner:.cyan}} [{name}] {{prefix}} ... {{msg}}",);
332332+333333+ // this thread is detached. this is okay in usage because SPINNER.get_or_init
334334+ // guarantees this function is called at most once
335335+336336+ thread::spawn(move || {
337337+ struct Bar {
338338+ prefix: String,
339339+ bar: ProgressBar,
340340+ }
341341+342342+ let mut current: Option<Bar> = None;
343343+344344+ let mut tasks = BTreeSet::<String>::new();
345345+ let mut task_idx = 0;
346346+ let mut interval = Instant::now();
347347+348348+ loop {
349349+ match rx.recv_timeout(Duration::from_millis(100)) {
350350+ Err(mpsc::RecvTimeoutError::Timeout) => {}
351351+352352+ Err(mpsc::RecvTimeoutError::Disconnected) => break,
353353+354354+ Ok(Message::Create { prefix, total }) => {
355355+ if let Some(bar) = current {
356356+ bar.bar.abandon()
357357+ }
358358+359359+ let style = ProgressStyle::with_template(&template)
360360+ .unwrap()
361361+ .tick_chars("⠇⠋⠙⠸⠴⠦⠿");
362362+363363+ let bar = ProgressDrawTarget::term(target.clone(), 20)
364364+ .pipe(|target| ProgressBar::with_draw_target(total, target))
365365+ .with_prefix(prefix.clone())
366366+ .with_style(style);
367367+368368+ bar.enable_steady_tick(Duration::from_millis(100));
369369+370370+ current = Some(Bar { prefix, bar });
371371+ }
372372+373373+ Ok(Message::Update { key, update }) => {
374374+ let Some(Bar {
375375+ ref bar,
376376+ ref prefix,
377377+ }) = current
378378+ else {
379379+ continue;
380380+ };
381381+382382+ if &key != prefix {
383383+ continue;
384384+ }
385385+386386+ bar.set_message(update);
387387+ bar.tick();
388388+ }
389389+390390+ Ok(Message::Finish { key, update }) => {
391391+ let Some(Bar {
392392+ ref bar,
393393+ ref prefix,
394394+ }) = current
395395+ else {
396396+ continue;
397397+ };
398398+399399+ if &key != prefix {
400400+ continue;
401401+ }
402402+403403+ bar.finish_with_message(update);
404404+ current = None;
405405+ }
406406+407407+ Ok(Message::Task { key, task }) => {
408408+ let Some(Bar {
409409+ ref bar,
410410+ ref prefix,
411411+ }) = current
412412+ else {
413413+ continue;
414414+ };
415415+416416+ if &key != prefix {
417417+ continue;
418418+ }
419419+420420+ if let Some(length) = bar.length() {
421421+ let counter = styled(format!("({}/{length})", bar.position())).dim();
422422+ bar.set_prefix(format!("{prefix} {counter}"))
423423+ }
424424+425425+ bar.set_message(styled(&task).magenta().to_string());
426426+ bar.tick();
427427+428428+ tasks.insert(task);
429429+ interval = Instant::now();
430430+ }
431431+432432+ Ok(Message::Done { key, task }) => {
433433+ let Some(Bar {
434434+ ref bar,
435435+ ref prefix,
436436+ }) = current
437437+ else {
438438+ continue;
439439+ };
440440+441441+ if &key != prefix {
442442+ continue;
443443+ }
444444+445445+ bar.inc(1);
446446+447447+ if let Some(length) = bar.length() {
448448+ let counter = styled(format!("({}/{length})", bar.position())).dim();
449449+ bar.set_prefix(format!("{prefix} {counter}"))
450450+ }
213451214214-#[cfg(feature = "common-logger")]
215215-pub use self::terminal::ConsoleLogger;
452452+ bar.set_message(styled(&task).green().to_string());
453453+ bar.tick();
454454+455455+ tasks.insert(task);
456456+ interval = Instant::now();
457457+ }
458458+ }
459459+460460+ if let Some(Bar {
461461+ ref prefix,
462462+ ref bar,
463463+ }) = current
464464+ {
465465+ let now = Instant::now();
466466+467467+ if now - interval > Duration::from_secs(10) {
468468+ interval = now;
469469+ if task_idx >= tasks.len() {
470470+ task_idx = 0
471471+ }
472472+ if let Some(task) = tasks.iter().nth(task_idx) {
473473+ spinner_log!(warn!(
474474+ "task {prefix} - {task} has been running for more than {}",
475475+ HumanDuration(bar.elapsed())
476476+ ));
477477+ bar.set_message(styled(task).magenta().to_string());
478478+ task_idx += 1;
479479+ }
480480+ }
481481+ }
482482+ }
483483+ });
484484+485485+ Spinner { tx, term }
486486+}
487487+488488+/// <https://github.com/rust-lang/mdBook/blob/07b25cdb643899aeca2307fbab7690fa7eeec36b/src/main.rs#L100-L109>
489489+fn log_format<W: io::Write>(formatter: &mut W, record: &log::Record) -> io::Result<()> {
490490+ let message = format!(
491491+ "{} [{}] ({}): {}",
492492+ chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
493493+ record.level(),
494494+ record.target(),
495495+ record.args()
496496+ );
497497+ let message = styled_log(message, record);
498498+ writeln!(formatter, "{message}",)
499499+}
500500+501501+fn styled_log<D>(message: D, record: &log::Record) -> StyledObject<D> {
502502+ match record.level() {
503503+ Level::Warn => styled(message).yellow(),
504504+ Level::Error => styled(message).red(),
505505+ Level::Info => styled(message),
506506+ _ => styled(message).dim(),
507507+ }
508508+}
-325
crates/mdbookkit/src/logging/terminal.rs
···11-use std::{
22- collections::BTreeSet,
33- io,
44- sync::{mpsc, OnceLock},
55- thread,
66- time::{Duration, Instant},
77-};
88-99-use anyhow::{Context, Result};
1010-use console::{colors_enabled_stderr, set_colors_enabled, StyledObject, Term};
1111-use env_logger::Logger;
1212-use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle};
1313-use log::{Level, LevelFilter, Log};
1414-use tap::{Pipe, Tap};
1515-1616-use super::{styled, Message};
1717-1818-/// Either a [`console::Term`] or an [`env_logger::Logger`].
1919-///
2020-/// This is automatically detected upon installation as the global logger. The logic is:
2121-///
2222-/// - If the `RUST_LOG` env var is set, this will use [`env_logger`].
2323-/// - If stderr is not "user-attended", as determined by [`console::user_attended_stderr()`],
2424-/// like if stderr is piped to a file, this will use [`env_logger`].
2525-/// - Otherwise, this will use [`console`].
2626-///
2727-/// When this is a [`console::Term`], logs are handled by the global [`indicatif`] spinner.
2828-///
2929-/// When this is an [`env_logger::Logger`], there will not be a spinner, and progress
3030-/// reports are printed as logs instead.
3131-pub enum ConsoleLogger {
3232- Console(Term),
3333- Logger(Logger),
3434-}
3535-3636-impl ConsoleLogger {
3737- /// Install a [`ConsoleLogger`] as the global [`log`] logger.
3838- pub fn install(name: &str) {
3939- Self::try_install(name).expect("logger should not have been set");
4040- }
4141-4242- pub fn try_install(name: &str) -> Result<()> {
4343- log::set_boxed_logger(Box::new(Self::new(name)))?;
4444- log::set_max_level(LevelFilter::max());
4545- Ok(())
4646- }
4747-4848- fn new(name: &str) -> Self {
4949- match maybe_logging() {
5050- Some(LevelFilter::Off) => SPINNER
5151- .get_or_init(|| spawn_spinner(name))
5252- .term
5353- .clone()
5454- .pipe(Self::Console),
5555- level => env_logger::Builder::new()
5656- .format(log_format)
5757- .parse_default_env()
5858- .tap_mut(|builder| {
5959- if let Some(level) = level {
6060- builder.filter_level(level);
6161- }
6262- })
6363- .build()
6464- .pipe(Self::Logger),
6565- }
6666- }
6767-}
6868-6969-fn maybe_logging() -> Option<LevelFilter> {
7070- if std::env::var("RUST_LOG")
7171- .map(|v| !v.is_empty())
7272- .unwrap_or(false)
7373- {
7474- // RUST_LOG to be parsed by env_logger
7575- None
7676- } else if !console::user_attended_stderr() {
7777- // RUST_LOG not set but stderr isn't a terminal
7878- // log info and above
7979- Some(LevelFilter::Info)
8080- } else {
8181- // use spinner instead
8282- Some(LevelFilter::Off)
8383- }
8484-}
8585-8686-impl Log for ConsoleLogger {
8787- fn enabled(&self, metadata: &log::Metadata) -> bool {
8888- match self {
8989- ConsoleLogger::Logger(logger) => logger.enabled(metadata),
9090- ConsoleLogger::Console(_) => {
9191- if metadata.target().starts_with(env!("CARGO_CRATE_NAME")) {
9292- metadata.level() <= Level::Info
9393- } else {
9494- metadata.level() <= Level::Warn
9595- }
9696- }
9797- }
9898- }
9999-100100- fn log(&self, record: &log::Record) {
101101- match self {
102102- ConsoleLogger::Logger(logger) => logger.log(record),
103103- ConsoleLogger::Console(term) => {
104104- if !self.enabled(record.metadata()) {
105105- return;
106106- }
107107- let Ok(message) = Vec::<u8>::new()
108108- .pipe(|mut buf| log_format(&mut buf, record).and(Ok(buf)))
109109- .context("failed to emit log message")
110110- .and_then(|buf| Ok(String::from_utf8(buf)?))
111111- else {
112112- return;
113113- };
114114- let message = styled_log(message.trim_end(), record);
115115- term.write_line(&message.to_string()).ok();
116116- }
117117- }
118118- }
119119-120120- fn flush(&self) {
121121- match self {
122122- ConsoleLogger::Console(term) => {
123123- term.flush().ok();
124124- }
125125- ConsoleLogger::Logger(logger) => {
126126- logger.flush();
127127- }
128128- }
129129- }
130130-}
131131-132132-pub static SPINNER: OnceLock<Spinner> = OnceLock::new();
133133-134134-pub struct Spinner {
135135- pub tx: mpsc::Sender<Message>,
136136- pub term: Term,
137137-}
138138-139139-fn spawn_spinner(name: &str) -> Spinner {
140140- // https://github.com/console-rs/indicatif/issues/698
141141- set_colors_enabled(colors_enabled_stderr());
142142-143143- let (tx, rx) = mpsc::channel();
144144-145145- let term = Term::stderr();
146146-147147- let target = term.clone();
148148- let template = format!("{{spinner:.cyan}} [{name}] {{prefix}} ... {{msg}}",);
149149-150150- // this thread is detached. this is okay in usage because SPINNER.get_or_init
151151- // guarantees this function is called at most once
152152-153153- thread::spawn(move || {
154154- struct Bar {
155155- prefix: String,
156156- bar: ProgressBar,
157157- }
158158-159159- let mut current: Option<Bar> = None;
160160-161161- let mut tasks = BTreeSet::<String>::new();
162162- let mut task_idx = 0;
163163- let mut interval = Instant::now();
164164-165165- loop {
166166- match rx.recv_timeout(Duration::from_millis(100)) {
167167- Err(mpsc::RecvTimeoutError::Timeout) => {}
168168-169169- Err(mpsc::RecvTimeoutError::Disconnected) => break,
170170-171171- Ok(Message::Create { prefix, total }) => {
172172- if let Some(bar) = current {
173173- bar.bar.abandon()
174174- }
175175-176176- let style = ProgressStyle::with_template(&template)
177177- .unwrap()
178178- .tick_chars("⠇⠋⠙⠸⠴⠦⠿");
179179-180180- let bar = ProgressDrawTarget::term(target.clone(), 20)
181181- .pipe(|target| ProgressBar::with_draw_target(total, target))
182182- .with_prefix(prefix.clone())
183183- .with_style(style);
184184-185185- bar.enable_steady_tick(Duration::from_millis(100));
186186-187187- current = Some(Bar { prefix, bar });
188188- }
189189-190190- Ok(Message::Update { key, update }) => {
191191- let Some(Bar {
192192- ref bar,
193193- ref prefix,
194194- }) = current
195195- else {
196196- continue;
197197- };
198198-199199- if &key != prefix {
200200- continue;
201201- }
202202-203203- bar.set_message(update);
204204- bar.tick();
205205- }
206206-207207- Ok(Message::Finish { key, update }) => {
208208- let Some(Bar {
209209- ref bar,
210210- ref prefix,
211211- }) = current
212212- else {
213213- continue;
214214- };
215215-216216- if &key != prefix {
217217- continue;
218218- }
219219-220220- bar.finish_with_message(update);
221221- current = None;
222222- }
223223-224224- Ok(Message::Task { key, task }) => {
225225- let Some(Bar {
226226- ref bar,
227227- ref prefix,
228228- }) = current
229229- else {
230230- continue;
231231- };
232232-233233- if &key != prefix {
234234- continue;
235235- }
236236-237237- if let Some(length) = bar.length() {
238238- let counter = styled(format!("({}/{length})", bar.position())).dim();
239239- bar.set_prefix(format!("{prefix} {counter}"))
240240- }
241241-242242- bar.set_message(styled(&task).magenta().to_string());
243243- bar.tick();
244244-245245- tasks.insert(task);
246246- interval = Instant::now();
247247- }
248248-249249- Ok(Message::Done { key, task }) => {
250250- let Some(Bar {
251251- ref bar,
252252- ref prefix,
253253- }) = current
254254- else {
255255- continue;
256256- };
257257-258258- if &key != prefix {
259259- continue;
260260- }
261261-262262- bar.inc(1);
263263-264264- if let Some(length) = bar.length() {
265265- let counter = styled(format!("({}/{length})", bar.position())).dim();
266266- bar.set_prefix(format!("{prefix} {counter}"))
267267- }
268268-269269- bar.set_message(styled(&task).green().to_string());
270270- bar.tick();
271271-272272- tasks.insert(task);
273273- interval = Instant::now();
274274- }
275275- }
276276-277277- if let Some(Bar {
278278- ref prefix,
279279- ref bar,
280280- }) = current
281281- {
282282- let now = Instant::now();
283283-284284- if now - interval > Duration::from_secs(10) {
285285- interval = now;
286286- if task_idx >= tasks.len() {
287287- task_idx = 0
288288- }
289289- if let Some(task) = tasks.iter().nth(task_idx) {
290290- spinner_log!(warn!(
291291- "task {prefix} - {task} has been running for more than {}",
292292- HumanDuration(bar.elapsed())
293293- ));
294294- bar.set_message(styled(task).magenta().to_string());
295295- task_idx += 1;
296296- }
297297- }
298298- }
299299- }
300300- });
301301-302302- Spinner { tx, term }
303303-}
304304-305305-/// <https://github.com/rust-lang/mdBook/blob/07b25cdb643899aeca2307fbab7690fa7eeec36b/src/main.rs#L100-L109>
306306-fn log_format<W: io::Write>(formatter: &mut W, record: &log::Record) -> io::Result<()> {
307307- let message = format!(
308308- "{} [{}] ({}): {}",
309309- chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
310310- record.level(),
311311- record.target(),
312312- record.args()
313313- );
314314- let message = styled_log(message, record);
315315- writeln!(formatter, "{message}",)
316316-}
317317-318318-fn styled_log<D>(message: D, record: &log::Record) -> StyledObject<D> {
319319- match record.level() {
320320- Level::Warn => styled(message).yellow(),
321321- Level::Error => styled(message).red(),
322322- Level::Info => styled(message),
323323- _ => styled(message).dim(),
324324- }
325325-}
+2-2
crates/mdbookkit/src/markdown.rs
···33use std::{borrow::Cow, fmt::Write, ops::Range};
4455use pulldown_cmark::{Event, Options};
66-use pulldown_cmark_to_cmark::{cmark, Error};
66+use pulldown_cmark_to_cmark::{Error, cmark};
77use tap::Pipe;
8899/// _Patch_ a Markdown string, instead of regenerating it entirely, in order to preserve
···102102}
103103104104/// <https://github.com/rust-lang/mdBook/blob/v0.4.47/src/utils/mod.rs#L197-L208>
105105-pub const fn mdbook_markdown() -> Options {
105105+pub const fn mdbook_markdown_options() -> Options {
106106 Options::empty()
107107 .union(Options::ENABLE_TABLES)
108108 .union(Options::ENABLE_FOOTNOTES)