mdbook_incremental_preview/
lib.rs

1use std::{
2    borrow::Cow,
3    cell::RefCell,
4    collections::{HashMap, HashSet},
5    ffi::OsStr,
6    io, iter, mem,
7    net::SocketAddr,
8    path::{Path, PathBuf},
9    sync::{Arc, Mutex, RwLock},
10    time::Duration,
11};
12
13use anyhow::{bail, Context};
14use drop_this::*;
15use futures_util::sink::SinkExt;
16use handlebars::Handlebars;
17use ignore::gitignore::Gitignore;
18use mdbook::{
19    book::{preprocessor_should_run, Book, Chapter},
20    config::{Code, HtmlConfig, Playground, RustEdition},
21    errors::*,
22    preprocess::{Preprocessor, PreprocessorContext},
23    renderer::{
24        html_handlebars::hbs_renderer::{make_data, RenderItemContext},
25        HtmlHandlebars, RenderContext,
26    },
27    theme::{self, playground_editor, Theme},
28    utils, BookItem, Config, MDBook, Renderer, MDBOOK_VERSION,
29};
30use notify::{RecommendedWatcher, RecursiveMode::*};
31use notify_debouncer_mini::{DebounceEventHandler, DebouncedEvent, Debouncer};
32use serde_json::json;
33use tempfile::{tempdir, TempDir};
34use tokio::{
35    fs::{self, File},
36    io::AsyncReadExt,
37    select, spawn,
38    sync::{mpsc, oneshot, watch},
39    task::{block_in_place, spawn_blocking, yield_now, JoinHandle},
40    time::timeout,
41};
42use tokio_gen_server::prelude::*;
43use tokio_two_join_set::TwoJoinSet;
44use tracing::*;
45use warp::{
46    filters::{
47        path::{FullPath, Tail},
48        ws::{WebSocket, Ws},
49        BoxedFilter,
50    },
51    reply::{with_header, WithHeader},
52    ws::Message,
53    Filter,
54};
55
56pub mod build_book;
57pub mod git_ignore;
58pub mod patch_registry;
59pub mod previewing;
60pub mod rebuilding;
61pub mod rendering;
62pub mod watch_files;
63pub mod web_server;
64
65use build_book::*;
66use git_ignore::*;
67use patch_registry::*;
68use previewing::*;
69use rebuilding::*;
70use rendering::*;
71use watch_files::*;
72use web_server::*;
73
74// NOTE: Below is adapted from
75// <https://github.com/rust-lang/mdBook/blob/3bdcc0a5a6f3c85dd751350774261dbc357b02bd/src/cmd/serve.rs>.
76
77/// The HTTP endpoint for the WebSocket used to trigger reloads when a file changes.
78const LIVE_PATCH_WEBSOCKET_PATH: &str = "__mdbook_incremental_preview_live_patch";
79
80// Serve the book at absolute path `book_root` at the given `socket_address`,
81// and patch it live continuously.
82pub async fn preview_continuously(
83    book_root: PathBuf,
84    socket_address: SocketAddr,
85    open_browser: bool,
86) -> Result<()> {
87    let previewer = Previewer::try_new()?;
88    let (handle, actor_ref) = previewer.spawn();
89    actor_ref.cast(PreviewInfo::BookRoot(book_root)).await?;
90    let msg = PreviewInfo::OpenPreview {
91        socket_address: Some(socket_address),
92        open_browser_at: open_browser.then_some("".into()),
93    };
94    actor_ref.cast(msg).await?;
95    try_join_actor_handle(handle).await?;
96    Ok(())
97}
98
99/// Runs the provided blocking function on the current thread without
100/// blocking the executor,
101/// then yield the control back to the executor.
102pub async fn block_n_yield<F, R>(f: F) -> R
103where
104    F: FnOnce() -> R,
105{
106    let result = block_in_place(f);
107    yield_now().await;
108    result
109}
110
111async fn shut_down_actor_n_log_err<A: Actor>(
112    handle: JoinHandle<ActorRunResult<A>>,
113    actor_ref: ActorRef<A>,
114    err_msg: &'static str,
115) {
116    actor_ref.cancel();
117    if let Err(err) = try_join_actor_handle(handle).await {
118        error!(?err, err_msg);
119    }
120}
121
122async fn try_join_actor_handle<A: Actor>(handle: JoinHandle<ActorRunResult<A>>) -> Result<()> {
123    handle.await?.exit_result
124}
125
126fn open<P: AsRef<OsStr>>(path: P) {
127    match opener::open(path) {
128        Err(err) => {
129            error!(?err, "opening web browser.")
130        }
131        Ok(_) => {
132            info!("Opened web browser.")
133        }
134    }
135}