├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── src ├── lib.rs ├── main.rs ├── md2rs.rs ├── rs2md.rs ├── testing │ ├── mod.rs │ └── test_snippets.rs └── timestamp.rs └── tests └── runner.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *~ 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tango" 3 | version = "0.8.3" 4 | authors = ["Felix S. Klock II "] 5 | description = "Markdown-based Literate programming in Rust, integrated with Cargo." 6 | license = "MIT/Apache-2.0" 7 | repository = "https://github.com/pnkfelix/tango" 8 | documentation = "https://github.com/pnkfelix/tango-demo/blob/master/README.md" 9 | 10 | [dependencies] 11 | ## Every time I tried to get `debug!` working with `cargo test` I am 12 | ## flummoxed. Not sure what my problem is, but for now these are not 13 | ## enough on their own, so I am leaving them out of the checked-in 14 | ## version. 15 | ## 16 | # log = "*" 17 | # env_logger = "*" 18 | filetime = "0.1" 19 | walkdir = "1.0" 20 | url = "1.4" 21 | ## dev-dependencies are only pulled in for tests/benchmarks 22 | [dev-dependencies] 23 | tempdir = "0.3" 24 | 25 | [[test]] 26 | name = "runner" 27 | ## 28 | ## Can set below to stop passing `--test` when compiling runner.rs 29 | ## (and thus use a `fn main` within it as its associated test driver). 30 | ## But for now I am trying to work within the integrated testing framework, 31 | ## which means in part avoiding global state like `env::set_current_dir` 32 | ## 33 | # harness = false 34 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Felix Stanley Klock II 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | 27 | Status API Training Shop Blog About Help 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tango 2 | Markdown-based Literate programming in Rust, integrated with Cargo. 3 | 4 | See https://github.com/pnkfelix/tango-demo/ for a demonstration of how to use it. 5 | 6 | (At some point I hope to document the source code here, but I suspect that I will wait until after I do a rewrite so that the source code actually has some sort of sane basis with a parser or at least proper regexps.) 7 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // a no-op `build.rs` to trick `cargo` into setting `OUT_DIR` env var. 3 | } 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // #[macro_use] 2 | // extern crate log; 3 | // extern crate env_logger; 4 | 5 | extern crate filetime; 6 | extern crate url; 7 | extern crate walkdir; 8 | 9 | use filetime::set_file_times; 10 | use walkdir::{WalkDir}; 11 | 12 | use std::convert; 13 | use std::error::Error as ErrorTrait; 14 | use std::ffi::OsStr; 15 | use std::fmt; 16 | use std::fs::{self, File}; 17 | use std::io::{self, Read, Write}; 18 | use std::ops; 19 | use std::path::{Path, PathBuf}; 20 | use std::cell::RefCell; 21 | 22 | use self::timestamp::{Timestamp, Timestamped}; 23 | 24 | pub mod timestamp; 25 | 26 | pub const STAMP: &'static str = "tango.stamp"; 27 | //pub const SRC_DIR: &'static str = "src"; 28 | // pnkfelix wanted the `LIT_DIR` to be `lit/`, but `cargo build` 29 | // currently assumes that *all* build sources live in `src/`. So it 30 | // is easier for now to just have the two directories be the same. 31 | //pub const LIT_DIR: &'static str = "src/lit"; 32 | 33 | thread_local! { 34 | pub static SRC_DIR: RefCell = RefCell::new("src".to_string()); 35 | pub static LIT_DIR: RefCell = RefCell::new("src".to_string()); 36 | } 37 | 38 | fn set_lit_dir(directory: String) { 39 | LIT_DIR.with(|lit_dir| { 40 | *lit_dir.borrow_mut() = directory 41 | }); 42 | } 43 | 44 | fn set_src_dir(directory: String) { 45 | SRC_DIR.with(|src_dir| { 46 | *src_dir.borrow_mut() = directory 47 | }); 48 | } 49 | 50 | /// Returns the current directory for storing the literate .md files 51 | pub fn get_lit_dir() -> String { 52 | LIT_DIR.with(|lit_dir| lit_dir.borrow().clone()) 53 | } 54 | 55 | /// Returns the current directory for storing the "source" .rs files 56 | pub fn get_src_dir() -> String { 57 | SRC_DIR.with(|src_dir| src_dir.borrow().clone()) 58 | } 59 | 60 | pub struct Config { 61 | src_dir: String, 62 | lit_dir: String, 63 | rerun_if: bool, 64 | } 65 | 66 | impl Config { 67 | pub fn new() -> Config { 68 | Config { 69 | src_dir: String::from("src"), 70 | lit_dir: String::from("src"), 71 | rerun_if: false, 72 | } 73 | } 74 | pub fn set_src_dir(&mut self, new_src_dir: String) -> &mut Config { 75 | self.src_dir = new_src_dir; 76 | self 77 | } 78 | pub fn set_lit_dir(&mut self, new_lit_dir: String) -> &mut Config { 79 | self.lit_dir = new_lit_dir; 80 | self 81 | } 82 | pub fn emit_rerun_if(&mut self) -> &mut Config { 83 | self.rerun_if = true; 84 | self 85 | } 86 | 87 | } 88 | 89 | 90 | #[derive(Debug)] 91 | pub enum Error { 92 | IoError(io::Error), 93 | CheckInputError { error: check::Error }, 94 | MtimeError(PathBuf), 95 | ConcurrentUpdate { path_buf: PathBuf, old_time: mtime, new_time: mtime }, 96 | Warnings(Vec), 97 | } 98 | 99 | #[derive(Debug)] 100 | pub enum Warning { 101 | EncodedUrlMismatch { actual: String, expect: String } 102 | } 103 | 104 | impl fmt::Display for Warning { 105 | fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { 106 | match *self { 107 | Warning::EncodedUrlMismatch { ref actual, ref expect } => { 108 | write!(w, "mismatch between encoded url, expect: {} actual: {}", 109 | expect, actual) 110 | } 111 | } 112 | } 113 | } 114 | 115 | impl From for Error { 116 | fn from(e: md2rs::Exception) -> Self { 117 | match e { 118 | md2rs::Exception::IoError(e) => Error::IoError(e), 119 | md2rs::Exception::Warnings(w) => Error::Warnings(w), 120 | } 121 | } 122 | } 123 | 124 | impl fmt::Display for Error { 125 | fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { 126 | match *self { 127 | Error::IoError(_) => 128 | write!(w, "IO error running `tango`"), 129 | Error::CheckInputError { .. } => 130 | write!(w, "input check errors running `tango`"), 131 | Error::MtimeError(ref p) => 132 | write!(w, "modification time error from `tango` checking {}", 133 | p.to_string_lossy()), 134 | Error::ConcurrentUpdate { ref path_buf, .. } => 135 | write!(w, "concurrent update during `tango` to source file {}", 136 | path_buf.to_string_lossy()), 137 | Error::Warnings(ref warnings) => { 138 | for warn in warnings { 139 | (write!(w, "WARNING: {}", warn))?; 140 | } 141 | Ok(()) 142 | } 143 | } 144 | } 145 | } 146 | 147 | impl ErrorTrait for Error { 148 | fn source(&self) -> Option<&(dyn ErrorTrait + 'static)> { 149 | match *self { 150 | Error::IoError(ref e) => Some(e), 151 | Error::CheckInputError { ref error, .. } => { 152 | Some(error) 153 | } 154 | Error::Warnings(_) | 155 | Error::MtimeError(_) | 156 | Error::ConcurrentUpdate { .. } => None, 157 | } 158 | } 159 | } 160 | 161 | impl convert::From for Error { 162 | fn from(e: io::Error) -> Self { 163 | Error::IoError(e) 164 | } 165 | } 166 | 167 | impl convert::From for Error { 168 | fn from(e: walkdir::Error) -> Self { 169 | Error::IoError(From::from(e)) 170 | } 171 | } 172 | 173 | pub type Result = std::result::Result; 174 | 175 | #[allow(non_camel_case_types)] 176 | pub type mtime = Timestamp; 177 | 178 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 179 | enum MtimeResult { 180 | NonExistant, 181 | Modified(mtime), 182 | } 183 | 184 | trait Mtime { fn modified(&self) -> Result; } 185 | impl Mtime for File { 186 | fn modified(&self) -> Result { 187 | // #![allow(deprecated)] 188 | // if let Some(p) = self.path() { 189 | // if !p.exists() { 190 | // return Err(Error::MtimeError(p.to_path_buf())); 191 | // } 192 | // } 193 | let m = (self.metadata())?; 194 | Ok(MtimeResult::Modified(m.timestamp())) 195 | } 196 | } 197 | impl Mtime for fs::DirEntry { 198 | fn modified(&self) -> Result { 199 | let m = (self.metadata())?; 200 | Ok(MtimeResult::Modified(m.timestamp())) 201 | } 202 | } 203 | impl Mtime for RsPath { 204 | fn modified(&self) -> Result { 205 | if self.0.exists() { 206 | let f = (File::open(&self.0))?; 207 | f.modified() 208 | } else { 209 | Ok(MtimeResult::NonExistant) 210 | } 211 | } 212 | } 213 | impl Mtime for MdPath { 214 | fn modified(&self) -> Result { 215 | if self.0.exists() { 216 | let f = (File::open(&self.0))?; 217 | f.modified() 218 | } else { 219 | Ok(MtimeResult::NonExistant) 220 | } 221 | } 222 | } 223 | 224 | pub fn process_root_with_config(config: Config) -> Result<()> { 225 | //let _root = (std::env::current_dir())?; 226 | //println!("Tango is running from: {:?}", root); 227 | //std::env::set_current_dir(_root).unwrap(); 228 | set_lit_dir(config.lit_dir); 229 | set_src_dir(config.src_dir); 230 | let emit_rerun_if = config.rerun_if; 231 | 232 | let stamp_path = Path::new(STAMP); 233 | if stamp_path.exists() { 234 | process_with_stamp((File::open(stamp_path))?, emit_rerun_if) 235 | } else { 236 | process_without_stamp(emit_rerun_if) 237 | } 238 | } 239 | 240 | 241 | pub fn process_root() -> Result<()> { 242 | //let _root = (std::env::current_dir())?; 243 | // println!("Tango is running from: {:?}", _root); 244 | 245 | let emit_rerun_if = false; 246 | let stamp_path = Path::new(STAMP); 247 | if stamp_path.exists() { 248 | process_with_stamp((File::open(stamp_path))?, emit_rerun_if) 249 | } else { 250 | process_without_stamp(emit_rerun_if) 251 | } 252 | } 253 | 254 | // Both of the functions below have the same basic outline: 255 | // 256 | // 1. gather_inputs(): Build up a list of potential transforms based 257 | // on existing files. 258 | // 259 | // 2. generate_content(): Apply each transform in turn, *iff* the 260 | // source is newer than target. 261 | // 262 | // 3. check_input_timestamps(): Ensure no input was concurrently 263 | // modified while tango ran. 264 | // 265 | // 4. adjust_stamp_timestamp(): Update the `tango.stamp` file to the 266 | // youngest timestamp we saw, creating the file if necessary. 267 | // 268 | // The reason there are two functions is that in one case we have a 269 | // pre-existing `tango.stamp` that we want to compare against during 270 | // `generate_content()` (to guard against diverging {source, target} 271 | // paths; *at most* one of {source, target} is meant to be updated in 272 | // between tango runs. 273 | // 274 | // (It probably wouldn't be hard to unify the two functions into a 275 | // single method on the `Context`, though.) 276 | 277 | fn process_with_stamp(stamp: File, emit_rerun_if: bool) -> Result<()> { 278 | println!("\n\nemit rerun if: {:?}\n\n", emit_rerun_if); 279 | if let Ok(MtimeResult::Modified(ts)) = stamp.modified() { 280 | println!("Rerunning tango; last recorded run was stamped: {}", 281 | ts.date_fulltime_badly()); 282 | } else { 283 | panic!("why are we trying to process_with_stamp when given: {:?}", stamp); 284 | } 285 | let mut c = (Context::new(Some(stamp)))?; 286 | c.emit_rerun_if = emit_rerun_if; 287 | (c.gather_inputs())?; 288 | (c.generate_content())?; 289 | (c.check_input_timestamps())?; 290 | (c.adjust_stamp_timestamp())?; 291 | // (c.report_dir(Path::new(".")))?; 292 | Ok(()) 293 | } 294 | 295 | fn process_without_stamp(emit_rerun_if: bool) -> Result<()> { 296 | println!("Running tango; no previously recorded run"); 297 | println!("\n\nemit rerun if: {:?}\n\n", emit_rerun_if); 298 | let mut c = (Context::new(None))?; 299 | c.emit_rerun_if = emit_rerun_if; 300 | (c.gather_inputs())?; 301 | (c.generate_content())?; 302 | (c.check_input_timestamps())?; 303 | (c.create_stamp())?; 304 | (c.adjust_stamp_timestamp())?; 305 | // (c.report_dir(Path::new(".")))?; 306 | Ok(()) 307 | } 308 | 309 | #[derive(Debug)] 310 | struct RsPath(PathBuf); 311 | #[derive(Debug)] 312 | struct MdPath(PathBuf); 313 | 314 | 315 | struct Context { 316 | orig_stamp: Option<(File, mtime)>, 317 | src_inputs: Vec>, 318 | lit_inputs: Vec>, 319 | newest_stamp: Option, 320 | emit_rerun_if: bool, 321 | } 322 | 323 | trait Extensions { 324 | fn extension(&self) -> Option<&str>; 325 | fn rs_extension(&self) -> bool { 326 | self.extension() == Some("rs") 327 | } 328 | fn md_extension(&self) -> bool { 329 | self.extension() == Some("md") 330 | } 331 | } 332 | 333 | impl Extensions for Path { 334 | fn extension(&self) -> Option<&str> { 335 | Path::extension(self).and_then(|s|s.to_str()) 336 | } 337 | } 338 | 339 | impl ops::Deref for RsPath { 340 | type Target = Path; fn deref(&self) -> &Path { &self.0 } 341 | } 342 | 343 | impl ops::Deref for MdPath { 344 | type Target = Path; fn deref(&self) -> &Path { &self.0 } 345 | } 346 | 347 | fn check_path(typename: &str, p: &Path, ext: &str, root: &str) { 348 | println!("\n in check_path, the root is: {r:?} , path is: {p:?}, ext is {e:?}", r=root, p=p, e=ext); 349 | if Extensions::extension(p) != Some(ext) { panic!("{t} requires `.{ext}` extension; path: {p:?}", t=typename, ext=ext, p=p); } 350 | if !p.starts_with(root) { panic!("{t} must be rooted at `{root}/`; path: {p:?}", t=typename, root=root, p=p); } 351 | } 352 | 353 | impl RsPath { 354 | fn new(p: PathBuf) -> RsPath { 355 | check_path("RsPath", &p, "rs", &get_src_dir()); 356 | RsPath(p) 357 | } 358 | fn to_md(&self) -> MdPath { 359 | let mut p = PathBuf::new(); 360 | p.push(get_lit_dir()); 361 | for c in self.0.components().skip(1) { 362 | let c: &OsStr = c.as_ref(); 363 | p.push(c.to_str().expect("how else can I replace root?")); 364 | } 365 | p.set_extension("md"); 366 | MdPath::new(p) 367 | } 368 | } 369 | 370 | impl MdPath { 371 | fn new(p: PathBuf) -> MdPath { 372 | check_path("MdPath", &p, "md", &get_lit_dir()); 373 | MdPath(p) 374 | } 375 | fn to_rs(&self) -> RsPath { 376 | let mut p = PathBuf::new(); 377 | p.push(get_src_dir()); 378 | for c in self.0.components().skip(1) { 379 | let c: &OsStr = c.as_ref(); 380 | p.push(c.to_str().expect("how else can I replace root?")); 381 | } 382 | p.set_extension("rs"); 383 | RsPath::new(p) 384 | } 385 | } 386 | 387 | trait Transforms: Sized + Mtime + fmt::Debug { 388 | type Target: Mtime + fmt::Debug; 389 | 390 | // Computes path to desired target based on self's (source) path. 391 | fn target(&self) -> Self::Target; 392 | 393 | // Constructs a transform for generating the target from self 394 | // (which is a path to the source), gathering the current 395 | // timestamps on both the source and the target. 396 | fn transform(self) -> Result> { 397 | let source_time = match self.modified() { 398 | Ok(MtimeResult::Modified(t)) => t, 399 | Ok(MtimeResult::NonExistant) => panic!("impossible for {:?} to be NonExistant", self), 400 | Err(e) => { 401 | println!("failure to extract mtime on source {:?}", self); 402 | return Err(e); 403 | } 404 | }; 405 | 406 | let target = self.target(); 407 | let target_time = match target.modified() { 408 | Ok(t) => t, 409 | Err(e) => { 410 | println!("failure to extract mtime on target {:?}", target); 411 | return Err(e); 412 | } 413 | }; 414 | Ok(Transform { source_time: source_time, 415 | target_time: target_time, 416 | original: self, 417 | generate: target, 418 | }) 419 | } 420 | } 421 | 422 | impl Transforms for RsPath { 423 | type Target = MdPath; 424 | fn target(&self) -> MdPath { self.to_md() } 425 | } 426 | 427 | impl Transforms for MdPath { 428 | type Target = RsPath; 429 | fn target(&self) -> RsPath { self.to_rs() } 430 | } 431 | 432 | #[derive(Debug)] 433 | pub struct Transform { 434 | source_time: mtime, 435 | target_time: MtimeResult, 436 | original: X, 437 | generate: Y, 438 | } 439 | 440 | pub mod check { 441 | use std::error::Error as ErrorTrait; 442 | use std::fmt; 443 | use std::ops; 444 | use std::path::{Path, PathBuf}; 445 | use std::result; 446 | use super::Transform; 447 | pub type PathTransform = Transform; 448 | #[derive(Debug)] 449 | pub enum ErrorKind { 450 | TargetYoungerThanOriginal { tgt: String, src: String }, 451 | NoTangoStampExists { tgt: String, src: String }, 452 | TangoStampOlderThanTarget { tgt: String }, 453 | } 454 | #[derive(Debug)] 455 | pub struct Error(ErrorKind, PathTransform); 456 | 457 | impl fmt::Display for Error { 458 | fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { 459 | match self.0 { 460 | ErrorKind::TargetYoungerThanOriginal { ref tgt, ref src } => { 461 | write!(w, "target `{}` is younger than source `{}`; \ 462 | therefore we assume target has modifications that need to be preserved.", 463 | tgt, src) 464 | } 465 | ErrorKind::NoTangoStampExists { ref src, ref tgt } => { 466 | write!(w, "both source `{}` and target `{}` exist but no `tango.stamp` is present", 467 | src, tgt) 468 | } 469 | ErrorKind::TangoStampOlderThanTarget { ref tgt } => { 470 | write!(w, "`tango.stamp` is older than target `{}`; \ 471 | therefore we assume source and target have diverged since last tango run.", 472 | tgt) 473 | } 474 | } 475 | } 476 | } 477 | 478 | impl ErrorTrait for Error { 479 | fn description(&self) -> &str { 480 | match self.0 { 481 | ErrorKind::TargetYoungerThanOriginal { .. }=> { 482 | "target is younger than source; \ 483 | therefore we assume target has modifications that need to be preserved." 484 | } 485 | ErrorKind::NoTangoStampExists { .. } => { 486 | "both source and target exist but no `tango.stamp` is present" 487 | } 488 | ErrorKind::TangoStampOlderThanTarget { .. } => { 489 | "`tango.stamp` is older than target; \ 490 | therefore we assume source and target have diverged since last tango run." 491 | } 492 | } 493 | } 494 | } 495 | 496 | pub type Result = result::Result; 497 | 498 | impl Transform 499 | where X: ops::Deref, Y: ops::Deref 500 | { 501 | pub fn error(&self, kind: ErrorKind) -> Error { 502 | let t = Transform { original: self.original.to_path_buf(), 503 | generate: self.generate.to_path_buf(), 504 | source_time: self.source_time, 505 | target_time: self.target_time, 506 | }; 507 | Error(kind, t) 508 | } 509 | } 510 | } 511 | 512 | enum TransformNeed { Needed, Unneeded, } 513 | 514 | impl Context { 515 | fn new(opt_stamp: Option) -> Result { 516 | let stamp_modified = match opt_stamp { 517 | None => None, 518 | Some(stamp) => { 519 | let mtime = (stamp.modified())?; 520 | let mtime = match mtime { 521 | MtimeResult::NonExistant => panic!("impossible"), 522 | MtimeResult::Modified(t) => t, 523 | }; 524 | Some((stamp, mtime)) 525 | } 526 | }; 527 | let c = Context { 528 | orig_stamp: stamp_modified, 529 | src_inputs: Vec::new(), 530 | lit_inputs: Vec::new(), 531 | newest_stamp: None, 532 | emit_rerun_if: true, 533 | }; 534 | Ok(c) 535 | } 536 | 537 | fn check_transform(&self, t: &Transform) -> check::Result 538 | where X: ops::Deref + Mtime, 539 | Y: ops::Deref + Mtime, 540 | { 541 | 542 | use self::check::ErrorKind::*; 543 | 544 | 545 | let t_mod = match t.target_time { 546 | MtimeResult::Modified(t) => t, 547 | MtimeResult::NonExistant => { 548 | assert!(!t.generate.exists()); 549 | return Ok(TransformNeed::Needed); 550 | } 551 | }; 552 | // let src = t.original.display().to_string(); 553 | // let tgt = t.generate.display().to_string(); 554 | let s_mod = t.source_time; 555 | 556 | let same_age_at_low_precision = s_mod.to_ms() == t_mod.to_ms(); 557 | 558 | if t_mod > s_mod { 559 | // Target is newer than source: therefore we do not want to 560 | // overwrite the target via this transform. 561 | return Ok(TransformNeed::Unneeded); 562 | } 563 | 564 | // Now know: t_mod <= s_mod 565 | 566 | if same_age_at_low_precision { 567 | // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 568 | // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 569 | println!("Warning: source and target have timestamps that differ only at nanosecond level\n \ 570 | precision. Tango currently treats such timestamps as matching, and therefore\n \ 571 | will not rebuild the target file.\n\ 572 | \n \ 573 | source: {SRC:?} timestamp: {SRC_TS} \n \ 574 | target: {TGT:?} timestamp: {TGT_TS}\n", 575 | SRC=t.original.display(), SRC_TS=s_mod.date_fulltime_badly(), 576 | TGT=t.generate.display(), TGT_TS=t_mod.date_fulltime_badly()); 577 | return Ok(TransformNeed::Unneeded); 578 | } 579 | 580 | // Now know: t_mod is older than source even after truncating 581 | // to millisecond precision. 582 | 583 | match self.orig_stamp { 584 | None => return Err(t.error(NoTangoStampExists { 585 | src: t.original.display().to_string(), 586 | tgt: t.generate.display().to_string(), 587 | })), 588 | Some((_, stamp_time)) => { 589 | let older_at_high_precision = stamp_time < t_mod; 590 | let older_at_low_precision = stamp_time.to_ms() < t_mod.to_ms(); 591 | if older_at_low_precision { 592 | // The target file was updated more recently than 593 | // the tango.stamp file, even after truncation to 594 | // millisecond precision. 595 | // 596 | // Therefore, we assume that user has updated both 597 | // the source and the target independently since 598 | // the last tango run. This is a scenario that 599 | // tango cannot currently recover from, so we 600 | // issue an error and tell the user to fix the 601 | // problem. 602 | return Err(t.error(TangoStampOlderThanTarget { 603 | tgt: t.generate.display().to_string(), 604 | })); 605 | } 606 | if older_at_high_precision && !older_at_low_precision { 607 | // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 608 | // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 609 | println!("Warning: `tango.stamp` and target `{}` have timestamps that differ only at \n\ 610 | nanosecond level precision. Tango currently treats such timestamps as,\n\ 611 | matching and will rebuild the target file rather than error", 612 | t.generate.display()); 613 | } 614 | 615 | // got here: tango.stamp is not older than the target 616 | // file. So we fall through to the base case. 617 | } 618 | } 619 | 620 | // Invariant: 621 | // Target `t` exists, but, 622 | // s_mod >= t_mod (and t_mod <= stamp_time if stamp exists). 623 | // 624 | // Thus it is safe to overwrite `t` based on source content. 625 | Ok(TransformNeed::Needed) 626 | } 627 | 628 | #[cfg(not_now)] 629 | fn report_dir(&self, p: &Path) -> Result<()> { 630 | let src_dir = get_src_dir(); 631 | let lit_dir = get_lit_dir(); 632 | let src_path = Path::new(&src_dir); 633 | let lit_path = Path::new(&lit_dir); 634 | 635 | for (i, ent) in (WalkDir::new(p))?.enumerate() { 636 | let ent = (ent)?; 637 | let modified = (ent.modified())?; 638 | println!("entry[{}]: {:?} {:?}", i, ent.path(), modified); 639 | } 640 | Ok(()) 641 | } 642 | 643 | fn update_newest_time(&mut self, new_time: mtime) { 644 | if let Some(ref mut stamp) = self.newest_stamp { 645 | if new_time > *stamp { 646 | *stamp = new_time; 647 | } 648 | } else { 649 | self.newest_stamp = Some(new_time); 650 | } 651 | } 652 | 653 | fn push_src(&mut self, t: Transform) { 654 | self.update_newest_time(t.source_time); 655 | self.src_inputs.push(t); 656 | } 657 | fn push_lit(&mut self, t: Transform) { 658 | self.update_newest_time(t.source_time); 659 | self.lit_inputs.push(t); 660 | } 661 | 662 | fn gather_inputs(&mut self) -> Result<()> { 663 | // println!("gather_inputs"); 664 | let src_dir = get_src_dir(); 665 | let lit_dir = get_lit_dir(); 666 | let src_path = Path::new(&src_dir); 667 | let lit_path = Path::new(&lit_dir); 668 | 669 | fn keep_file_name(p: &Path) -> std::result::Result<(), &'static str> { 670 | match p.file_name().and_then(|x|x.to_str()) { 671 | None => 672 | Err("file name is not valid unicode"), 673 | Some(s) if s.starts_with('.') => 674 | Err("file name has leading period"), 675 | Some(..) => 676 | Ok(()), 677 | } 678 | } 679 | 680 | fn warn_if_nonexistant(m: &M) -> Result<()> { 681 | match m.modified() { 682 | Err(e) => Err(e), 683 | Ok(MtimeResult::Modified(..)) => Ok(()), 684 | Ok(MtimeResult::NonExistant) => { 685 | // This can arise; namely some tools are 686 | // generating symlinks in `src` of the form 687 | // 688 | // `src/.#lib.md -> fklock@fklock-Oenone.local.96195` 689 | // 690 | // where the target is non-existant (presumably as 691 | // a way to locally mark a file as being open by 692 | // the tool?), and then this script interprets it 693 | // as being open. 694 | println!("warning: non-existant source: {:?}", m); 695 | Ok(()) 696 | } 697 | } 698 | 699 | } 700 | 701 | // This loop gathers all of the .rs files that currently 702 | // exist, and schedules transforms that would turn them into 703 | // corresponding target .md files. 704 | 705 | // println!("gather-rs"); 706 | for ent in WalkDir::new(src_path).into_iter() { 707 | let ent = (ent)?; 708 | let p = ent.path(); 709 | if let Err(why) = keep_file_name(p) { 710 | println!("skipping {}; {}", p.display(), why); 711 | continue; 712 | } 713 | if !p.rs_extension() { 714 | // println!("gather-rs skip {} due to non .rs", p.display()); 715 | continue; 716 | } 717 | let rs = RsPath::new(p.to_path_buf()); 718 | (warn_if_nonexistant(&rs))?; 719 | 720 | if self.emit_rerun_if { 721 | println!("cargo:rerun-if-changed={}", &rs.display()); 722 | } 723 | 724 | let t = (rs.transform())?; 725 | match self.check_transform(&t) { 726 | Ok(TransformNeed::Needed) => self.push_src(t), 727 | Ok(TransformNeed::Unneeded) => {} 728 | Err(e) => { 729 | println!("gather_inputs err: {}", e); 730 | return Err(Error::CheckInputError { 731 | error: e, 732 | }) 733 | } 734 | } 735 | } 736 | 737 | // This loop gathers all of the .md files that currently 738 | // exist, and schedules transforms that would turn them into 739 | // corresponding target .rs files. 740 | 741 | //println!("gather-md, lit_path is: {:?}", lit_path); 742 | for ent in WalkDir::new(lit_path).into_iter() { 743 | //println!("ent is {:?}", ent); 744 | let ent = (ent)?; 745 | let p = ent.path(); 746 | if let Err(why) = keep_file_name(p) { 747 | println!("skipping {}; {}", p.display(), why); 748 | continue; 749 | } 750 | if !p.md_extension() { 751 | // println!("gather-md skip {} due to non .md", p.display()); 752 | continue; 753 | } 754 | let md = MdPath::new(p.to_path_buf()); 755 | (warn_if_nonexistant(&md))?; 756 | 757 | if self.emit_rerun_if { 758 | println!("cargo:rerun-if-changed={}", &md.display()); 759 | } 760 | 761 | let t = (md.transform())?; 762 | match self.check_transform(&t) { 763 | Ok(TransformNeed::Needed) => { 764 | // println!("gather-md add {:?}", t);; 765 | self.push_lit(t) 766 | } 767 | Ok(TransformNeed::Unneeded) => { 768 | // println!("gather-md discard unneeded {:?}", t);; 769 | } 770 | Err(e) => { 771 | println!("gather_inputs err: {}", e); 772 | return Err(Error::CheckInputError { 773 | error: e, 774 | }) 775 | } 776 | } 777 | } 778 | 779 | // At this point we've scheduled all the transforms we want to 780 | // run; they will be applied unconditionally, even if both 781 | // source and target exist. (The intent is that a target 782 | // younger than source would have been filtered during the 783 | // .check_transform calls above.) 784 | 785 | Ok(()) 786 | } 787 | fn generate_content(&mut self) -> Result<()> { 788 | for &Transform { ref original, ref generate, source_time, .. } in &self.src_inputs { 789 | let source = (File::open(&original.0))?; 790 | let target = (File::create(&generate.0))?; 791 | assert!(source_time > 0); 792 | println!("generating lit {:?}", &generate.0); 793 | (rs2md(source, target))?; 794 | let timestamp = source_time.to_filetime(); 795 | println!("backdating lit {:?} to {}", &generate.0, source_time.date_fulltime_badly()); 796 | (set_file_times(&generate.0, timestamp, timestamp))?; 797 | } 798 | for &mut Transform { ref original, ref generate, ref mut source_time, .. } in &mut self.lit_inputs { 799 | let source = (File::open(&original.0))?; 800 | let target = (File::create(&generate.0))?; 801 | assert!(*source_time > 0); 802 | println!("generating src {:?}", &generate.0); 803 | (md2rs(source, target))?; 804 | println!("backdating src {:?} to {}", &generate.0, source_time.date_fulltime_badly()); 805 | (set_file_times(&generate.0, 806 | source_time.to_filetime(), 807 | source_time.to_filetime()))?; 808 | let source = (File::open(&original.0))?; 809 | let target = (File::open(&generate.0))?; 810 | match (source.modified(), target.modified()) { 811 | (Ok(MtimeResult::Modified(src_time)), 812 | Ok(MtimeResult::Modified(tgt_time))) => { 813 | // At this point, we would *like* to assert this: 814 | #[cfg(not_possible_right_now)] assert_eq!(src_time, tgt_time); 815 | // but it does not work, due to this bug: 816 | // https://github.com/alexcrichton/filetime/issues/9 817 | 818 | assert_eq!(src_time.to_ms(), tgt_time.to_ms()); 819 | } 820 | (Ok(MtimeResult::NonExistant), _) => panic!("how could source not exist"), 821 | (_, Ok(MtimeResult::NonExistant)) => panic!("how could target not exist"), 822 | (Err(_), Err(_)) => panic!("errored looking up both source and target times"), 823 | (Err(_), _) => panic!("errored looking up source time"), 824 | (_, Err(_)) => panic!("errored looking up target time"), 825 | } 826 | } 827 | Ok(()) 828 | } 829 | fn check_input_timestamps(&mut self) -> Result<()> { 830 | for &Transform { ref original, source_time, .. } in &self.src_inputs { 831 | if let MtimeResult::Modified(new_time) = (original.modified())? { 832 | if new_time != source_time { 833 | return Err(Error::ConcurrentUpdate { 834 | path_buf: original.to_path_buf(), 835 | old_time: source_time, 836 | new_time: new_time, 837 | }) 838 | } 839 | } 840 | } 841 | for &Transform { ref original, source_time, .. } in &self.lit_inputs { 842 | if let MtimeResult::Modified(new_time) = (original.modified())? { 843 | if new_time != source_time { 844 | return Err(Error::ConcurrentUpdate { 845 | path_buf: original.to_path_buf(), 846 | old_time: source_time, 847 | new_time: new_time, 848 | }) 849 | } 850 | } 851 | } 852 | Ok(()) 853 | } 854 | fn create_stamp(&mut self) -> Result<()> { 855 | let _f = (File::create(STAMP))?; 856 | Ok(()) 857 | } 858 | fn adjust_stamp_timestamp(&mut self) -> Result<()> { 859 | if let Some(stamp) = self.newest_stamp { 860 | assert!(stamp > 0); 861 | println!("re-stamping tango.stamp to {}", stamp.date_fulltime_badly()); 862 | 863 | match set_file_times(STAMP, stamp.to_filetime(), stamp.to_filetime()) { 864 | Ok(()) => Ok(()), 865 | Err(e) => Err(Error::IoError(e)), 866 | } 867 | } else { 868 | Ok(()) 869 | } 870 | } 871 | } 872 | 873 | fn rs2md(source: R, target: W) -> Result<()> { 874 | let mut converter = rs2md::Converter::new(); 875 | converter.convert(source, target).map_err(Error::IoError) 876 | } 877 | 878 | fn md2rs(source: R, target: W) -> Result<()> { 879 | let converter = md2rs::Converter::new(); 880 | converter.convert(source, target).map_err(From::from) 881 | } 882 | 883 | mod md2rs; 884 | 885 | mod rs2md; 886 | 887 | fn encode_to_url(code: &str) -> String { 888 | use url::percent_encoding as enc; 889 | // let new_code: String = enc::utf8_percent_encode(code.trim(), enc::QUERY_ENCODE_SET); 890 | let new_code: String = enc::utf8_percent_encode(code.trim(), enc::USERINFO_ENCODE_SET).collect(); 891 | format!("https://play.rust-lang.org/?code={}&version=nightly", new_code) 892 | } 893 | 894 | #[cfg(test)] 895 | mod testing; 896 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate tango; 2 | 3 | use std::error::Error; 4 | 5 | pub fn main() { 6 | tango::process_root().unwrap_or_else(|e| { 7 | let mut cause: Option<&dyn Error> = Some(&e); 8 | while let Some(c) = cause { 9 | let next_cause = c.source(); 10 | if next_cause.is_some() { 11 | println!("{}, due to", c); 12 | } else { 13 | println!("root error: {}", c); 14 | } 15 | cause = next_cause; 16 | } 17 | panic!("IO error {}", e); 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/md2rs.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead, Write}; 2 | 3 | pub struct Converter { 4 | state: State, 5 | blank_line_count: usize, 6 | buffered_lines: String, 7 | warnings: Vec, 8 | } 9 | 10 | use super::Warning; 11 | 12 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 13 | enum State { MarkdownBlank, MarkdownText, MarkdownMeta, Rust, } 14 | impl Converter { 15 | pub fn new() -> Converter { 16 | Converter { 17 | state: State::MarkdownBlank, 18 | blank_line_count: 0, 19 | buffered_lines: String::new(), 20 | warnings: vec![], 21 | } 22 | } 23 | } 24 | 25 | pub enum Exception { 26 | IoError(io::Error), 27 | Warnings(Vec), 28 | } 29 | 30 | impl From for Exception { 31 | fn from(e: io::Error) -> Self { 32 | Exception::IoError(e) 33 | } 34 | } 35 | 36 | impl Converter { 37 | pub fn convert(mut self, r:R, mut w:W) -> Result<(), Exception> { 38 | let source = io::BufReader::new(r); 39 | for line in source.lines() { 40 | let line = (line)?; 41 | (self.handle(&line, &mut w))?; 42 | } 43 | if self.warnings.is_empty() { 44 | Ok(()) 45 | } else { 46 | Err(Exception::Warnings(self.warnings)) 47 | } 48 | } 49 | 50 | pub fn handle(&mut self, line: &str, w: &mut dyn Write) -> io::Result<()> { 51 | let str9 = line.chars().take(9).collect::(); 52 | let str7 = line.chars().take(7).collect::(); 53 | match (self.state, &str7[..], &str9[..]) { 54 | (State::MarkdownBlank, "```rust", _) | 55 | (State::MarkdownText, "```rust", _) => { 56 | self.buffered_lines = String::new(); 57 | let rest = &line.chars().skip(7).collect::(); 58 | if rest != "" { 59 | (self.transition(w, State::MarkdownMeta))?; 60 | (self.meta_note(&rest, w))?; 61 | } 62 | self.transition(w, State::Rust) 63 | } 64 | (State::MarkdownBlank, _, "```{.rust") | 65 | (State::MarkdownText, _, "```{.rust") => { 66 | self.buffered_lines = String::new(); 67 | let rest = &line.chars().skip(9).collect::(); 68 | if rest != "" { 69 | (self.transition(w, State::MarkdownMeta))?; 70 | (self.meta_note(&format!(" {{{}", rest), w))?; 71 | } 72 | self.transition(w, State::Rust) 73 | } 74 | (State::Rust, "```", _) => { 75 | self.transition(w, State::MarkdownBlank) 76 | } 77 | 78 | // FIXME: accum blank lines and only emit them with 79 | // prefix if there's no state transition; otherwise 80 | // emit them with no prefix. (This is in part the 81 | // motivation for the `fn finish_section` design.) 82 | (_, "", _) => { 83 | self.blank_line(w) 84 | } 85 | 86 | _ => { 87 | // HACK: if we find anything that looks like a markdown-named playpen link ... 88 | let open_pat = "["; 89 | let close_pat = "]: https://play.rust-lang.org/?code="; 90 | if let (Some(open), Some(close)) = (line.find(open_pat), line.find(close_pat)) { 91 | // ... then we assume it is associated with the (hopefully immediately preceding) 92 | // code block, so we emit a `//@@@` named tag for that code block. 93 | 94 | // checking here that emitted code block matches 95 | // up with emitted url. If non-match, then warn 96 | // the user, and suggest they re-run `tango` after 97 | // touching the file to generate matching url. 98 | let expect = super::encode_to_url(&self.buffered_lines); 99 | let actual = &line[(close+3)..]; 100 | if expect != actual { 101 | self.warnings.push(Warning::EncodedUrlMismatch { 102 | actual: actual.to_string(), 103 | expect: expect 104 | }) 105 | } 106 | self.name_block(line, &line[open+1..close], w) 107 | } else { 108 | self.nonblank_line(line, w) 109 | } 110 | } 111 | } 112 | } 113 | 114 | pub fn meta_note(&mut self, note: &str, w: &mut dyn Write) -> io::Result<()> { 115 | assert!(note != ""); 116 | self.nonblank_line(note, w) 117 | } 118 | 119 | pub fn name_block(&mut self, _line: &str, name: &str, w: &mut dyn Write) -> io::Result<()> { 120 | assert!(name != ""); 121 | writeln!(w, "//@@@ {}", name) 122 | } 123 | 124 | pub fn nonblank_line(&mut self, line: &str, w: &mut dyn Write) -> io::Result<()> { 125 | let (blank_prefix, line_prefix) = match self.state { 126 | State::MarkdownBlank => ("", "//@ "), 127 | State::MarkdownText => ("//@", "//@ "), 128 | State::MarkdownMeta => ("//@", "//@@"), 129 | State::Rust => ("", ""), 130 | }; 131 | for _ in 0..self.blank_line_count { 132 | (writeln!(w, "{}", blank_prefix))?; 133 | 134 | } 135 | self.blank_line_count = 0; 136 | 137 | match self.state { 138 | State::MarkdownBlank => 139 | (self.transition(w, State::MarkdownText))?, 140 | State::MarkdownMeta | 141 | State::MarkdownText => {} 142 | State::Rust => { 143 | self.buffered_lines.push_str("\n"); 144 | self.buffered_lines.push_str(line); 145 | } 146 | } 147 | 148 | writeln!(w, "{}{}", line_prefix, line) 149 | } 150 | 151 | fn blank_line(&mut self, _w: &mut dyn Write) -> io::Result<()> { 152 | match self.state { 153 | State::Rust => { 154 | self.buffered_lines.push_str("\n"); 155 | } 156 | State::MarkdownBlank | 157 | State::MarkdownMeta | 158 | State::MarkdownText => {} 159 | } 160 | self.blank_line_count += 1; 161 | Ok(()) 162 | } 163 | 164 | fn finish_section(&mut self, w: &mut dyn Write) -> io::Result<()> { 165 | for _ in 0..self.blank_line_count { 166 | (writeln!(w, ""))?; 167 | } 168 | self.blank_line_count = 0; 169 | Ok(()) 170 | } 171 | 172 | fn transition(&mut self, w: &mut dyn Write, s: State) -> io::Result<()> { 173 | match s { 174 | State::MarkdownMeta => { 175 | assert!(self.state != State::Rust); 176 | (self.finish_section(w))?; 177 | } 178 | State::Rust => { 179 | assert!(self.state != State::Rust); 180 | self.buffered_lines = String::new(); 181 | } 182 | State::MarkdownText => { 183 | assert_eq!(self.state, State::MarkdownBlank); 184 | (self.finish_section(w))?; 185 | } 186 | State::MarkdownBlank => { 187 | assert_eq!(self.state, State::Rust); 188 | (self.finish_section(w))?; 189 | } 190 | } 191 | self.state = s; 192 | Ok(()) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/rs2md.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, BufRead, Write}; 2 | use super::encode_to_url; 3 | 4 | #[derive(Debug)] 5 | pub struct Converter { 6 | output_state: State, 7 | blank_line_count: usize, 8 | buffered_code: String, 9 | meta_note: Option, 10 | } 11 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 12 | enum State { MarkdownFirstLine, MarkdownLines, Rust, } 13 | impl Converter { 14 | pub fn new() -> Converter { 15 | Converter { output_state: State::MarkdownFirstLine, 16 | blank_line_count: 0, 17 | buffered_code: String::new(), 18 | meta_note: None, } 19 | } 20 | } 21 | 22 | #[derive(Debug)] 23 | enum Effect<'a> { 24 | BlankLn, 25 | WriteLn(&'a str), 26 | StartCodeBlock, 27 | FinisCodeBlock, 28 | BlankLitComment, 29 | } 30 | 31 | #[derive(Debug)] 32 | enum EffectContext<'a> { 33 | Finalize, 34 | NonblankLine(&'a str), 35 | Transition(State), 36 | } 37 | 38 | impl Converter { 39 | pub fn convert(&mut self, r:R, mut w:W) -> io::Result<()> { 40 | let source = io::BufReader::new(r); 41 | for line in source.lines() { 42 | let line = (line)?; 43 | (self.handle(&line, &mut w))?; 44 | } 45 | self.finalize(&mut w) 46 | } 47 | 48 | pub fn finalize(&mut self, w: &mut dyn Write) -> io::Result<()> { 49 | match self.output_state { 50 | State::Rust => 51 | self.effect(EffectContext::Finalize, Effect::FinisCodeBlock, w), 52 | State::MarkdownFirstLine | 53 | State::MarkdownLines => 54 | Ok(()) 55 | } 56 | } 57 | 58 | pub fn handle(&mut self, line: &str, w: &mut dyn Write) -> io::Result<()> { 59 | let line_right = line.trim_start(); 60 | if line_right.is_empty() { 61 | self.blank_line(w) 62 | } else if line_right.starts_with("//@ ") { 63 | let line = &line_right[4..]; 64 | if line.trim().is_empty() { 65 | (self.blank_line(w))? 66 | } 67 | match self.output_state { 68 | State::Rust => 69 | (self.transition(w, State::MarkdownFirstLine))?, 70 | State::MarkdownFirstLine => 71 | (self.transition(w, State::MarkdownLines))?, 72 | State::MarkdownLines => 73 | {} 74 | } 75 | if line.trim().is_empty() { 76 | Ok(()) 77 | } else { 78 | self.nonblank_line(line, w) 79 | } 80 | } else if line_right.starts_with("//@@@") { 81 | let line = &line_right[5..]; 82 | if !line.trim().is_empty() { 83 | match self.output_state { 84 | State::Rust => { 85 | (self.transition(w, State::MarkdownFirstLine))?; 86 | (self.emit_named_code(line.trim(), w))?; 87 | } 88 | State::MarkdownFirstLine => { 89 | (self.transition(w, State::MarkdownLines))?; 90 | (self.emit_named_code(line.trim(), w))?; 91 | } 92 | State::MarkdownLines => { 93 | (self.emit_named_code(line.trim(), w))?; 94 | } 95 | } 96 | } 97 | Ok(()) 98 | } else if line_right.starts_with("//@@") { 99 | let line = &line_right[4..]; 100 | if !line.trim().is_empty() { 101 | self.set_meta_note(line.trim()); 102 | } 103 | Ok(()) 104 | } else if line_right.starts_with("//@") { 105 | let line = &line_right[3..]; 106 | match self.output_state { 107 | State::Rust => 108 | (self.transition(w, State::MarkdownFirstLine))?, 109 | State::MarkdownFirstLine => 110 | (self.transition(w, State::MarkdownLines))?, 111 | State::MarkdownLines => 112 | {} 113 | } 114 | if line.trim().is_empty() { 115 | self.blank_line(w) 116 | } else { 117 | self.nonblank_line(line, w) 118 | } 119 | } else { 120 | match self.output_state { 121 | State::MarkdownFirstLine | 122 | State::MarkdownLines => 123 | (self.transition(w, State::Rust))?, 124 | _ => {} 125 | } 126 | self.nonblank_line(line, w) 127 | } 128 | } 129 | 130 | fn emit_named_code(&mut self, name: &str, w: &mut dyn Write) -> io::Result<()> { 131 | writeln!(w, "[{}]: {}", name, encode_to_url(&self.buffered_code)) 132 | } 133 | 134 | fn set_meta_note(&mut self, note: &str) { 135 | if let Some(ref prev_note) = self.meta_note { 136 | println!("warning: discarding meta note {} for {}", prev_note, note); 137 | } 138 | self.meta_note = Some(note.to_string()); 139 | } 140 | 141 | fn effect(&mut self, _c: EffectContext, e: Effect, w: &mut dyn Write) -> io::Result<()> { 142 | // println!("effect _c: {:?} e: {:?}", _c, e); 143 | match e { 144 | Effect::BlankLn => writeln!(w, ""), 145 | Effect::WriteLn(line) => writeln!(w, "{}", line), 146 | Effect::StartCodeBlock => { 147 | if let Some(ref note) = self.meta_note { 148 | assert_eq!(note.chars().next(), Some('{')); 149 | (writeln!(w, "```{{.rust{}", ¬e[1..]))?; 150 | } else { 151 | (writeln!(w, "```rust"))?; 152 | } 153 | self.meta_note = None; 154 | self.buffered_code = String::new(); 155 | Ok(()) 156 | } 157 | Effect::FinisCodeBlock => { 158 | (writeln!(w, "```"))?; 159 | Ok(()) 160 | } 161 | Effect::BlankLitComment => writeln!(w, ""), 162 | } 163 | } 164 | 165 | fn nonblank_line(&mut self, line: &str, w: &mut dyn Write) -> io::Result<()> { 166 | for _ in 0..self.blank_line_count { 167 | (self.effect(EffectContext::NonblankLine(line), Effect::BlankLn, w))?; 168 | } 169 | if State::Rust == self.output_state { 170 | self.buffered_code = format!("{}\n{}", self.buffered_code, line); 171 | } 172 | self.blank_line_count = 0; 173 | self.effect(EffectContext::NonblankLine(line), Effect::WriteLn(line), w) 174 | } 175 | 176 | fn blank_line(&mut self, _w: &mut dyn Write) -> io::Result<()> { 177 | self.blank_line_count += 1; 178 | if State::Rust == self.output_state { 179 | self.buffered_code = format!("{}\n", self.buffered_code); 180 | } 181 | Ok(()) 182 | } 183 | 184 | fn finish_section(&mut self, _w: &mut dyn Write) -> io::Result<()> { 185 | Ok(()) 186 | } 187 | 188 | fn transition(&mut self, w: &mut dyn Write, s: State) -> io::Result<()> { 189 | match s { 190 | State::MarkdownFirstLine => { 191 | assert_eq!(self.output_state, State::Rust); 192 | (self.effect(EffectContext::Transition(s), Effect::FinisCodeBlock, w))?; 193 | for _ in 0..self.blank_line_count { 194 | (self.effect(EffectContext::Transition(s), Effect::BlankLn, w))?; 195 | } 196 | self.blank_line_count = 0; 197 | } 198 | State::MarkdownLines => { 199 | assert_eq!(self.output_state, State::MarkdownFirstLine); 200 | for _ in 0..self.blank_line_count { 201 | (self.effect(EffectContext::Transition(s), Effect::BlankLitComment, w))?; 202 | } 203 | self.blank_line_count = 0; 204 | } 205 | State::Rust => { 206 | assert!(self.output_state != State::Rust); 207 | (self.finish_section(w))?; 208 | for _ in 0..self.blank_line_count { 209 | (self.effect(EffectContext::Transition(s), Effect::BlankLn, w))?; 210 | } 211 | self.blank_line_count = 0; 212 | (self.effect(EffectContext::Transition(s), Effect::StartCodeBlock, w))?; 213 | } 214 | } 215 | self.output_state = s; 216 | Ok(()) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/testing/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{md2rs, rs2md}; 2 | mod test_snippets; 3 | 4 | struct DifferingLines<'a> { 5 | left_line_num: usize, 6 | left: &'a str, 7 | right_line_num: usize, 8 | right: &'a str, 9 | } 10 | 11 | enum ComparisonResult<'a> { 12 | Ok, 13 | LineDifferences(Vec>), 14 | LineCountMismatch(usize, usize, Vec), 15 | } 16 | 17 | // #[cfg(test)] 18 | fn compare_lines<'a>(a: &'a str, b: &'a str) -> ComparisonResult<'a> { 19 | let a: Vec<_> = a.lines().collect(); 20 | let b: Vec<_> = b.lines().collect(); 21 | let mut i = 0; 22 | let mut j = 0; 23 | 24 | let mut differing_lines: Vec = Vec::new(); 25 | 26 | while i < a.len() && j < b.len() { 27 | if a[i] == b[j] { 28 | i += 1; 29 | j += 1; 30 | continue; 31 | } 32 | 33 | differing_lines.push(DifferingLines { 34 | left_line_num: i, 35 | right_line_num: j, 36 | left: a[i], 37 | right: b[j], 38 | }); 39 | 40 | for j_ in (j+1)..b.len() { 41 | if a[i] == b[j_] { 42 | j = j_; 43 | continue; 44 | } 45 | } 46 | 47 | for i_ in (i+1)..a.len() { 48 | if a[i_] == b[j] { 49 | i = i_; 50 | continue; 51 | } 52 | } 53 | 54 | i += 1; 55 | j += 1; 56 | } 57 | 58 | if differing_lines.len() != 0 { 59 | ComparisonResult::LineDifferences(differing_lines) 60 | } else if i == a.len() && j == b.len() && i == j { 61 | ComparisonResult::Ok 62 | } else { 63 | let mut v = Vec::new(); 64 | if a.len() > b.len() { 65 | for i in b.len()..a.len() { 66 | v.push(a[i].to_string()); 67 | } 68 | } else { 69 | for j in a.len()..b.len() { 70 | v.push(b[j].to_string()); 71 | } 72 | } 73 | ComparisonResult::LineCountMismatch(a.len(), b.len(), v) 74 | } 75 | } 76 | 77 | fn panic_if_different<'a>(name_a: &str, a: &'a str, name_b: &str, b: &'a str) { 78 | match compare_lines(a, b) { 79 | ComparisonResult::LineDifferences(differences) => { 80 | for difference in differences { 81 | println!("lines {lnum} and {rnum} differ:\n{nl:>8}: {l}\n{nr:>8}: {r}", 82 | lnum=difference.left_line_num+1, 83 | rnum=difference.right_line_num+1, 84 | nl=name_a, 85 | l=difference.left, 86 | nr=name_b, 87 | r=difference.right); 88 | } 89 | panic!("saw differences"); 90 | } 91 | ComparisonResult::LineCountMismatch(a, b, v) => { 92 | for line in v { 93 | println!("excess line: {}", line); 94 | } 95 | panic!("Content differs:\n{nl:>8}: {l} lines\n{nr:>8}: {r} lines", 96 | nl=name_a, 97 | l=a, 98 | nr=name_b, 99 | r=b); 100 | } 101 | ComparisonResult::Ok => {} 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | fn core_test_md2rs(md: &str, rs: &str) { 107 | let mut output = Vec::new(); 108 | md2rs(md.as_bytes(), &mut output).unwrap(); 109 | let output = String::from_utf8(output).unwrap(); 110 | panic_if_different("actual", &output, "expect", rs); 111 | } 112 | 113 | #[cfg(test)] 114 | fn warn_test_md2rs(md: &str, rs: &str) { 115 | let mut output = Vec::new(); 116 | match md2rs(md.as_bytes(), &mut output) { 117 | Err(super::Error::Warnings(_)) => {} 118 | Ok(_) => panic!("expected successful conversion with warning"), 119 | Err(_) => panic!("error in converion"), 120 | } 121 | let output = String::from_utf8(output).unwrap(); 122 | panic_if_different("actual", &output, "expect", rs); 123 | } 124 | 125 | #[cfg(test)] 126 | fn core_test_rs2md(rs: &str, md: &str) { 127 | let mut output = Vec::new(); 128 | rs2md(rs.as_bytes(), &mut output).unwrap(); 129 | let output = String::from_utf8(output).unwrap(); 130 | panic_if_different("actual", &output, "expect", md); 131 | } 132 | 133 | #[test] 134 | fn test_onetext_md2rs() { 135 | core_test_md2rs(test_snippets::ONE_TEXT_LINE_MD, 136 | test_snippets::ONE_TEXT_LINE_RS); 137 | } 138 | 139 | #[test] 140 | fn test_onetext_rs2md() { 141 | core_test_rs2md(test_snippets::ONE_TEXT_LINE_RS, 142 | test_snippets::ONE_TEXT_LINE_MD); 143 | } 144 | 145 | #[test] 146 | fn test_onerust_md2rs() { 147 | core_test_md2rs(test_snippets::ONE_RUST_LINE_MD, 148 | test_snippets::ONE_RUST_LINE_RS); 149 | } 150 | 151 | #[test] 152 | fn test_onerust_rs2md() { 153 | core_test_rs2md(test_snippets::ONE_RUST_LINE_RS, 154 | test_snippets::ONE_RUST_LINE_MD); 155 | } 156 | 157 | #[test] 158 | fn test_hello_md2rs() { 159 | core_test_md2rs(test_snippets::HELLO_MD, test_snippets::HELLO_RS); 160 | } 161 | 162 | #[test] 163 | fn test_hello_rs2md() { 164 | core_test_rs2md(test_snippets::HELLO_RS, test_snippets::HELLO_MD); 165 | } 166 | 167 | #[test] 168 | fn test_hello2_md2rs() { 169 | core_test_md2rs(test_snippets::HELLO2_MD, test_snippets::HELLO2_RS); 170 | } 171 | 172 | #[test] 173 | fn test_hello2_rs2md() { 174 | core_test_rs2md(test_snippets::HELLO2_RS, test_snippets::HELLO2_MD); 175 | } 176 | 177 | #[test] 178 | fn test_hello3_md2rs() { 179 | core_test_md2rs(test_snippets::HELLO3_MD, test_snippets::HELLO3_RS); 180 | } 181 | 182 | #[test] 183 | fn test_hello3_rs2md() { 184 | core_test_rs2md(test_snippets::HELLO3_RS, test_snippets::HELLO3_MD); 185 | } 186 | 187 | #[test] 188 | fn test_hello4_md2rs() { 189 | core_test_md2rs(test_snippets::HELLO4_MD, test_snippets::HELLO4_RS); 190 | } 191 | 192 | #[test] 193 | fn test_hello4_rs2md() { 194 | core_test_rs2md(test_snippets::HELLO4_RS, test_snippets::HELLO4_MD); 195 | } 196 | 197 | #[test] 198 | fn test_prodigal5_md2rs() { 199 | core_test_md2rs(test_snippets::PRODIGAL5_MD, test_snippets::HARVEST5_RS); 200 | } 201 | 202 | #[test] 203 | fn test_prodigal5return_md2rs() { 204 | core_test_rs2md(test_snippets::HARVEST5_RS, test_snippets::RETURN5_MD); 205 | } 206 | 207 | #[test] 208 | fn test_hello6_metadata_md2rs() { 209 | core_test_md2rs(test_snippets::HELLO6_METADATA_MD, 210 | test_snippets::HELLO6_METADATA_RS); 211 | } 212 | 213 | #[test] 214 | fn test_hello6_metadata_rs2md() { 215 | core_test_rs2md(test_snippets::HELLO6_METADATA_RS, 216 | test_snippets::HELLO6_METADATA_MD); 217 | } 218 | 219 | #[test] 220 | fn test_hello7_link_to_play_md2rs() { 221 | core_test_md2rs(test_snippets::HELLO7_LINK_TO_PLAY_MD, 222 | test_snippets::HELLO7_LINK_TO_PLAY_RS); 223 | } 224 | 225 | #[test] 226 | fn test_hello7_link_to_play_rs2md() { 227 | core_test_rs2md(test_snippets::HELLO7_LINK_TO_PLAY_RS, 228 | test_snippets::HELLO7_LINK_TO_PLAY_MD); 229 | } 230 | 231 | #[test] 232 | fn test_hello8_link_to_play_md2rs() { 233 | core_test_md2rs(test_snippets::HELLO8_LINK_TO_PLAY_MD, 234 | test_snippets::HELLO8_LINK_TO_PLAY_RS); 235 | } 236 | 237 | #[test] 238 | fn test_hello8_link_to_play_rs2md() { 239 | core_test_rs2md(test_snippets::HELLO8_LINK_TO_PLAY_RS, 240 | test_snippets::HELLO8_LINK_TO_PLAY_MD); 241 | } 242 | 243 | #[test] 244 | fn test_hello9_link_to_play_md2rs_warn() { 245 | warn_test_md2rs(test_snippets::HELLO9_LINK_TO_PLAY_MD_WARN, 246 | test_snippets::HELLO9_LINK_TO_PLAY_RS); 247 | } 248 | 249 | #[test] 250 | fn test_hello10_link_to_play_eq_md2rs() { 251 | core_test_md2rs(test_snippets::HELLO10_LINK_TO_PLAY_EQ_MD, 252 | test_snippets::HELLO10_LINK_TO_PLAY_EQ_RS); 253 | } 254 | 255 | #[test] 256 | fn test_hello10_link_to_play_eq_rs2md() { 257 | core_test_rs2md(test_snippets::HELLO10_LINK_TO_PLAY_EQ_RS, 258 | test_snippets::HELLO10_LINK_TO_PLAY_EQ_MD); 259 | } 260 | 261 | #[test] 262 | fn test_hello11_link_to_play_md2rs() { 263 | core_test_md2rs(test_snippets::HELLO11_LINK_TO_PLAY_HTML_SEP_MD, 264 | test_snippets::HELLO11_LINK_TO_PLAY_HTML_SEP_RS); 265 | } 266 | 267 | #[test] 268 | fn test_hello11_link_to_play_rs2md() { 269 | core_test_rs2md(test_snippets::HELLO11_LINK_TO_PLAY_HTML_SEP_RS, 270 | test_snippets::HELLO11_LINK_TO_PLAY_HTML_SEP_MD); 271 | } 272 | 273 | #[test] 274 | fn test_hello12_link_to_play_md2rs() { 275 | core_test_md2rs(test_snippets::HELLO12_LINK_TO_PLAY_MARKDOWN_FOLLOW_MD, 276 | test_snippets::HELLO12_LINK_TO_PLAY_MARKDOWN_FOLLOW_RS); 277 | } 278 | 279 | #[test] 280 | fn test_hello12_link_to_play_rs2md() { 281 | core_test_rs2md(test_snippets::HELLO12_LINK_TO_PLAY_MARKDOWN_FOLLOW_RS, 282 | test_snippets::HELLO12_LINK_TO_PLAY_MARKDOWN_FOLLOW_MD); 283 | } 284 | -------------------------------------------------------------------------------- /src/testing/test_snippets.rs: -------------------------------------------------------------------------------- 1 | pub const ONE_TEXT_LINE_RS: &'static str = "//@ This is a demo without code."; 2 | pub const ONE_TEXT_LINE_MD: &'static str = "This is a demo without code."; 3 | 4 | pub const ONE_RUST_LINE_RS: &'static str = r#"fn main() { println!("one rust line"); }"#; 5 | 6 | pub const ONE_RUST_LINE_MD: &'static str = r#"```rust 7 | fn main() { println!("one rust line"); } 8 | ``` 9 | "#; 10 | 11 | pub const HELLO_RS: &'static str = r#"//@ # Hello World 12 | //@ This is a Hello World demo. 13 | 14 | // Code started here (at this normal comment) 15 | fn main() { println!("Hello World"); } 16 | //@ And then the text resumes here. 17 | "#; 18 | 19 | pub const HELLO_MD: &'static str = r#"# Hello World 20 | This is a Hello World demo. 21 | 22 | ```rust 23 | // Code started here (at this normal comment) 24 | fn main() { println!("Hello World"); } 25 | ``` 26 | And then the text resumes here. 27 | "#; 28 | 29 | pub const HELLO2_RS: &'static str = r#"//@ # Hello World 30 | //@ This is a second Hello World demo. 31 | 32 | // Code started here (at this normal comment) 33 | fn main() { println!("Hello World"); } 34 | 35 | //@ And then the text resumes here, after a line break. 36 | "#; 37 | 38 | pub const HELLO2_MD: &'static str = r#"# Hello World 39 | This is a second Hello World demo. 40 | 41 | ```rust 42 | // Code started here (at this normal comment) 43 | fn main() { println!("Hello World"); } 44 | ``` 45 | 46 | And then the text resumes here, after a line break. 47 | "#; 48 | 49 | pub const HELLO3_RS: &'static str = r#" 50 | 51 | // Code started here (at this normal comment) 52 | fn main() { hello() } 53 | 54 | //@ Here is some expository text in the middle 55 | //@ It spans ... 56 | //@ ... multiple lines 57 | 58 | // Here is yet more code! 59 | // (and we end with code, not doc) 60 | fn hello() { println!("Hello World"); } 61 | "#; 62 | 63 | pub const HELLO3_MD: &'static str = r#" 64 | 65 | ```rust 66 | // Code started here (at this normal comment) 67 | fn main() { hello() } 68 | ``` 69 | 70 | Here is some expository text in the middle 71 | It spans ... 72 | ... multiple lines 73 | 74 | ```rust 75 | // Here is yet more code! 76 | // (and we end with code, not doc) 77 | fn hello() { println!("Hello World"); } 78 | ``` 79 | "#; 80 | 81 | pub const HELLO4_MD: &'static str = r#"# Hello World 82 | Here is some expository text, but this one ... 83 | 84 | ... has a gap between its lines. 85 | "#; 86 | 87 | pub const HELLO4_RS: &'static str = r#"//@ # Hello World 88 | //@ Here is some expository text, but this one ... 89 | //@ 90 | //@ ... has a gap between its lines. 91 | "#; 92 | 93 | pub const PRODIGAL5_MD: &'static str = r#"# Hello World 94 | ```rust 95 | let code_fragment; 96 | ``` 97 | 98 | This looks like it has a nice para break before its starts, 99 | but note the tab 100 | "#; 101 | 102 | pub const HARVEST5_RS: &'static str = r#"//@ # Hello World 103 | let code_fragment; 104 | //@ 105 | //@ This looks like it has a nice para break before its starts, 106 | //@ but note the tab 107 | "#; 108 | 109 | pub const RETURN5_MD: &'static str = r#"# Hello World 110 | ```rust 111 | let code_fragment; 112 | ``` 113 | 114 | This looks like it has a nice para break before its starts, 115 | but note the tab 116 | "#; 117 | 118 | pub const HELLO6_METADATA_MD: &'static str = r#"# Hello World 119 | 120 | ```{.rust .css_class_metadata } 121 | // The question is, can we preserve the .css_class_metdata 122 | ``` 123 | "#; 124 | 125 | pub const HELLO6_METADATA_RS: &'static str = r#"//@ # Hello World 126 | 127 | //@@ { .css_class_metadata } 128 | // The question is, can we preserve the .css_class_metdata 129 | "#; 130 | 131 | pub const HELLO7_LINK_TO_PLAY_MD: &'static str = r#"# Hello World 132 | 133 | ```rust 134 | // 135 | ``` 136 | [hello7]: https://play.rust-lang.org/?code=%2F%2F&version=nightly 137 | "#; 138 | 139 | pub const HELLO7_LINK_TO_PLAY_RS: &'static str = r#"//@ # Hello World 140 | 141 | // 142 | //@@@ hello7 143 | "#; 144 | 145 | pub const HELLO8_LINK_TO_PLAY_MD: &'static str = r#"# Hello World 146 | 147 | ```rust 148 | // Here is some content 149 | fn main() { } 150 | ``` 151 | [hello8]: https://play.rust-lang.org/?code=%2F%2F%20Here%20is%20some%20content%0Afn%20main()%20%7B%20%7D&version=nightly 152 | "#; 153 | 154 | pub const HELLO8_LINK_TO_PLAY_RS: &'static str = r#"//@ # Hello World 155 | 156 | // Here is some content 157 | fn main() { } 158 | //@@@ hello8 159 | "#; 160 | 161 | pub const HELLO9_LINK_TO_PLAY_MD_WARN: &'static str = r#"# Hello World 162 | 163 | ```rust 164 | // Here is some content 165 | fn main() { } 166 | ``` 167 | [hello9]: https://play.rust-lang.org/?code=does_not_match&version=nightly 168 | "#; 169 | 170 | pub const HELLO9_LINK_TO_PLAY_RS: &'static str = r#"//@ # Hello World 171 | 172 | // Here is some content 173 | fn main() { } 174 | //@@@ hello9 175 | "#; 176 | 177 | pub const HELLO10_LINK_TO_PLAY_EQ_MD: &'static str = r#"# Hello World 178 | 179 | ```rust 180 | = 181 | ``` 182 | [hello10]: https://play.rust-lang.org/?code=%3D&version=nightly 183 | "#; 184 | 185 | pub const HELLO10_LINK_TO_PLAY_EQ_RS: &'static str = r#"//@ # Hello World 186 | 187 | = 188 | //@@@ hello10 189 | "#; 190 | 191 | pub const HELLO11_LINK_TO_PLAY_HTML_SEP_MD: &'static str = r#"# Hello World 192 | 193 | ```rust 194 | Hi 195 | ``` 196 |