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:
parent
958bfe58c5
commit
355d65b3ba
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
219
Cargo.lock
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
22
mdbook-slide-evaluator/Cargo.toml
Normal file
22
mdbook-slide-evaluator/Cargo.toml
Normal 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"
|
65
mdbook-slide-evaluator/README.md
Normal file
65
mdbook-slide-evaluator/README.md
Normal 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.
|
327
mdbook-slide-evaluator/src/evaluator.rs
Normal file
327
mdbook-slide-evaluator/src/evaluator.rs
Normal 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()
|
||||
}
|
||||
}
|
16
mdbook-slide-evaluator/src/lib.rs
Normal file
16
mdbook-slide-evaluator/src/lib.rs
Normal 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;
|
122
mdbook-slide-evaluator/src/main.rs
Normal file
122
mdbook-slide-evaluator/src/main.rs
Normal 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(())
|
||||
}
|
54
mdbook-slide-evaluator/src/slides.rs
Normal file
54
mdbook-slide-evaluator/src/slides.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user