From 89c8eb0ef22bd74bc3038c7d208589acbfd4d54a Mon Sep 17 00:00:00 2001 From: Andrew Walbran <qwandor@google.com> Date: Thu, 30 Mar 2023 12:42:21 +0100 Subject: [PATCH] Add utility to build exercise starting directory from Markdown. --- Cargo.lock | 144 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + exerciser/Cargo.toml | 14 ++++ exerciser/src/main.rs | 151 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 310 insertions(+) create mode 100644 exerciser/Cargo.toml create mode 100644 exerciser/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9aab17b2..39ce3cf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-example" version = "0.1.0" @@ -9,6 +18,23 @@ dependencies = [ "buddy_system_allocator", ] +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -167,6 +193,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.3.0" @@ -188,6 +227,16 @@ dependencies = [ "libc", ] +[[package]] +name = "exerciser" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "pretty_env_logger", + "pulldown-cmark", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -350,6 +399,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.2.6" @@ -413,6 +471,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + [[package]] name = "hyper" version = "0.14.25" @@ -839,6 +906,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -854,6 +931,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.26" @@ -962,6 +1056,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "reqwest" version = "0.11.16" @@ -1263,6 +1374,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -1370,6 +1490,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1420,6 +1549,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.0" @@ -1534,6 +1669,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 68284911..18691deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "exerciser", "src/exercises", "src/bare-metal/useful-crates/allocator-example", "src/bare-metal/useful-crates/zerocopy-example", diff --git a/exerciser/Cargo.toml b/exerciser/Cargo.toml new file mode 100644 index 00000000..135b2100 --- /dev/null +++ b/exerciser/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "exerciser" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Andrew Walbran <qwandor@google.com>"] +description = "A tool for extracting starter code for exercises from Markdown files." +repository = "https://github.com/google/comprehensive-rust" + +[dependencies] +anyhow = "1.0.68" +log = "0.4.17" +pretty_env_logger = "0.4.0" +pulldown-cmark = { version = "0.9.2", default-features = false } diff --git a/exerciser/src/main.rs b/exerciser/src/main.rs new file mode 100644 index 00000000..e6812ded --- /dev/null +++ b/exerciser/src/main.rs @@ -0,0 +1,151 @@ +// Copyright 2023 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 anyhow::Context; +use log::{info, trace}; +use pulldown_cmark::{CowStr, Event, Parser, Tag}; +use std::{ + env::args, + fs::{create_dir, create_dir_all, read_to_string, File}, + io::Write, + path::Path, + process::exit, +}; + +const INCLUDE_START: &str = "{{#include "; +const INCLUDE_END: &str = "}}"; + +fn main() -> anyhow::Result<()> { + pretty_env_logger::init(); + + let args = args().collect::<Vec<_>>(); + + if args.len() != 3 { + eprintln!("Usage:"); + eprintln!( + " {} <src/exercises/exercise.md> <output directory>", + args[0] + ); + exit(1); + } + + let input_filename = Path::new(&args[1]); + let output_directory = Path::new(&args[2]); + + create_dir(output_directory).with_context(|| { + format!("Failed to create output directory {:?}", output_directory) + })?; + + let input_directory = input_filename + .parent() + .with_context(|| "Input file has no parent directory.")?; + let input_contents = read_to_string(input_filename) + .with_context(|| format!("Failed to open {:?}", input_filename))?; + let parser = Parser::new(&input_contents); + + let mut next_filename: Option<String> = None; + let mut current_file: Option<File> = None; + for event in parser { + trace!("{:?}", event); + match event { + Event::Code(x) => { + info!("{}:", x); + next_filename = Some(x.into_string()); + } + Event::Start(Tag::CodeBlock(x)) => { + info!("Start {:?}", x); + if let Some(filename) = &next_filename { + let full_filename = output_directory.join(filename); + info!("Opening {:?}", full_filename); + if let Some(directory) = full_filename.parent() { + create_dir_all(directory)?; + } + current_file = Some(File::create(full_filename)?); + next_filename = None; + } + } + Event::Text(text) => { + info!("Text: {:?}", text); + if let Some(output_file) = &mut current_file { + write_output(text, input_directory, output_file)?; + } + } + Event::End(Tag::CodeBlock(x)) => { + info!("End {:?}", x); + current_file = None; + } + _ => {} + } + } + + Ok(()) +} + +fn write_output( + text: CowStr, + input_directory: &Path, + output_file: &mut File, +) -> anyhow::Result<()> { + for line in text.lines() { + info!("Line: {:?}", line); + if let (Some(start), Some(end)) = + (line.find(INCLUDE_START), line.find(INCLUDE_END)) + { + let include = line[start + INCLUDE_START.len()..end].trim(); + info!("Include {:?}", include); + if let Some(colon) = include.find(":") { + write_include( + &include[0..colon], + Some(&include[colon + 1..]), + input_directory, + output_file, + )?; + } else { + write_include(include, None, input_directory, output_file)?; + } + } else { + output_file.write(line.as_bytes())?; + output_file.write(b"\n")?; + } + } + + Ok(()) +} + +fn write_include( + include_filename: &str, + section: Option<&str>, + input_directory: &Path, + output_file: &mut File, +) -> anyhow::Result<()> { + let full_include_filename = input_directory.join(include_filename); + let input_file = read_to_string(full_include_filename)?; + if let Some(section) = section { + let start_anchor = format!("ANCHOR: {}", section); + let end_anchor = format!("ANCHOR_END: {}", section); + for line in input_file + .lines() + .skip_while(|line| !line.contains(&start_anchor)) + .skip(1) + .take_while(|line| !line.contains(&end_anchor)) + { + output_file.write(line.as_bytes())?; + output_file.write(b"\n")?; + } + } else { + output_file.write(input_file.as_bytes())?; + } + + Ok(()) +}