Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion tools/website-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ version = "0.1.0"
edition = "2021"
build = "build.rs"
publish = false
rust-version = "1.62"
rust-version = "1.81"

[dependencies]
yew-agent = { path = "../../packages/yew-agent/" }

[dev-dependencies]
derive_more = { version = "2.0", features = ["from"] }
gloo = "0.11"
gloo-net = "0.6"
js-sys = "0.3"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
weblog = "0.3.0"
Expand Down
223 changes: 186 additions & 37 deletions tools/website-test/build.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,141 @@
use std::collections::HashMap;
use std::fmt::{self, Write};
use std::error::Error;
use std::fmt::Write;
use std::fs::File;
use std::io::{self, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::{env, fs};

use glob::glob;

type Result<T = ()> = core::result::Result<T, Box<dyn Error + 'static>>;

macro_rules! e {
($($fmt:tt),* $(,)?) => {
return Err(format!($($fmt),*).into())
};
}

macro_rules! assert {
($condition:expr, $($fmt:tt),* $(,)?) => {
if !$condition { e!($($fmt),*) }
};
}

#[derive(Debug, Default)]
struct Level {
nested: HashMap<String, Level>,
files: Vec<PathBuf>,
}

fn main() {
let home = env::var("CARGO_MANIFEST_DIR").unwrap();
let pattern = format!("{home}/../../website/docs/**/*.md*");
let base = format!("{home}/../../website");
let base = Path::new(&base).canonicalize().unwrap();
let dir_pattern = format!("{home}/../../website/docs/**");
for dir in glob(&dir_pattern).unwrap() {
println!("cargo:rerun-if-changed={}", dir.unwrap().display());
}

let mut level = Level::default();
fn should_combine_code_blocks(path: &Path) -> io::Result<bool> {
const FLAG: &[u8] = b"<!-- COMBINE CODE BLOCKS -->";

for entry in glob(&pattern).unwrap() {
let path = entry.unwrap();
let path = Path::new(&path).canonicalize().unwrap();
println!("cargo:rerun-if-changed={}", path.display());
let rel = path.strip_prefix(&base).unwrap();
let mut file = File::open(path)?;
match file.seek(SeekFrom::End(-32)) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::InvalidInput => return Ok(false),
Err(e) => return Err(e),
}
let mut buf = [0u8; 32];
file.read_exact(&mut buf)?;
Ok(buf.trim_ascii_end().ends_with(FLAG))
}

let mut parts = vec![];
fn apply_diff(src: &mut String, preamble: &str, added: &str, removed: &str) -> Result {
assert!(
!preamble.is_empty() || !removed.is_empty(),
"Failure on applying a diff: \nNo preamble or text to remove provided, unable to find \
location to insert:\n{added}\nIn the following text:\n{src}",
);

let mut matches = src.match_indices(if preamble.is_empty() {
removed
} else {
preamble
});
let Some((preamble_start, _)) = matches.next() else {
e!(
"Failure on applying a diff: \ncouldn't find the following text:\n{preamble}\n\nIn \
the following text:\n{src}"
)
};

assert!(
matches.next().is_none(),
"Failure on applying a diff: \nAmbiguous preamble:\n{preamble}\nIn the following \
text:\n{src}\nWhile trying to remove the following text:\n{removed}\nAnd add the \
following:\n{added}\n"
);

let preamble_end = preamble_start + preamble.len();
assert!(
src.get(preamble_end..preamble_end + removed.len()) == Some(removed),
"Failure on applying a diff: \nText to remove not found:\n{removed}\n\nIn the following \
text:\n{src}",
);

src.replace_range(preamble_end..preamble_end + removed.len(), added);
Ok(())
}

for part in rel {
parts.push(part.to_str().unwrap());
fn combined_code_blocks(path: &Path) -> Result<String> {
let file = BufReader::new(File::open(path)?);
let mut res = String::new();

let mut err = Ok(());
let mut lines = file
.lines()
.filter_map(|i| i.map_err(|e| err = Err(e)).ok());
while let Some(line) = lines.next() {
if !line.starts_with("```rust") {
continue;
}

level.insert(path.clone(), &parts[..]);
let mut preamble = String::new();
let mut added = String::new();
let mut removed = String::new();
let mut diff_applied = false;
for line in &mut lines {
if line.starts_with("```") {
if !added.is_empty() || !removed.is_empty() {
apply_diff(&mut res, &preamble, &added, &removed)?;
} else if !diff_applied {
// if no diff markers were found, just add the contents
res += &preamble;
}
break;
} else if let Some(line) = line.strip_prefix('+') {
if line.starts_with(char::is_whitespace) {
added += " ";
}
added += line;
added += "\n";
} else if let Some(line) = line.strip_prefix('-') {
if line.starts_with(char::is_whitespace) {
removed += " ";
}
removed += line;
removed += "\n";
} else if line.trim_ascii() == "// ..." {
// disregard the preamble
preamble.clear();
} else {
if !added.is_empty() || !removed.is_empty() {
diff_applied = true;
apply_diff(&mut res, &preamble, &added, &removed)?;
preamble += &added;
added.clear();
removed.clear();
}
preamble += &line;
preamble += "\n";
}
}
}

let out = format!("{}/website_tests.rs", env::var("OUT_DIR").unwrap());

fs::write(out, level.to_contents()).unwrap();
Ok(res)
}

impl Level {
Expand All @@ -53,14 +148,14 @@ impl Level {
}
}

fn to_contents(&self) -> String {
fn to_contents(&self) -> Result<String> {
let mut dst = String::new();

self.write_inner(&mut dst, 0).unwrap();
dst
self.write_inner(&mut dst, 0)?;
Ok(dst)
}

fn write_into(&self, dst: &mut String, name: &str, level: usize) -> fmt::Result {
fn write_into(&self, dst: &mut String, name: &str, level: usize) -> Result {
self.write_space(dst, level);
let name = name.replace(['-', '.'], "_");
writeln!(dst, "pub mod {name} {{")?;
Expand All @@ -73,24 +168,33 @@ impl Level {
Ok(())
}

fn write_inner(&self, dst: &mut String, level: usize) -> fmt::Result {
fn write_inner(&self, dst: &mut String, level: usize) -> Result {
for (name, nested) in &self.nested {
nested.write_into(dst, name, level)?;
}

self.write_space(dst, level);

for file in &self.files {
let stem = Path::new(file)
let stem = file
.file_stem()
.unwrap()
.ok_or_else(|| format!("no filename in path {file:?}"))?
.to_str()
.unwrap()
.ok_or_else(|| format!("non-UTF8 path: {file:?}"))?
.replace('-', "_");

self.write_space(dst, level);

writeln!(dst, "#[doc = include_str!(r\"{}\")]", file.display())?;
if should_combine_code_blocks(file)? {
let res = combined_code_blocks(file)?;
self.write_space(dst, level);
writeln!(dst, "/// ```rust, no_run")?;
for line in res.lines() {
self.write_space(dst, level);
writeln!(dst, "/// {line}")?;
}
self.write_space(dst, level);
writeln!(dst, "/// ```")?;
} else {
self.write_space(dst, level);
writeln!(dst, "#[doc = include_str!(r\"{}\")]", file.display())?;
}
self.write_space(dst, level);
writeln!(dst, "pub fn {stem}_md() {{}}")?;
}
Expand All @@ -104,3 +208,48 @@ impl Level {
}
}
}

fn inner_main() -> Result {
let home = env::var("CARGO_MANIFEST_DIR")?;
let pattern = format!("{home}/../../website/docs/**/*.md*");
let base = format!("{home}/../../website");
let base = Path::new(&base).canonicalize()?;
let dir_pattern = format!("{home}/../../website/docs/**");
for dir in glob(&dir_pattern)? {
println!("cargo:rerun-if-changed={}", dir?.display());
}

let mut level = Level::default();

for entry in glob(&pattern)? {
let path = entry?.canonicalize()?;
println!("cargo:rerun-if-changed={}", path.display());
let rel = path.strip_prefix(&base)?;

let mut parts = vec![];

for part in rel {
parts.push(
part.to_str()
.ok_or_else(|| format!("Non-UTF8 path: {rel:?}"))?,
);
}

level.insert(path.clone(), &parts[..]);
}

let out = format!("{}/website_tests.rs", env::var("OUT_DIR")?);

fs::write(out, level.to_contents()?)?;
Ok(())
}

fn main() -> ExitCode {
match inner_main() {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}
3 changes: 0 additions & 3 deletions tools/website-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#![allow(clippy::needless_doctest_main)]
pub mod tutorial;

include!(concat!(env!("OUT_DIR"), "/website_tests.rs"));
7 changes: 0 additions & 7 deletions tools/website-test/src/tutorial.rs

This file was deleted.

49 changes: 49 additions & 0 deletions website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,55 @@ Note this only builds for English locale unlike a production build.
> Documentation is written in `mdx`, a superset of markdown empowered with jsx.
> JetBrains and VSCode both provide MDX plugins.

## Testing

```console
cargo make website-test
```

[`website-test`](../tools/website-test) is a tool to test all code blocks in the docs as Rust doctests.
It gathers the Rust code blocks automatically, but by default they're all tested separate. In case of a
walkthrough, it makes more sense to combine the changes described in the blocks & test the code as one.
For this end `website-test` scans all doc files for a special flag:

```html
<!-- COMBINE CODE BLOCKS -->
```
If a file ends with this specific comment (and an optional newline after it), all code blocks will be
sown together, with respect to the diff markers in them. For example:

```md
\`\`\`rust
fn main() {
println!("Hello, World");
}
\`\`\`

\`\`\`rust
fn main() {
- println!("Hello, World");
+ println!("Goodbye, World");
}
\`\`\`

<!-- COMBINE CODE BLOCKS -->
```

Will be tested as:
```rust
fn main() {
println!("Goodbye, World");
}
```

:::warning
The current implementation only uses the code before the diff or the code to remove as context,
so make sure there's enough of it. The test assembler will tell you if there isn't.
:::

While assembling the code blocks, the test assembler will put special meaning into a code
line `// ...`. This line tells the test assembler to disregard any previous context for applying a diff

## Production Build

```console
Expand Down
12 changes: 5 additions & 7 deletions website/docs/concepts/function-components/properties.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -318,14 +318,12 @@ Props are evaluated in the order they're specified, as shown by the following ex
#[derive(yew::Properties, PartialEq)]
struct Props { first: usize, second: usize, last: usize }

fn main() {
let mut g = 1..=3;
let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() });
let mut g = 1..=3;
let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() });

assert_eq!(props.first, 1);
assert_eq!(props.second, 2);
assert_eq!(props.last, 3);
}
assert_eq!(props.first, 1);
assert_eq!(props.second, 2);
assert_eq!(props.last, 3);
```

## Anti Patterns
Expand Down
Loading
Loading