aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFederico Igne <git@federicoigne.com>2022-12-30 11:23:59 +0000
committerFederico Igne <git@federicoigne.com>2022-12-30 12:35:18 +0000
commit909876999eb1eb968f71e6f8b199689cfc68f711 (patch)
treed8f7080ebec3c7f1723c0ceb68ca39c63481533f
parent6f8fa69e1ad8ad68c8d1afa455b414cd459bb498 (diff)
downloadpangler-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.md78
-rw-r--r--src/main.rs45
2 files changed, 96 insertions, 27 deletions
diff --git a/README.md b/README.md
index 69ca732..5bd146e 100644
--- a/README.md
+++ b/README.md
@@ -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
51An identifier can also be a file name matching the following regex 51### Identifiers
52
53An identifier can either be a string, representing a macro to be used inside other code blocks, or a filename.
54In the latter, the code block will be considered a valid entry point for code generation.
55
56```{#types .rust}
57#[derive(Eq, Hash, PartialEq)]
58enum Key {
59 Macro(String),
60 Entry(PathBuf)
61}
62
63impl 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
74There are currently 3 (possibly overlapping) scenarios in which an identifier is considered a valid filename.
75
76First, the filename matches the following regex
52 77
53```{#regex_path .rust} 78```{#regex_path .rust}
54static ref PATH: Regex = 79static ref PATH: Regex =
@@ -57,8 +82,7 @@ static ref PATH: Regex =
57 ).unwrap(); 82 ).unwrap();
58``` 83```
59 84
60In that case the code block is considered a valid **entry point** for the generation of a file with that name. 85For example
61The 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
69File names can be generated in subfolders using the `path` attribute. 93Second, the code block contains a `path` attribute.
70The following code block determines the content of file `path/to/file.py`. 94With this feature, we can generated a file into a more complex folder structure.
95
96For 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
78This path is relative to the current working directory, unless [the `-o`/`--output` flag is used](#command-line-interface). 104This path is relative to the current working directory, unless [the `-o`/`--output` flag is used](#command-line-interface).
79 105
106Third, the code contains the `entry` class.
107This 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
115Any ID that doesn't match any of the previous cases is considered an internal macro.
80Code blocks without an ID are ignored. 116Code blocks without an ID are ignored.
81 117
82```{#code_block_gathering .rust} 118```{#code_block_gathering .rust}
83if !id.is_empty() { 119if !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
99Code blocks are processed in order. 141Code blocks are processed in order.
100By default, if an identifier is already defined, the code block is appended to the current corresponding value. 142By 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}
317type Blocks<'a> = HashMap<String,Cow<'a,str>>; 359type Blocks<'a> = HashMap<Key,Cow<'a,str>>;
318``` 360```
319 361
320Code 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. 362Code 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;
506use regex::{Captures,Regex}; 549use regex::{Captures,Regex};
507``` 550```
508 551
509We wrap the regex definition in a `lazy_static` macro 552We wrap the regex definitions in a `lazy_static` macro
510 553
511```{#regex_definition .rust} 554```{#regex_path_lazy .rust}
512lazy_static! { 555lazy_static! {
513 <<regex_path>> 556 <<regex_path>>
557}
558```
559
560```{#regex_macro_lazy .rust}
561lazy_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
30type Blocks<'a> = HashMap<String,Cow<'a,str>>; 30#[derive(Eq, Hash, PartialEq)]
31enum Key {
32 Macro(String),
33 Entry(PathBuf)
34}
35
36impl 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
45type Blocks<'a> = HashMap<Key,Cow<'a,str>>;
31 46
32fn build( 47fn 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}