diff options
| author | Federico Igne <git@federicoigne.com> | 2022-12-30 11:23:59 +0000 |
|---|---|---|
| committer | Federico Igne <git@federicoigne.com> | 2022-12-30 12:35:18 +0000 |
| commit | 909876999eb1eb968f71e6f8b199689cfc68f711 (patch) | |
| tree | d8f7080ebec3c7f1723c0ceb68ca39c63481533f | |
| parent | 6f8fa69e1ad8ad68c8d1afa455b414cd459bb498 (diff) | |
| download | pangler-909876999eb1eb968f71e6f8b199689cfc68f711.tar.gz pangler-909876999eb1eb968f71e6f8b199689cfc68f711.zip | |
fix: allow more flexible entrypoint definition
Entrypoints can now be forced by using the class `entry` or by
specifying an attribute `path`.
Compatibility with previous versions is maintained and any identifier
that actually looks like a file is considered as such.
| -rw-r--r-- | README.md | 78 | ||||
| -rw-r--r-- | src/main.rs | 45 |
2 files changed, 96 insertions, 27 deletions
| @@ -48,7 +48,32 @@ the language of the code snippet can be provided and is useful to enable correct | |||
| 48 | ``` | 48 | ``` |
| 49 | ~~~ | 49 | ~~~ |
| 50 | 50 | ||
| 51 | An identifier can also be a file name matching the following regex | 51 | ### Identifiers |
| 52 | |||
| 53 | An identifier can either be a string, representing a macro to be used inside other code blocks, or a filename. | ||
| 54 | In the latter, the code block will be considered a valid entry point for code generation. | ||
| 55 | |||
| 56 | ```{#types .rust} | ||
| 57 | #[derive(Eq, Hash, PartialEq)] | ||
| 58 | enum Key { | ||
| 59 | Macro(String), | ||
| 60 | Entry(PathBuf) | ||
| 61 | } | ||
| 62 | |||
| 63 | impl Key { | ||
| 64 | fn get_path(&self) -> Option<&PathBuf> { | ||
| 65 | match self { | ||
| 66 | Self::Entry(s) => Some(&s), | ||
| 67 | Self::Macro(_) => None | ||
| 68 | } | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | ``` | ||
| 73 | |||
| 74 | There are currently 3 (possibly overlapping) scenarios in which an identifier is considered a valid filename. | ||
| 75 | |||
| 76 | First, the filename matches the following regex | ||
| 52 | 77 | ||
| 53 | ```{#regex_path .rust} | 78 | ```{#regex_path .rust} |
| 54 | static ref PATH: Regex = | 79 | static ref PATH: Regex = |
| @@ -57,8 +82,7 @@ static ref PATH: Regex = | |||
| 57 | ).unwrap(); | 82 | ).unwrap(); |
| 58 | ``` | 83 | ``` |
| 59 | 84 | ||
| 60 | In that case the code block is considered a valid **entry point** for the generation of a file with that name. | 85 | For example |
| 61 | The code block defines the content of the new file. | ||
| 62 | 86 | ||
| 63 | ~~~ | 87 | ~~~ |
| 64 | ```{#file.py .python} | 88 | ```{#file.py .python} |
| @@ -66,8 +90,10 @@ The code block defines the content of the new file. | |||
| 66 | ``` | 90 | ``` |
| 67 | ~~~ | 91 | ~~~ |
| 68 | 92 | ||
| 69 | File names can be generated in subfolders using the `path` attribute. | 93 | Second, the code block contains a `path` attribute. |
| 70 | The following code block determines the content of file `path/to/file.py`. | 94 | With this feature, we can generated a file into a more complex folder structure. |
| 95 | |||
| 96 | For example, the following code block determines the content of file `path/to/file.py`. | ||
| 71 | 97 | ||
| 72 | ~~~ | 98 | ~~~ |
| 73 | ```{#file.py .python path="path/to/"} | 99 | ```{#file.py .python path="path/to/"} |
| @@ -77,16 +103,30 @@ The following code block determines the content of file `path/to/file.py`. | |||
| 77 | 103 | ||
| 78 | This path is relative to the current working directory, unless [the `-o`/`--output` flag is used](#command-line-interface). | 104 | This path is relative to the current working directory, unless [the `-o`/`--output` flag is used](#command-line-interface). |
| 79 | 105 | ||
| 106 | Third, the code contains the `entry` class. | ||
| 107 | This is useful when declaring an entry point that doesn't match any of the previous cases. | ||
| 108 | |||
| 109 | ~~~ | ||
| 110 | ```{#Dockerfile .dockerfile .entry} | ||
| 111 | [Docker directives] | ||
| 112 | ``` | ||
| 113 | ~~~ | ||
| 114 | |||
| 115 | Any ID that doesn't match any of the previous cases is considered an internal macro. | ||
| 80 | Code blocks without an ID are ignored. | 116 | Code blocks without an ID are ignored. |
| 81 | 117 | ||
| 82 | ```{#code_block_gathering .rust} | 118 | ```{#code_block_gathering .rust} |
| 83 | if !id.is_empty() { | 119 | if !id.is_empty() { |
| 84 | let key = { | 120 | let key = { |
| 85 | let path = attrs.iter().find(|(k,_)| k == "path"); | 121 | <<regex_path_lazy>> |
| 86 | if let Some(path) = path { | 122 | let entry = clss.contains(&String::from("entry")); |
| 87 | format!("{}{}", path.1, id) | 123 | let path = attrs |
| 124 | .into_iter() | ||
| 125 | .find_map(|(k,p)| if k == "path" { Some(p.clone()) } else { None }); | ||
| 126 | if entry || path.is_some() || PATH.is_match(id) { | ||
| 127 | Key::Entry(PathBuf::from(path.unwrap_or_default()).join(id)) | ||
| 88 | } else { | 128 | } else { |
| 89 | id.to_string() | 129 | Key::Macro(id.to_string()) |
| 90 | } | 130 | } |
| 91 | }; | 131 | }; |
| 92 | <<code_block>> | 132 | <<code_block>> |
| @@ -96,6 +136,8 @@ if !id.is_empty() { | |||
| 96 | } | 136 | } |
| 97 | ``` | 137 | ``` |
| 98 | 138 | ||
| 139 | ### Redefining code blocks | ||
| 140 | |||
| 99 | Code blocks are processed in order. | 141 | Code blocks are processed in order. |
| 100 | By default, if an identifier is already defined, the code block is appended to the current corresponding value. | 142 | By default, if an identifier is already defined, the code block is appended to the current corresponding value. |
| 101 | 143 | ||
| @@ -314,7 +356,7 @@ use std::collections::HashMap; | |||
| 314 | ``` | 356 | ``` |
| 315 | 357 | ||
| 316 | ```{#types .rust} | 358 | ```{#types .rust} |
| 317 | type Blocks<'a> = HashMap<String,Cow<'a,str>>; | 359 | type Blocks<'a> = HashMap<Key,Cow<'a,str>>; |
| 318 | ``` | 360 | ``` |
| 319 | 361 | ||
| 320 | Code blocks are wrapped into a [`Cow`](https://doc.rust-lang.org/stable/std/borrow/enum.Cow.html), i.e., a "copy-on-write" smart pointer, to avoid string duplication, unless strictly necessary. | 362 | Code blocks are wrapped into a [`Cow`](https://doc.rust-lang.org/stable/std/borrow/enum.Cow.html), i.e., a "copy-on-write" smart pointer, to avoid string duplication, unless strictly necessary. |
| @@ -366,7 +408,7 @@ In case we reach the maximum allowed depth we truncate code block substitution a | |||
| 366 | |caps: &Captures| { | 408 | |caps: &Captures| { |
| 367 | if current_depth < max_depth { | 409 | if current_depth < max_depth { |
| 368 | let block = blocks | 410 | let block = blocks |
| 369 | .get(&caps[2]) | 411 | .get(&Key::Macro(caps[2].to_string())) |
| 370 | .expect("Block not present") | 412 | .expect("Block not present") |
| 371 | .clone(); | 413 | .clone(); |
| 372 | indent(block, caps[1].len()) | 414 | indent(block, caps[1].len()) |
| @@ -387,10 +429,11 @@ fn build( | |||
| 387 | blocks: &Blocks, | 429 | blocks: &Blocks, |
| 388 | max_depth: u32 | 430 | max_depth: u32 |
| 389 | ) { | 431 | ) { |
| 390 | <<regex_definition>> | 432 | <<regex_macro_lazy>> |
| 391 | blocks | 433 | blocks |
| 392 | .iter() | 434 | .iter() |
| 393 | .for_each(|(path,code)| if PATH.is_match(path) { | 435 | .filter_map(|(key,code)| { key.get_path().map(|k| (k,code)) }) |
| 436 | .for_each(|(path,code)| { | ||
| 394 | <<code_generation>> | 437 | <<code_generation>> |
| 395 | }) | 438 | }) |
| 396 | } | 439 | } |
| @@ -506,11 +549,16 @@ use lazy_static::lazy_static; | |||
| 506 | use regex::{Captures,Regex}; | 549 | use regex::{Captures,Regex}; |
| 507 | ``` | 550 | ``` |
| 508 | 551 | ||
| 509 | We wrap the regex definition in a `lazy_static` macro | 552 | We wrap the regex definitions in a `lazy_static` macro |
| 510 | 553 | ||
| 511 | ```{#regex_definition .rust} | 554 | ```{#regex_path_lazy .rust} |
| 512 | lazy_static! { | 555 | lazy_static! { |
| 513 | <<regex_path>> | 556 | <<regex_path>> |
| 557 | } | ||
| 558 | ``` | ||
| 559 | |||
| 560 | ```{#regex_macro_lazy .rust} | ||
| 561 | lazy_static! { | ||
| 514 | <<regex_macro>> | 562 | <<regex_macro>> |
| 515 | } | 563 | } |
| 516 | ``` | 564 | ``` |
diff --git a/src/main.rs b/src/main.rs index 02fbcd4..7d2f786 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -27,7 +27,22 @@ struct Config { | |||
| 27 | input: Vec<PathBuf>, | 27 | input: Vec<PathBuf>, |
| 28 | } | 28 | } |
| 29 | 29 | ||
| 30 | type Blocks<'a> = HashMap<String,Cow<'a,str>>; | 30 | #[derive(Eq, Hash, PartialEq)] |
| 31 | enum Key { | ||
| 32 | Macro(String), | ||
| 33 | Entry(PathBuf) | ||
| 34 | } | ||
| 35 | |||
| 36 | impl Key { | ||
| 37 | fn get_path(&self) -> Option<&PathBuf> { | ||
| 38 | match self { | ||
| 39 | Self::Entry(s) => Some(&s), | ||
| 40 | Self::Macro(_) => None | ||
| 41 | } | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | type Blocks<'a> = HashMap<Key,Cow<'a,str>>; | ||
| 31 | 46 | ||
| 32 | fn build( | 47 | fn build( |
| 33 | base: &Option<PathBuf>, | 48 | base: &Option<PathBuf>, |
| @@ -35,10 +50,6 @@ fn build( | |||
| 35 | max_depth: u32 | 50 | max_depth: u32 |
| 36 | ) { | 51 | ) { |
| 37 | lazy_static! { | 52 | lazy_static! { |
| 38 | static ref PATH: Regex = | ||
| 39 | Regex::new( | ||
| 40 | r"^(?:[[:word:]\.-]+/)*[[:word:]\.-]+\.[[:alpha:]]+$" | ||
| 41 | ).unwrap(); | ||
| 42 | static ref MACRO: Regex = | 53 | static ref MACRO: Regex = |
| 43 | Regex::new( | 54 | Regex::new( |
| 44 | r"(?m)^([[:blank:]]*)<<([^>\s]+)>>" | 55 | r"(?m)^([[:blank:]]*)<<([^>\s]+)>>" |
| @@ -46,7 +57,8 @@ fn build( | |||
| 46 | } | 57 | } |
| 47 | blocks | 58 | blocks |
| 48 | .iter() | 59 | .iter() |
| 49 | .for_each(|(path,code)| if PATH.is_match(path) { | 60 | .filter_map(|(key,code)| { key.get_path().map(|k| (k,code)) }) |
| 61 | .for_each(|(path,code)| { | ||
| 50 | let mut current_depth = 0; | 62 | let mut current_depth = 0; |
| 51 | let mut code = code.clone(); | 63 | let mut code = code.clone(); |
| 52 | while let Cow::Owned(new_code) = MACRO.replace_all( | 64 | while let Cow::Owned(new_code) = MACRO.replace_all( |
| @@ -54,7 +66,7 @@ fn build( | |||
| 54 | |caps: &Captures| { | 66 | |caps: &Captures| { |
| 55 | if current_depth < max_depth { | 67 | if current_depth < max_depth { |
| 56 | let block = blocks | 68 | let block = blocks |
| 57 | .get(&caps[2]) | 69 | .get(&Key::Macro(caps[2].to_string())) |
| 58 | .expect("Block not present") | 70 | .expect("Block not present") |
| 59 | .clone(); | 71 | .clone(); |
| 60 | indent(block, caps[1].len()) | 72 | indent(block, caps[1].len()) |
| @@ -132,11 +144,20 @@ fn main() -> Result<()> { | |||
| 132 | if let Block::CodeBlock((id,clss,attrs), code) = block { | 144 | if let Block::CodeBlock((id,clss,attrs), code) = block { |
| 133 | if !id.is_empty() { | 145 | if !id.is_empty() { |
| 134 | let key = { | 146 | let key = { |
| 135 | let path = attrs.iter().find(|(k,_)| k == "path"); | 147 | lazy_static! { |
| 136 | if let Some(path) = path { | 148 | static ref PATH: Regex = |
| 137 | format!("{}{}", path.1, id) | 149 | Regex::new( |
| 150 | r"^(?:[[:word:]\.-]+/)*[[:word:]\.-]+\.[[:alpha:]]+$" | ||
| 151 | ).unwrap(); | ||
| 152 | } | ||
| 153 | let entry = clss.contains(&String::from("entry")); | ||
| 154 | let path = attrs | ||
| 155 | .into_iter() | ||
| 156 | .find_map(|(k,p)| if k == "path" { Some(p.clone()) } else { None }); | ||
| 157 | if entry || path.is_some() || PATH.is_match(id) { | ||
| 158 | Key::Entry(PathBuf::from(path.unwrap_or_default()).join(id)) | ||
| 138 | } else { | 159 | } else { |
| 139 | id.to_string() | 160 | Key::Macro(id.to_string()) |
| 140 | } | 161 | } |
| 141 | }; | 162 | }; |
| 142 | if clss.iter().any(|c| c == "override") { | 163 | if clss.iter().any(|c| c == "override") { |
| @@ -162,4 +183,4 @@ fn main() -> Result<()> { | |||
| 162 | ); | 183 | ); |
| 163 | pandoc.execute().unwrap(); | 184 | pandoc.execute().unwrap(); |
| 164 | Ok(()) | 185 | Ok(()) |
| 165 | } \ No newline at end of file | 186 | } |
