1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-02-10 12:36:25 +02:00

Add mdbook-slide-evaluator (#2258)

I created a first implementation for the mdbook-slide-evaluator I
described in #2234.

One has to run a WebDriver compatible browser (e.g. selenium-chromium)
so the slides can be rendered. The browser can access the file either
via a file:// or http:// uri.

The tool grabs the configurable element from that page and evaluates the
size of this element. Output can be stored in a csv file or at stdout
and looks like this at the moment:
```
$ mdbook-slide-evaluator book/html/android/aidl
book/html/android/aidl/birthday-service.html: 750x134
book/html/android/aidl/example-service/changing-definition.html: 750x555
book/html/android/aidl/example-service/changing-implementation.html: 750x786
book/html/android/aidl/example-service/client.html: 750x1096
book/html/android/aidl/example-service/deploy.html: 750x635
book/html/android/aidl/example-service/interface.html: 750x570
book/html/android/aidl/example-service/server.html: 750x837
book/html/android/aidl/example-service/service-bindings.html: 750x483
book/html/android/aidl/example-service/service.html: 750x711
book/html/android/aidl/types/arrays.html: 750x291
book/html/android/aidl/types/file-descriptor.html: 750x1114
book/html/android/aidl/types/objects.html: 750x1258
book/html/android/aidl/types/parcelables.html: 750x637
book/html/android/aidl/types/primitives.html: 750x366
book/html/android/aidl/types.html: 750x197
```

---------

Co-authored-by: Martin Geisler <martin@geisler.net>
This commit is contained in:
michael-kerscher 2024-08-21 09:18:07 +02:00 committed by GitHub
parent 958bfe58c5
commit 355d65b3ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 824 additions and 5 deletions

3
.gitignore vendored
View File

@ -27,3 +27,6 @@ crowdin.yml
# Google's Project IDX files and VSCode
.idx/
.vscode/
# Python virtualenv (for mdbook-slide-evaluator local installation)
.venv/

219
Cargo.lock generated
View File

@ -276,6 +276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
@ -300,6 +301,18 @@ dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
@ -326,6 +339,27 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
name = "control-flow-basics"
version = "0.1.0"
[[package]]
name = "cookie"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"time",
"version_check",
]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -426,6 +460,27 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "cxx"
version = "1.0.126"
@ -495,6 +550,15 @@ dependencies = [
"cxx-build",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -630,6 +694,31 @@ dependencies = [
"thiserror",
]
[[package]]
name = "fantoccini"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d266f4ccd0f172c4daebf7419868bb1948688097ed3b7f746493ed921023a3"
dependencies = [
"base64 0.22.0",
"cookie 0.18.1",
"futures-core",
"futures-util",
"http 1.1.0",
"http-body-util",
"hyper 1.2.0",
"hyper-tls",
"hyper-util",
"mime",
"openssl",
"serde",
"serde_json",
"time",
"tokio",
"url",
"webdriver",
]
[[package]]
name = "fastrand"
version = "2.0.1"
@ -814,6 +903,12 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.14"
@ -931,6 +1026,12 @@ dependencies = [
"http 0.2.11",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -1426,6 +1527,24 @@ dependencies = [
"pulldown-cmark 0.12.0",
]
[[package]]
name = "mdbook-slide-evaluator"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"csv",
"fantoccini",
"glob",
"log",
"pretty_env_logger",
"serde",
"strum",
"tokio",
"tokio-util",
"url",
]
[[package]]
name = "memchr"
version = "2.6.4"
@ -1591,6 +1710,12 @@ dependencies = [
"notify",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.17"
@ -1883,6 +2008,12 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -2483,6 +2614,28 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -2621,6 +2774,37 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -2711,16 +2895,15 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
"tracing",
]
[[package]]
@ -2879,6 +3062,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
@ -2906,9 +3095,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.0"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
@ -3074,6 +3263,26 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webdriver"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144ab979b12d36d65065635e646549925de229954de2eb3b47459b432a42db71"
dependencies = [
"base64 0.21.5",
"bytes",
"cookie 0.16.2",
"http 0.2.11",
"log",
"serde",
"serde_derive",
"serde_json",
"thiserror",
"time",
"unicode-segmentation",
"url",
]
[[package]]
name = "winapi"
version = "0.3.9"

View File

@ -3,6 +3,7 @@
members = [
"mdbook-course",
"mdbook-exerciser",
"mdbook-slide-evaluator",
"src/android/testing",
"src/bare-metal/useful-crates/allocator-example",
"src/bare-metal/useful-crates/zerocopy-example",

View File

@ -0,0 +1,22 @@
[package]
name = "mdbook-slide-evaluator"
version = "0.1.0"
authors = ["Michael Kerscher <kerscher@google.com>"]
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/google/comprehensive-rust"
description = "A tool for evaluating mdbook slides by rendering the html pages and spot violations to the policies"
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.9", features = ["derive"] }
csv = "1.3.0"
fantoccini = "0.21.0"
glob = "0.3.1"
log = "0.4.22"
pretty_env_logger = "0.5.0"
serde = { version = "1.0.204", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
tokio = { version = "1.38.1", features = ["full"] }
tokio-util = "0.7.11"
url = "2.5.2"

View File

@ -0,0 +1,65 @@
mdbook-slide-evaluator allows you to evaluate the rendered slides. This way one
can find if there is too much content on the slides and if sorted by size one
can focus on the worst violations first.
# How to run
## Start a WebDriver compatible browser
### Alternative: Docker
Start a
[selenium docker container](https://github.com/SeleniumHQ/docker-selenium?tab=readme-ov-file#quick-start)
and mount the book folder into the container at `/book/`:
```
$ docker run -d -p 4444:4444 -p 7900:7900 --volume /path/to/my/workspace/comprehensive-rust/book:/book --shm-size="2g" selenium/standalone-chromium:latest
```
As the tool is running with a different base directory, you can use a relative
directory e.g., `../book/`:
```
$ cargo run -- ../book
```
### Alternative: Local WebDriver browser with `webdriver-manager`
Use [webdriver-manager](https://pypi.org/project/webdriver-manager/) to install
a e.g., a `chromedriver` onto your system with:
```
$ pip install selenium webdriver-manager
$ python3
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install(), port=4444))
# end the session when you are done.
```
You can provide the absolute path here as the browser has the same view on the
filesystem:
```
$ cargo run -- /path/to/my/workspace/comprehensive-rust/book
```
## Run mdbook-slide-size
If a screenshot directory is provided, the tool can also create screenshots to
evaluate this manually. The tool always recursively grabs all `*.html` files
from the given directory and processes it.
```
cargo run -- --screenshot-dir screenshots ../book/html/
```
# Roadmap
To avoid a `docker mount`, try to build a data uri from the given slide. This
has the challenge that this contains links to local stylesheets that have to be
included. `css_inline` can be used for that and this already works (kind of). If
someone wants to take a stab at this, feel free to contact the author.

View File

@ -0,0 +1,327 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fs;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use fantoccini::elements::Element;
use fantoccini::Client;
use log::{debug, warn};
use serde::Serialize;
use strum::Display;
use tokio_util::sync::CancellationToken;
use url::Url;
use crate::slides::{Book, Slide};
/// An Evaluator is used to render a book that is a collection of slides
/// and extract information from an element on that page. It further can
/// take a screenshot of this element and store it. A webclient instance is
/// created on creation and dropped once the Evaluator is dropped.
pub struct Evaluator<'a> {
/// webclient used to render html
webclient: Client,
/// selector for the element that is scored
element_selector: fantoccini::wd::Locator<'a>,
/// store screenshot in this directory if provided
screenshot_dir: Option<PathBuf>,
/// html base uri to the source_dir used as a prefix for each page
html_base_url: Url,
/// base directory for all processed files
source_dir: PathBuf,
/// if this token is cancelled, the process needs to end gracefully
cancellation_token: CancellationToken,
/// the policy applied to the slides
slide_policy: SlidePolicy,
}
/// element coordinates returned by the browser
#[derive(Debug)]
struct ElementSize {
/// the width of the element
width: f64,
/// the height of the element
height: f64,
}
impl From<(f64, f64, f64, f64)> for ElementSize {
fn from(value: (f64, f64, f64, f64)) -> Self {
Self { width: value.2, height: value.3 }
}
}
#[derive(Debug)]
/// holds the evaluation result for a slide
pub struct EvaluationResult {
/// metadata about the slide
slide: Slide,
/// the size of the main content element
element_size: ElementSize,
/// all policy violations
policy_violations: Vec<PolicyViolation>,
}
/// holds all evaluation results for a book
pub struct EvaluationResults {
/// metadata about the book
_book: Book,
/// the collected evaluation results
results: Vec<EvaluationResult>,
}
#[derive(Serialize)]
struct ExportFormat {
filename: PathBuf,
element_width: usize,
element_height: usize,
policy_violations: String,
}
impl EvaluationResults {
/// export the evaluation results to the given csv file, overwrites if
/// allowed
pub fn export_csv(
&self,
file: &Path,
overwrite: bool,
violations_only: bool,
) -> anyhow::Result<()> {
if file.exists() && !overwrite {
Err(anyhow!(
"Not allowed to overwrite existing evaluation results at {}",
file.display()
))?;
};
let mut csv_writer = csv::Writer::from_path(file)?;
for result in &self.results {
if violations_only && result.policy_violations.is_empty() {
continue;
}
csv_writer.serialize(ExportFormat {
filename: (*result.slide.filename).to_path_buf(),
element_width: result.element_size.width.round() as usize,
element_height: result.element_size.height.round() as usize,
policy_violations: result
.policy_violations
.iter()
.map(PolicyViolation::to_string)
.collect::<Vec<_>>()
.join(";"),
})?;
}
Ok(())
}
/// dump the results to stdout
pub fn export_stdout(&self, violations_only: bool) {
for result in &self.results {
if violations_only && result.policy_violations.is_empty() {
continue;
}
println!(
"{}: {}x{} [{}]",
result.slide.filename.display(),
result.element_size.width,
result.element_size.height,
result
.policy_violations
.iter()
.map(PolicyViolation::to_string)
.collect::<Vec<_>>()
.join(";"),
);
}
}
}
impl<'a> Evaluator<'_> {
/// create a new instance with the provided config.
/// fails if the webclient cannot be created
pub fn new(
webclient: Client,
element_selector: &'a str,
screenshot_dir: Option<PathBuf>,
html_base_url: Url,
source_dir: PathBuf,
cancellation_token: CancellationToken,
slide_policy: SlidePolicy,
) -> Evaluator<'a> {
let element_selector = fantoccini::Locator::XPath(element_selector);
Evaluator {
webclient,
element_selector,
screenshot_dir,
html_base_url,
source_dir,
cancellation_token,
slide_policy,
}
}
/// navigate the webdriver to the given url.
/// ensure that html_base_url is set before calling this
/// after this call the webdriver will see the content at the url
async fn webdriver_open_url(&self, url: &Url) -> Result<(), anyhow::Error> {
debug!("open url in webclient: {}", url);
self.webclient.goto(url.as_str()).await?;
Ok(())
}
/// evaluate the currently opened webpage return the selected content
/// element if available
async fn get_content_element_from_slide(
&self,
) -> anyhow::Result<Option<Element>> {
match self.webclient.find(self.element_selector).await {
Result::Ok(result) => Ok(Some(result)),
Result::Err(fantoccini::error::CmdError::Standard(
fantoccini::error::WebDriver {
error: fantoccini::error::ErrorStatus::NoSuchElement,
..
},
)) => anyhow::Ok(None),
Result::Err(error) => Err(anyhow!(error))?,
}
}
/// extract the element coordinates from this element
async fn get_element_coordinates(
&self,
element: &Element,
) -> anyhow::Result<ElementSize> {
let coordinates = Into::<ElementSize>::into(element.rectangle().await?);
Ok(coordinates)
}
/// store the screenshot as png to the given path
fn store_screenshot(
&self,
screenshot: Vec<u8>,
filename: &Path,
) -> anyhow::Result<()> {
let relative_filename = filename.strip_prefix(&self.source_dir)?;
let output_filename = self
.screenshot_dir
.as_ref()
.unwrap()
.join(relative_filename.with_extension("png"));
debug!("write screenshot to {}", output_filename.to_str().unwrap());
// create directories if necessary
let output_dir = output_filename.parent().unwrap();
if !output_dir.exists() {
debug!("creating {}", output_dir.to_str().unwrap());
fs::create_dir_all(output_dir)?;
}
let mut file =
fs::OpenOptions::new().create(true).write(true).open(output_filename)?;
file.write_all(&screenshot)?;
Ok(())
}
/// evaluate a single slide
pub async fn eval_slide(
&self,
slide: &Slide,
) -> anyhow::Result<Option<EvaluationResult>> {
debug!("evaluating {:?}", slide);
let url = self.html_base_url.join(&slide.filename.display().to_string())?;
self.webdriver_open_url(&url).await?;
let Some(content_element) = self.get_content_element_from_slide().await?
else {
return Ok(None);
};
let element_size = self.get_element_coordinates(&content_element).await?;
if self.screenshot_dir.is_some() {
let screenshot = content_element.screenshot().await?;
self.store_screenshot(screenshot, &slide.filename)?;
}
let policy_violations = self.slide_policy.eval_size(&element_size);
let result = EvaluationResult {
slide: slide.clone(),
element_size,
policy_violations,
};
debug!("information about element: {:?}", result);
Ok(Some(result))
}
/// evaluate an entire book
pub async fn eval_book(&self, book: Book) -> anyhow::Result<EvaluationResults> {
let mut results = vec![];
debug!("slide count: {}", book.slides().len());
for slide in book.slides().iter() {
if self.cancellation_token.is_cancelled() {
debug!("received cancel request, return already completed results");
break;
}
let Some(result) = self.eval_slide(slide).await? else {
warn!("slide with no content - ignore: {:?}", slide);
continue;
};
results.push(result);
}
Ok(EvaluationResults { _book: book, results })
}
}
/// all possible policy violations
#[derive(Debug, Display, Serialize)]
enum PolicyViolation {
/// violation of the maximum height
MaxWidth,
/// violation of the maximum width
MaxHeight,
}
/// the SlidePolicy struct contains all parameters for evaluating a slide
pub struct SlidePolicy {
/// the maximum allowed width of a slide
pub max_width: usize,
/// the maximum allowed height of a slide
pub max_height: usize,
}
impl SlidePolicy {
/// evaluate if the width is within the policy
fn eval_width(&self, element_size: &ElementSize) -> Option<PolicyViolation> {
if element_size.width as usize > self.max_width {
return Some(PolicyViolation::MaxWidth);
}
return None;
}
/// evaluate if the width is within the policy
fn eval_height(&self, element_size: &ElementSize) -> Option<PolicyViolation> {
if element_size.height as usize > self.max_height {
return Some(PolicyViolation::MaxHeight);
}
return None;
}
/// evaluate all size policies
fn eval_size(&self, element_size: &ElementSize) -> Vec<PolicyViolation> {
[self.eval_height(element_size), self.eval_width(element_size)]
.into_iter()
.flatten()
.collect()
}
}

View File

@ -0,0 +1,16 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod evaluator;
pub mod slides;

View File

@ -0,0 +1,122 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::path::PathBuf;
use clap::Parser;
use log::{debug, info};
use mdbook_slide_evaluator::evaluator::{Evaluator, SlidePolicy};
use mdbook_slide_evaluator::slides::Book;
use tokio_util::sync::CancellationToken;
use url::Url;
#[derive(Parser)]
#[command(version, about, arg_required_else_help(true))]
struct Args {
/// the URI of the webdriver
#[arg(long, default_value_t=String::from("http://localhost:4444"))]
webdriver: String,
/// the XPath to element that is evaluated
#[arg(long, default_value_t=String::from(r#"//*[@id="content"]/main"#))]
element: String,
/// take screenshots of the content element if provided
#[arg(short, long)]
screenshot_dir: Option<PathBuf>,
/// a base url that is used to render the files (relative to source_dir).
/// if you mount the slides at source_dir into / in a webdriver docker
/// container you can use the default
#[arg(long, default_value_t=Url::parse("file:///").unwrap())]
base_url: Url,
/// exports to csv file if provided, otherwise to stdout
#[arg(long)]
export: Option<PathBuf>,
/// allows overwriting the export file
#[arg(long, default_value_t = false)]
overwrite: bool,
/// the height of the webclient that renders the slide
#[arg(long, default_value_t = 1920)]
webclient_width: u32,
/// the width of the webclient that renders the slide
#[arg(long, default_value_t = 1080)]
webclient_height: u32,
/// max width of a slide
#[arg(long, default_value_t = 750)]
width: usize,
/// max height of a slide - default height/width values have 16/9 ratio
#[arg(long, default_value_t = 1333)]
height: usize,
/// if set only violating slides are shown
#[arg(long, default_value_t = false)]
violations_only: bool,
/// directory of the book that is evaluated
source_dir: PathBuf,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// pretty env receives log level from RUST_LOG env variable
pretty_env_logger::init();
let args = Args::parse();
// gather information about the book from the filesystem
let book = Book::from_html_slides(args.source_dir.clone())?;
// create a new webclient that is used by the evaluator
let webclient: fantoccini::Client =
fantoccini::ClientBuilder::native().connect(&args.webdriver).await?;
// use a defined window size for reproducible results
webclient.set_window_size(args.webclient_width, args.webclient_height).await?;
let cancellation_token = CancellationToken::new();
let slide_policy =
SlidePolicy { max_width: args.width, max_height: args.height };
// create a new evaluator (connects to the provided webdriver)
let evaluator = Evaluator::new(
webclient.clone(),
&args.element,
args.screenshot_dir,
args.base_url,
args.source_dir.to_path_buf(),
cancellation_token.clone(),
slide_policy,
);
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
info!("received CTRL+C");
// send a cancel signal
cancellation_token.cancel();
});
// evaluate each slide
let score_results = evaluator.eval_book(book).await?;
if let Some(export_file) = args.export {
score_results.export_csv(
&export_file,
args.overwrite,
args.violations_only,
)?;
} else {
score_results.export_stdout(args.violations_only);
}
// close webclient as otherwise the unclosed session cannot be reused
debug!("closing webclient");
webclient.close().await?;
Ok(())
}

View File

@ -0,0 +1,54 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use log::debug;
/// a slide is a page in the book
#[derive(Debug, Clone)]
pub struct Slide {
pub filename: Arc<Path>,
}
/// a book is a collection of slides
pub struct Book {
/// the path to the root directory of this book
_source_dir: PathBuf,
/// the collection of slides
slides: Vec<Slide>,
}
impl Book {
/// create a book from all html files in the source_dir
pub fn from_html_slides(source_dir: PathBuf) -> anyhow::Result<Book> {
let mut slides = vec![];
let files = glob::glob(&format!(
"{}/**/*.html",
source_dir.to_str().expect("invalid path")
))?;
for file in files {
let slide = Slide { filename: file?.into() };
debug!("add {:?}", slide);
slides.push(slide);
}
Ok(Book { _source_dir: source_dir, slides })
}
/// return a reference to the slides of this book
pub fn slides(&self) -> &[Slide] {
&self.slides
}
}