├── src ├── process │ ├── mod.rs │ └── state.rs ├── parser │ ├── mod.rs │ ├── define_parser.rs │ └── arg_parser.rs ├── cli │ └── mod.rs ├── manual_src │ └── fun │ │ ├── split.r4d │ │ ├── after.r4d │ │ ├── slice.r4d │ │ ├── stripr.r4d │ │ ├── stripf.r4d │ │ ├── cut.r4d │ │ ├── sep.r4d │ │ ├── striprl.r4d │ │ ├── stripfl.r4d │ │ ├── strip.r4d │ │ ├── gt.r4d │ │ ├── gte.r4d │ │ ├── pipe.r4d │ │ ├── align.r4d │ │ ├── eq.r4d │ │ ├── alignby.r4d │ │ └── PS.r4d ├── map │ ├── mod.rs │ ├── anon_map.rs │ ├── sigmap.rs │ ├── hookmap.rs │ ├── runtime_map.rs │ └── macro_map.rs ├── debug_help_message.txt ├── bin │ ├── rad.rs │ └── rado.rs ├── test.rs ├── wasm.rs ├── package.rs ├── storage.rs ├── extension.rs ├── script.rs ├── lib.rs ├── consts.rs ├── auth.rs ├── formatter.rs ├── error.rs ├── lexor.rs └── common.rs ├── docs ├── issues.md ├── 3_2_roadmap.md ├── index.md ├── similar.md ├── update_indices.r4d ├── format_comment.md ├── 3_0_hassle.md ├── bugs_to_handle.md ├── storage.md ├── code_manipulation.md ├── milestone.md ├── modes.md ├── debugger_refactor.md ├── file_operation.md ├── newline_rules.md ├── 3_0.md ├── ext.md ├── macro_types.md ├── change.md ├── r4d_internal.md ├── usage.md ├── debug.md └── macro_syntax.md ├── .gitignore ├── LICENSE ├── Cargo.toml └── README.md /src/process/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for processing related structs and enums 2 | 3 | mod processor; 4 | mod state; 5 | pub use processor::Processor; 6 | pub(crate) use state::ProcessorState; 7 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | //! Parser module for multiple text formats 2 | 3 | mod arg_parser; 4 | mod define_parser; 5 | 6 | pub use arg_parser::{ArgParser, SplitVariant}; 7 | pub use define_parser::DefineParser; 8 | -------------------------------------------------------------------------------- /docs/issues.md: -------------------------------------------------------------------------------- 1 | # Known issues 2 | 3 | - Include doesn't respect pipe attributes 4 | - User configured macro's name is not printed in logs 5 | - Parsing rule is not working properly on complicated escaped rules 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | auto.sh 3 | cache.txt 4 | compiled.r4c 5 | diff.out 6 | diff.src 7 | err.txt 8 | def.r4d 9 | nested.r4d 10 | lib.r4f 11 | pkg 12 | test 13 | wasm 14 | mac.r4d 15 | addr.csv 16 | sig.json 17 | -------------------------------------------------------------------------------- /docs/3_2_roadmap.md: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | For every candiate upgrade, macros and new features might be added by 4 | situations + some bug fixes. 5 | 6 | # 3.2-rc 1. 7 | # 3.2-rc 2. 8 | # 3.2-rc 3. 9 | # 3.2-rc 4. 10 | # 3.2-rc 5. 11 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Cli module 2 | //! Cli module takes care of command line argument parsing and executing branches accordingly 3 | //! 4 | //! Cli module is only included in binary feature flag. 5 | 6 | mod cli_rad; 7 | mod cli_rado; 8 | pub use cli_rad::RadCli; 9 | pub use cli_rado::RadoCli; 10 | -------------------------------------------------------------------------------- /src/manual_src/fun/split.r4d: -------------------------------------------------------------------------------- 1 | Split text into an array 2 | 3 | # Arguments 4 | 5 | - a_sep : A separator string 6 | - a_text : Text to split 7 | 8 | # Demo 9 | 10 | ``` 11 | $split(_,I _have_ a_ many_ underbar_ in_ me) 12 | === 13 | I ,have, a, many, underbar, in, me 14 | ``` 15 | 16 | # Example 17 | 18 | $assert(\\*a,b,c*\\,$split(/,a/b/c)) 19 | -------------------------------------------------------------------------------- /src/manual_src/fun/after.r4d: -------------------------------------------------------------------------------- 1 | Get a substring after a pattern. This is not a regex but an exact match finder. 2 | 3 | # Arguments 4 | 5 | - a_pattern : A pattern to find 6 | - a_content : Content to get a sub string 7 | 8 | # Demo 9 | 10 | ``` 11 | $after(goal : ,My goal : Complete a project) 12 | === 13 | Complete a project 14 | ``` 15 | 16 | # Example 17 | 18 | $assert(world,$after($space(),Hello world)) 19 | -------------------------------------------------------------------------------- /src/manual_src/fun/slice.r4d: -------------------------------------------------------------------------------- 1 | Get a slice from an array 2 | 3 | # Arguments 4 | 5 | - a_min : A start index ( trimmed ) 6 | - a_max : A end index ( trimmed ) 7 | - a_array : An array to process 8 | 9 | # Demo 10 | 11 | ``` 12 | % This may look very confusing because it is 13 | $slice(0,3,1,2,3,4) 14 | === 15 | 1,2,3,4 16 | ``` 17 | 18 | # Example 19 | 20 | $assert(\\*2,3*\\,$slice(1,2,1,2,3,4,5,6)) 21 | -------------------------------------------------------------------------------- /src/manual_src/fun/stripr.r4d: -------------------------------------------------------------------------------- 1 | Strip from rear 2 | 3 | # Arguments 4 | 5 | - a_count : Count of characters to strip ( trimmed ) 6 | - a_content : Content to strip 7 | 8 | # Demo 9 | 10 | ``` 11 | % Use raw variant to prevent trimming 12 | $staticr(has_nl_at_end,Long text $nl()) 13 | $stripr(2,$has_nl_at_end()) 14 | === 15 | Long text 16 | ``` 17 | 18 | # Example 19 | 20 | $assert(Hmp,$stripr(2,Hmp::)) 21 | -------------------------------------------------------------------------------- /src/manual_src/fun/stripf.r4d: -------------------------------------------------------------------------------- 1 | Strip from front 2 | 3 | # Arguments 4 | 5 | - a_count : Count of characters to strip ( trimmed ) 6 | - a_content : Content to strip 7 | 8 | # Demo 9 | 10 | ``` 11 | $static(header_level,4) 12 | $mie(header_level,+1) 13 | $stripf($header_level(),#### I'm header four) 14 | === 15 | I'm header four 16 | ``` 17 | 18 | # Example 19 | 20 | $assert(List item,$stripf(2,- List item)) 21 | -------------------------------------------------------------------------------- /src/manual_src/fun/cut.r4d: -------------------------------------------------------------------------------- 1 | Split text into an array to get a value 2 | 3 | # Arguments 4 | 5 | - a_sep : A separator string 6 | - a_index : An index to cut out 7 | - a_text : Text to split 8 | 9 | # Demo 10 | 11 | ``` 12 | $syscmd|(ls -lah LICENSE) 13 | $static(size , $cut-($space(),1) ) 14 | File size is $size() 15 | === 16 | File size is 1 17 | ``` 18 | 19 | # Example 20 | 21 | $assert(b,$cut(/,-2,a/b/c)) 22 | -------------------------------------------------------------------------------- /src/manual_src/fun/sep.r4d: -------------------------------------------------------------------------------- 1 | Separate content. This technically doubles newlines. 2 | 3 | # Arguments 4 | 5 | - a_content : Content to separate 6 | 7 | # Demo 8 | 9 | ``` 10 | $sep(Tightly 11 | packed 12 | contents 13 | look depressing) 14 | === 15 | Tightly 16 | 17 | packed 18 | 19 | contents 20 | 21 | look depressing 22 | 23 | ``` 24 | 25 | # Example 26 | 27 | $assert(4,$countl($sep(1 28 | 2))) 29 | 30 | $sep(1 31 | 2) 32 | -------------------------------------------------------------------------------- /src/manual_src/fun/striprl.r4d: -------------------------------------------------------------------------------- 1 | Strip rear lines from text 2 | 3 | # Arguments 4 | 5 | - a_count : Count of lines to strip ( trimmed ) 6 | - a_content : Content to strip 7 | 8 | # Demo 9 | 10 | ``` 11 | % Strip examples from manual 12 | % Although not all example is a single line 13 | $striprl|(4,$syscmd(rad -M strip)) 14 | $countl-() 15 | === 16 | 22 17 | ``` 18 | 19 | # Example 20 | 21 | $assert(a$nl()b,$stripfl(1,c$nl()a$nl()b)) 22 | -------------------------------------------------------------------------------- /src/manual_src/fun/stripfl.r4d: -------------------------------------------------------------------------------- 1 | Strip front lines from text 2 | 3 | # Arguments 4 | 5 | - a_count : Count of lines to strip ( trimmed ) 6 | - a_content : Content to strip 7 | 8 | # Demo 9 | 10 | ``` 11 | $syscmd|(top -b1 -n1) 12 | 13 | $static(header_lines_from_top,9) 14 | $rename(header_lines_from_top,hlt) 15 | 16 | $countl( $stripfl-($hlt()) ) 17 | === 18 | 221 19 | ``` 20 | 21 | # Example 22 | 23 | $assert(a$nl()b,$stripfl(1,c$nl()a$nl()b)) 24 | -------------------------------------------------------------------------------- /src/manual_src/fun/strip.r4d: -------------------------------------------------------------------------------- 1 | Strip surrounding characters from text 2 | 3 | # Arguments 4 | 5 | - a_count : Count of characters to strip ( trimmed ) 6 | - a_content : Content to strip 7 | 8 | # Demo 9 | 10 | ``` 11 | $static(screen_width,10) 12 | $static(border_padding,2) 13 | 14 | $repeat|($screen_width(),~) 15 | $strip-($border_padding()) 16 | === 17 | ~~~~~~ % From 10 to 6 18 | ``` 19 | 20 | # Example 21 | 22 | $assert(llo Wor,$strip(2,Hello World)) 23 | -------------------------------------------------------------------------------- /src/manual_src/fun/gt.r4d: -------------------------------------------------------------------------------- 1 | Check if left value is greater than right value 2 | 3 | Consider using eval macro if you're trying mathematical evaluation. 4 | 5 | # Return : Boolean 6 | 7 | # Arguments 8 | 9 | - a_lvalue : A left value to compare 10 | - a_rvalue : A right value to compare 11 | 12 | # Demo 13 | 14 | ``` 15 | $static(item_count,5) 16 | $if($gt($item_count(),3),Yeah) 17 | === 18 | Yeah 19 | ``` 20 | 21 | # Example 22 | 23 | $assert(true,$gt(c,b)) 24 | $assert(false,$gt(text,text)) 25 | -------------------------------------------------------------------------------- /src/manual_src/fun/gte.r4d: -------------------------------------------------------------------------------- 1 | Check if left value is greater than or equal to right value 2 | 3 | Consider using eval macro if you're trying mathematical evaluation. 4 | 5 | # Return : Boolean 6 | 7 | # Arguments 8 | 9 | - a_lvalue : A left value to compare 10 | - a_rvalue : A right value to compare 11 | 12 | # Demo 13 | 14 | ``` 15 | $static(item_count,5) 16 | $if($gte($item_count(),5),Yeah) 17 | === 18 | Yeah 19 | ``` 20 | 21 | # Example 22 | 23 | $assert(true,$gte(c,b)) 24 | $assert(true,$gte(text,text)) 25 | -------------------------------------------------------------------------------- /src/manual_src/fun/pipe.r4d: -------------------------------------------------------------------------------- 1 | Gets a piped value. This truncates an original value by default if not 2 | configured as other. 3 | 4 | Pipe can be used as a temporary container and doesn't get affected by hygiene 5 | mode. 6 | 7 | # Arguments 8 | 9 | - a_pipe_name : A name of pipe target ( trimmed, optional ) 10 | 11 | # Demo 12 | 13 | ``` 14 | $pipe(abc) % Pipes to value 15 | $-() % This prints piped value which is [abc] in this case 16 | $-() % Pipe gets truncated thus nothing is printed. 17 | ``` 18 | 19 | # Exmaple 20 | 21 | $pipe(Hello) 22 | $assert(Hello,$-()) 23 | -------------------------------------------------------------------------------- /src/manual_src/fun/align.r4d: -------------------------------------------------------------------------------- 1 | Align texts with a character filler 2 | 3 | # Arguments 4 | 5 | - a_type : Types of alignment [\"Left\", \"right\", \"center\"] ( trimmed ) 6 | - a_width : Total width of aligned chunk [ Unsigned integer ] ( trimmed ) 7 | - a_fill : A character to fill ( trimmed ) 8 | - a_text : Text to align 9 | 10 | # Demo 11 | 12 | ``` 13 | <-$align(center,18,^,HEADER)-> 14 | === 15 | <-^^^^^^HEADER^^^^^^-> 16 | ``` 17 | 18 | # Example 19 | 20 | $assert(Hello***,$align(left ,8,*,Hello)) 21 | $assert(***Hello,$align(right ,8,*,Hello)) 22 | $assert(**Hello*,$align(center,8,*,Hello)) 23 | -------------------------------------------------------------------------------- /src/map/mod.rs: -------------------------------------------------------------------------------- 1 | //! Map includes multiple data collection map 2 | //! 3 | //! 4 | //! Macro map is a main regulator with following contents 5 | //! 6 | //! - anon_map 7 | //! - deterred_map 8 | //! - function_map 9 | //! - hook_map 10 | //! - runtime_map 11 | //! - sig_map 12 | 13 | pub mod anon_map; 14 | pub mod deterred_map; 15 | pub mod deterred_map_impl; 16 | pub mod function_map; 17 | pub mod function_map_impl; 18 | mod macro_map; 19 | pub(crate) use macro_map::MacroMap; 20 | #[cfg(feature = "hook")] 21 | pub mod hookmap; 22 | pub mod runtime_map; 23 | #[cfg(feature = "signature")] 24 | pub mod sigmap; 25 | -------------------------------------------------------------------------------- /src/manual_src/fun/eq.r4d: -------------------------------------------------------------------------------- 1 | Check if given two values are same 2 | 3 | Consider using eval macro if you're trying mathematical evaluation. 4 | 5 | # Return : Boolean 6 | 7 | # Arguments 8 | 9 | - a_lvalue : A left value to compare 10 | - a_rvalue : A right value to cmpare 11 | 12 | # Demo 13 | 14 | ``` 15 | $static(lv,$eval(20*3)) % Left value 16 | $static(rv,$eval(40+20)) % Right value 17 | $if^( 18 | $eq($lv(),$rv()), % This is same with $eval( $lv() = $rv() ) 19 | Wow so equal 20 | ) 21 | === 22 | Wow so equal 23 | ``` 24 | 25 | # Example 26 | 27 | $assert(false,$eq(a,b)) 28 | $assert(true,$eq(23,23)) 29 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | ### [Detailed usage](usage.md) 4 | 5 | ### [Full macro list](macro_indices.md) 6 | 7 | ### [Macro syntax](macro_syntax.md) 8 | 9 | ### [About modes](modes.md) 10 | 11 | ### [Macro types](macro_types.md) 12 | 13 | ### [About file operation](file_operation.md) 14 | 15 | ### [Macro files manipulation](code_manipulation.md) 16 | 17 | ### [How to extend function macros](ext.md) 18 | 19 | ### [Extend a processor with storage feature](storage.md) 20 | 21 | ### [How to debug](debug.md) 22 | 23 | ### [Understadning surplus newlines](newline_rules.md) 24 | 25 | ### [R4d internal logics](r4d_internal.md) 26 | 27 | ### [R4d milestones](milestone.md) 28 | -------------------------------------------------------------------------------- /src/debug_help_message.txt: -------------------------------------------------------------------------------- 1 | 2 | help(h) : Print this help 3 | next(n,\n) : Next line 4 | macro(m) : Next macro 5 | step(s) : Next macro including nested 6 | until(u) : Next macro but before invocation 7 | continue(c) : Next break point 8 | clear(cl) : Clear terminal 9 | print(p) : Print variable 10 | 11 | - name(n) : Print current macro name 12 | - arg(a) : Print current macro's processed argument 13 | - raw(r) : Print current macro's raw, unprocessed argument 14 | - text(t) : Print current line text 15 | - line(l) : Print current line number 16 | - span(s) : Print span of lines until the line 17 | -------------------------------------------------------------------------------- /docs/similar.md: -------------------------------------------------------------------------------- 1 | # Add similarity algorithm for... 2 | 3 | 1. Invalid macro execution -> SUggest most similar macro name 4 | 2. Invalid macro manual -> Also SUggest most similar macro name 5 | 6 | # TODO 7 | 8 | * [ ] Learn about similar string algorithm 9 | * [ ] Add a search algorithm for manual command 10 | 11 | ``` 12 | ~Reference~ 13 | https://yassineelkhal.medium.com/the-complete-guide-to-string-similarity-algorithms-1290ad07c6b7 14 | ``` 15 | 16 | # Example 17 | 18 | ```shell 19 | rad --man evaluation 20 | --> There is no macro named as "evaluation". Did you mean "eval"? 21 | ``` 22 | This would be really helpful. 23 | 24 | Or even better, create a new command search 25 | ```shell 26 | rad --search evaluation 27 | --- 28 | Macro name: eval 29 | Short desc: blah blah blah 30 | 31 | Macro name: evalk 32 | Short desc: blah blah blah 33 | --- 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/update_indices.r4d: -------------------------------------------------------------------------------- 1 | $strict() 2 | $require(fin,fout) 3 | 4 | $define(get_name,a_text=$index^(1,$split(:,$a_text()))) 5 | $define(make_toc,a_item=* [$a_item()](#$a_item())) 6 | 7 | $define=( 8 | make_header, 9 | a_chunk= 10 | $let( 11 | t_line, 12 | $indexl(1, $a_chunk()) 13 | ) 14 | $let(macro_name,$index(1,$split^(:,$t_line()))) 15 | ### $macro_name() 16 | ) 17 | 18 | $define=(markify, 19 | a_man= 20 | $sep($until(>>,$a_man())) 21 | >> 22 | $after(>>,$a_man()) 23 | ) 24 | 25 | $static(function_names,$grepf(Macro Name.*,macro.man)) 26 | $dump(macro_indices.md) 27 | $relay(file,macro_indices.md) 28 | # TOC 29 | $forline=( 30 | $make_toc($get_name($:())), 31 | $function_names() 32 | ) 33 | 34 | # Macros 35 | 36 | $forby($make_header($:^())$nl()$markify($:()),----,$include*(macro.man,true)) 37 | $halt() 38 | -------------------------------------------------------------------------------- /src/bin/rad.rs: -------------------------------------------------------------------------------- 1 | //! Rad is a main binary executable for processing 2 | 3 | use r4d::RadResult; 4 | 5 | /// Main entry for rad binary 6 | pub fn main() -> RadResult<()> { 7 | // Enable color on pager such as "less" 8 | // by overloading color related environment 9 | #[cfg(feature = "color")] 10 | colored::control::set_override(true); 11 | 12 | // Command line parse 13 | #[cfg(feature = "basic")] 14 | { 15 | use r4d::RadCli; 16 | use std::io::Write; 17 | let mut cli = RadCli::new(); 18 | if let Err(err) = cli.parse() { 19 | cli.print_error(&err.to_string())?; 20 | writeln!( 21 | std::io::stderr(), 22 | "Int: Rad panicked with unrecoverable error." 23 | )?; 24 | std::process::exit(1); 25 | } 26 | } 27 | 28 | // End 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /docs/format_comment.md: -------------------------------------------------------------------------------- 1 | # How would one implement format comment macro 2 | 3 | Suppose I have an example like following 4 | 5 | Hello world % This is comment 6 | Hello Sui % This is comment 7 | 8 | I want to make this example into next aligned one 9 | 10 | Hello world % This is comment 11 | Hello Sui % This is comment 12 | 13 | Frankly speaking, there is no such thing as easy way of doing things. 14 | 15 | Let's do some crude yet functional proto-typing of logics. 16 | 17 | 1. Save lines into a vector. 18 | 2. Set value max width as default value (0) 19 | 3. Set container for line blank offset and pattern index 20 | 3. Find a value, pattern. If exists, update max width. If not, simply pass 21 | 5. After the whole iteration -> 22 | 1. Split a line into two &str ( first, pattern after ) 23 | 2. Lengthen first part with empty blanks 24 | 3. Attach pattern after 25 | 4. Print to text 26 | -------------------------------------------------------------------------------- /src/bin/rado.rs: -------------------------------------------------------------------------------- 1 | //! Rado is a wrapper binary executable for a processing 2 | //! 3 | //! Rado provides multiple ergonomic functionalities 4 | 5 | use r4d::RadResult; 6 | 7 | /// Main entry 8 | pub fn main() -> RadResult<()> { 9 | // Enable color on pager such as "less" 10 | // by overloading color related environment 11 | #[cfg(feature = "color")] 12 | colored::control::set_override(true); 13 | 14 | // Command line parse 15 | #[cfg(feature = "basic")] 16 | { 17 | use r4d::RadoCli; 18 | use std::io::Write; 19 | if let Err(content) = RadoCli::new().parse() { 20 | writeln!(std::io::stderr(), "{}", content)?; 21 | writeln!( 22 | std::io::stderr(), 23 | "Int: Rad panicked with unrecoverable error." 24 | )?; 25 | std::process::exit(1); 26 | } 27 | } 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /docs/3_0_hassle.md: -------------------------------------------------------------------------------- 1 | ### 3.0 hassle 2 | 3 | The reason 3.0 was publshed so preemptively was because I'm having some 4 | individual hardship these days. Side project maintenance is becoming a burden 5 | on current state, and I wanted to end a milestone at least giving complete 6 | features for those who were waiting. Consuquently I missed one of the most 7 | important feature for version 3.0. I tried to keep the project to comply with 8 | semantic version as much as possible but looks like this will be an exception. 9 | 10 | ### 3.0.1 will be new stable release 11 | 12 | 3.0.1 version will be treated as real 3.0 version release. And 3.0 version will 13 | not work same as upcoming versions. 14 | 15 | The biggest update from 3.0 is argument parsing behaviour with related macro 16 | name changes. 17 | 18 | ### R4d will have long term update 19 | 20 | There will be bug fixes but feature update will be seldomly applied. 21 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use crate::{RadError, RadResult, RadStorage, StorageOutput, StorageResult}; 2 | use std::io::Write; 3 | 4 | pub struct TestStorage; 5 | 6 | impl RadStorage for TestStorage { 7 | fn update(&mut self, args: &[String]) -> crate::StorageResult<()> { 8 | match args[0].as_str() { 9 | "err" => return StorageResult::Err(Box::new(RadError::Interrupt)), 10 | _ => return StorageResult::Ok(()), 11 | } 12 | } 13 | 14 | fn extract(&mut self, serialize: bool) -> crate::StorageResult> { 15 | StorageResult::Ok(None) 16 | } 17 | } 18 | 19 | #[test] 20 | fn function_name_test() -> RadResult<()> { 21 | use crate::Processor; 22 | let mut processor = Processor::new(); 23 | processor.add_static_rules(&[("test", "")])?; 24 | writeln!(std::io::stdout(), "{}", processor.get_static("test")?); 25 | processor.replace_macro("test", "WOWZER"); 26 | writeln!(std::io::stdout(), "{}", processor.get_static("test")?); 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /src/manual_src/fun/alignby.r4d: -------------------------------------------------------------------------------- 1 | Align lines by a separator 2 | 3 | # Arguments 4 | 5 | - a_separator : A separator string 6 | - a_lines : Lines to align 7 | 8 | # Demo 9 | 10 | ``` 11 | $alignby^(//, 12 | let mut processor = Processor::new() // Make a processor 13 | .lenient(true) // Disable strict mode 14 | .hygiene(Hygiene::Macro) // Enable hygiene mode 15 | .pipe_truncate(false) // Disable pipe truncate 16 | .write_to_file(Path::new("out.txt"))? // default is stdout) 17 | === 18 | let mut processor = Processor::new() // Make a processor 19 | .lenient(true) // Disable strict mode 20 | .hygiene(Hygiene::Macro) // Enable hygiene mode 21 | .pipe_truncate(false) // Disable pipe truncate 22 | .write_to_file(Path::new("out.txt"))? // default is stdout 23 | ``` 24 | 25 | # Example 26 | 27 | $assert=( 28 | First %% wow 29 | Second part %% bob 30 | Thirdos %% Howzer, 31 | $alignby^=( 32 | %%, 33 | First %% wow 34 | Second part %% bob 35 | Thirdos %% Howzer 36 | 37 | -------------------------------------------------------------------------------- /src/wasm.rs: -------------------------------------------------------------------------------- 1 | use crate::{Processor, RadError, WriteOption}; 2 | use console_error_panic_hook; 3 | use wasm_bindgen::prelude::*; 4 | 5 | type WasmResult = Result; 6 | 7 | impl From for JsValue { 8 | fn from(err: RadError) -> Self { 9 | JsValue::from_str(&err.to_string()) 10 | } 11 | } 12 | 13 | #[wasm_bindgen] 14 | pub struct RadProcessor { 15 | processor: Processor<'static>, 16 | } 17 | 18 | #[wasm_bindgen] 19 | impl RadProcessor { 20 | #[wasm_bindgen(constructor)] 21 | pub fn new() -> Self { 22 | console_error_panic_hook::set_once(); 23 | 24 | let mut processor = Processor::new() 25 | .lenient(true) 26 | .hygiene(crate::Hygiene::Input); 27 | processor.set_write_option(WriteOption::Return); 28 | 29 | Self { processor } 30 | } 31 | 32 | pub fn process_string(&mut self, src: &str) -> WasmResult { 33 | let ret = self 34 | .processor 35 | .process_string(None, src)? 36 | .unwrap_or(String::new()); 37 | Ok(ret) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 simon creek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/map/anon_map.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::DefineParser; 2 | use crate::runtime_map::RuntimeMacro; 3 | use crate::{RadError, RadResult}; 4 | 5 | #[derive(Default)] 6 | pub(crate) struct AnonMap { 7 | macros: Vec, 8 | } 9 | 10 | impl AnonMap { 11 | pub fn new() -> Self { 12 | Self::default() 13 | } 14 | 15 | pub fn new_macro(&mut self, body: &str) -> RadResult<()> { 16 | let mut full_body = "anon,".to_string(); 17 | full_body.push_str(body); 18 | 19 | let (_, arg, body) = DefineParser::new() 20 | .parse_define(&full_body) 21 | .ok_or_else(|| { 22 | RadError::InvalidMacroDefinition( 23 | "Invalid definition for anonymous macro".to_string(), 24 | ) 25 | })?; 26 | let rt_macro = RuntimeMacro::new("anon", &arg, &body, false); 27 | self.macros.push(rt_macro); 28 | Ok(()) 29 | } 30 | 31 | pub fn get_anon(&self) -> Option<&RuntimeMacro> { 32 | self.macros.last() 33 | } 34 | 35 | pub fn clear(&mut self) { 36 | self.macros.clear(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/manual_src/fun/PS.r4d: -------------------------------------------------------------------------------- 1 | Return a platform specific path separator 2 | 3 | - On windows, this returns '\\' 4 | - On non windows, this returns '/' 5 | 6 | # Note 7 | 8 | If you want to join a file path consider using a 'path' macro. 9 | If you want to get a file name use a 'name' macro. 10 | If you want to get a parent path name use a 'parent' macro. 11 | 12 | This macro is usable in a situation where a path is given by input and it has 13 | to be processed into a platform specific path dynamically without knowing which 14 | platform the processor resides in. 15 | 16 | # Demo 17 | 18 | $PS() % Either '\\' or '/' 19 | 20 | ``` 21 | $static(win_path,0) % Windows split path container 22 | $static(unix_path,0) % unix split path container 23 | $static(given_path,a/b/c/d) % user given path 24 | $static(s_path,$split(\\,$given_path())) % Split path, Use two escapes 25 | 26 | $ifelse=( 27 | $eq!(0,$len($s_path())), 28 | $path($PS(),$s_path()), 29 | $regex(/,$given_path(),$PS()) 30 | ) 31 | === 32 | % rad test --comment any | rad --stream-chunk comp 33 | /a/b/c/d 34 | ``` 35 | 36 | # Example 37 | 38 | $assert(c,$cut($PS(),-1,a/b/c)) 39 | -------------------------------------------------------------------------------- /docs/bugs_to_handle.md: -------------------------------------------------------------------------------- 1 | # NOT YET 2 | 3 | 1. There is something not working in mac.r4d but not reproducible in the folder... what? 4 | - After process exited, logger panics and I don't know why. 5 | - What I tried but no success 6 | - Simple exit call 7 | - Exit call inside if macro 8 | 9 | 2. * [ ] Fix parsing rule 10 | * [ ] Parsing rule conflicts with some regex expressions 11 | ``` 12 | $regexpr(in_paren,\\(([^)]+)\\)) 13 | ``` 14 | 15 | * [ ] Assert doesn't check value correctly. Yet this looks like parsing 16 | error. No.. 17 | ``` 18 | $assert(\*(*\,\*(*\) 19 | = Assert requires two arguments 20 | ``` 21 | 22 | 3. * [-] Dryrun doesn't print log positions well. weird... 23 | - Simple demo doesn't reproduce this... what? 24 | 25 | # SOLVED 26 | 27 | # POSSIBLY SOLVED 28 | 29 | 1. Counter was acting strange. 30 | 31 | `args.is_empty()` -> THis line leaves empty string with 1 sized vector 32 | For e.g. counter emits very peculiar error when no argumen was given. Fix all 33 | of them. God damn it. 34 | I changed parsing logic. Yet keep an eye for this variants. 35 | 36 | 2. * [-] Dryrun doesn't print log positions well. weird... 37 | - Simple demo doesn't reproduce this... what? 38 | 39 | 3. 40 | -------------------------------------------------------------------------------- /docs/storage.md: -------------------------------------------------------------------------------- 1 | ### Storage 2 | 3 | Storage features provdies a single trait of ```RadStorage``` which implements 4 | ```update``` and ```extract```. 5 | 6 | You can call update and extract method with update and extract macro 7 | respectively. 8 | 9 | #### Example 10 | 11 | You need to import [Serde](https://github.com/serde-rs/serde) in your proejct 12 | for stroage. 13 | 14 | Create storage struct 15 | 16 | ```rust 17 | use serde::{Serialize,Deserialize}; 18 | use r4d::{StorageOutput, StorageResult}; 19 | 20 | #[derive(Serialize,Deserialize)] 21 | pub struct PlotModel { 22 | // informations 23 | ... 24 | } 25 | 26 | impl RadStorage for PlotModel { 27 | fn update(&mut self, args : &[String]) -> StorageResult<()> { 28 | // Update logics 29 | } 30 | 31 | // $extract() macro calls extract method with the serialize value of "false" 32 | fn extract(&mut self, serialize: bool) -> StorageResult> { 33 | if serialize { 34 | Ok(Some(StorageOutput::Binary(bincode::serialize(self)?))) 35 | } else { 36 | Ok(Some(StorageOutput::Text(serde_json::to_string(self)?))) 37 | } 38 | } 39 | } 40 | ``` 41 | And add storage to processor before processing. 42 | 43 | ```rust 44 | processor.set_storage(Box::new(PlotModel::default())); 45 | ``` 46 | -------------------------------------------------------------------------------- /src/package.rs: -------------------------------------------------------------------------------- 1 | //! Packaging is about creating a executable script and executing it. 2 | 3 | use crate::{Processor, RadResult}; 4 | use flate2::bufread::GzDecoder; 5 | use flate2::write::GzEncoder; 6 | use flate2::Compression; 7 | use serde::{Deserialize, Serialize}; 8 | use std::io::prelude::*; 9 | 10 | /// Statically packaged (or scriptable) script 11 | #[derive(Serialize, Deserialize)] 12 | pub(crate) struct StaticScript { 13 | pub header: Vec, 14 | pub body: Vec, 15 | } 16 | 17 | impl StaticScript { 18 | /// Create a new instance 19 | pub fn new(processor: &Processor, body: Vec) -> RadResult { 20 | let header = processor.serialize_rules()?; 21 | Ok(Self { header, body }) 22 | } 23 | 24 | /// Unpack binary input into a static script 25 | pub fn unpack(source: Vec) -> RadResult { 26 | let mut decompressed = Vec::new(); 27 | let mut decoder = GzDecoder::new(&source[..]); 28 | decoder.read_to_end(&mut decompressed).unwrap(); 29 | let object = bincode::deserialize::(&decompressed[..]).unwrap(); 30 | Ok(object) 31 | } 32 | 33 | /// Package static script into binary bytes 34 | pub fn package(&mut self, file: Option<&std::path::Path>) -> RadResult> { 35 | let serialized = bincode::serialize(&self).unwrap(); 36 | let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); 37 | encoder.write_all(&serialized).unwrap(); 38 | let something = encoder.finish().unwrap(); 39 | if let Some(file) = file { 40 | std::fs::write(file, something).unwrap(); 41 | Ok(vec![]) 42 | } else { 43 | Ok(something) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/code_manipulation.md: -------------------------------------------------------------------------------- 1 | # Code manipulation 2 | 3 | You can define and execute macros within a single file. But you can separate macro 4 | codes for better maintainability. 5 | 6 | The easiest way is to simply separate definitions and executions into a 7 | separate r4d files. For example, put all define and static calls inside 8 | ```def.r4d``` and put all logic macros inside ```index.r4d```. Executing a 9 | command ```rad def.r4d index.r4d``` will work as if they were a single file. 10 | 11 | ## Code freezing 12 | 13 | The other way is to freeze macros inside a file. Macro freezing is conceptually 14 | equal to m4's freezing. Rad processes macro definitions and save it into a 15 | binary file. The frozen file can be melted anytime and can be used without 16 | processing. 17 | 18 | ```bash 19 | # Freeze to a file 20 | rad file_to_freeze --freeze -o lib.r4f 21 | 22 | # Metl a file before processing a file 23 | rad file_to_execute --melt lib.r4f 24 | ``` 25 | 26 | **Rules for freezing** 27 | 28 | There are some rules applied to when macro is being freezed to a file. General 29 | rule of thumb is that **optional definition is not allowed**. Consequently 30 | following rules are applied. 31 | 32 | - Only first level "define" is allowed. 33 | - Deterred macro is not expanded. 34 | - Every permission is closed. 35 | 36 | # Packaging macro codes 37 | 38 | You can merge macro codes into a statically packaged execution script with 39 | ```--package``` flag. 40 | 41 | ```bash 42 | # Package into a single file 43 | rad index.r4d --melt lib-license.r4f lib-i18n.r4f --compile -o bin.r4c 44 | 45 | # Execute a file 46 | # File that has .r4c extension is automatically interpreted as static script 47 | rad bin.r4c 48 | 49 | # You need to feed a flag "script" to interpret input as packaged script. 50 | rad bin.r4d --script 51 | ``` 52 | -------------------------------------------------------------------------------- /src/map/sigmap.rs: -------------------------------------------------------------------------------- 1 | //! Signature map module 2 | 3 | use crate::consts::LINE_ENDING; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | use std::iter::FromIterator; 7 | 8 | /// Map for macro signatures 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct SignatureMap { 11 | pub content: HashMap, 12 | } 13 | 14 | impl SignatureMap { 15 | /// Create a new instance 16 | pub fn new(sigs: Vec) -> Self { 17 | let sig = HashMap::from_iter(sigs.into_iter().map(|sig| (sig.name.clone(), sig))); 18 | Self { content: sig } 19 | } 20 | } 21 | 22 | /// Type(variant) of macro 23 | #[derive(Debug, Serialize, Deserialize)] 24 | pub enum MacroVariant { 25 | Deterred, 26 | Function, 27 | Runtime, 28 | Static, 29 | } 30 | 31 | /// Macro signature struct 32 | #[derive(Debug, Serialize, Deserialize)] 33 | pub struct MacroSignature { 34 | pub variant: MacroVariant, 35 | pub name: String, 36 | pub args: Vec, 37 | pub expr: String, 38 | pub desc: Option, 39 | } 40 | 41 | impl std::fmt::Display for MacroSignature { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | write!( 44 | f, 45 | "Macro Type : {:#?} 46 | Macro Name : {} 47 | Arguments : {:?} 48 | Usage : {} 49 | Description >> 50 | {}", 51 | self.variant, 52 | self.name, 53 | self.args, 54 | self.expr, 55 | self.desc 56 | .as_ref() 57 | .map(|d| d 58 | .lines() 59 | .map(|line| " ".to_owned() + line) 60 | .collect::>() 61 | .join(LINE_ENDING)) 62 | .unwrap_or_default() 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/milestone.md: -------------------------------------------------------------------------------- 1 | # Confession about version 1.0 and 2.0 2 | 3 | R4d was my first project to be this big and consistent. Others were simply a 4 | toy project or simply not this big enough. I was not experienced about semantic 5 | versioning or project maintenance. Frankly speaking I pushed 1.0 too early. I 6 | didn't like the idea of sticking to pre 1.0 stage for ages and believed I 7 | should give definite signs about project progress. After some time passed and I 8 | had regretted my choice but I also learnt semantic versioning and couldn't dare 9 | to make everything break. Although I know most of the crates.io downloads are 10 | bots and "real" person who has even touched my programs are handful, I didn't 11 | want to break the custom anyway. 12 | 13 | Therefore 2.0's milestone was to fix bugs and bad decisions that I made. 14 | Technically 2.0 was a real "1.0" stable version. On other other hand 2.0's fix 15 | and updates were mostly centered on my usage which was dynamic text generation 16 | for presentations. 17 | 18 | # 3.0 for generic text manipulation 19 | 20 | 3.0 was a huge goal. Because unlike previous versions, 3.0's goal was to make a 21 | r4d generic text manipulation tool. R4d 2.0 focused on text generation. While I 22 | was adding new macros for r4d, I realized that r4d can be a fully featured text 23 | manipulation engine, which is hella cool. I had to put many breaking changes 24 | and literally hundreds of new macros. 25 | 26 | To sum it up, pevious versions of r4d aimed to be a good tool for other 27 | programs thus lacked many features. For 3.0, r4d became a program that can 28 | handle multiple operations by itself. 29 | 30 | Due to the nature of its changes, 3.0 version has many breaking changes that is 31 | not compatible with 2.0 macro codes, which is a shame. This will not happen in 32 | the future and will be only permitted by major users' request or critical 33 | security demands. 34 | -------------------------------------------------------------------------------- /docs/modes.md: -------------------------------------------------------------------------------- 1 | ### Strict related modes 2 | 3 | Processor's error behaviour can be either **strict, lenient or purge**. 4 | 5 | Default mode is strict mode. Within strict mode, certain behaviours is not 6 | allowed. On strict mode, 7 | 8 | - Overriding environment variables is an error 9 | - Overriding existing macros is an error 10 | - Failed executions of macros will interrupt a whole processing ( panics ) 11 | - Getting non-existent environment arguments yields a warning 12 | 13 | Lenient mode is more flexible than strict mode. 14 | 15 | - Overriding environment variables is not an error 16 | - Overriding existing macro is not an error. 17 | - Failed macros will be pasted as literal text. 18 | 19 | Purge is a special lenient mode that purges non-existent macros without any 20 | error messages or left-over of macros. 21 | 22 | ``` 23 | % On lenient mode 24 | $nono() 25 | % On purge mode nothing is printed. 26 | $nono() 27 | === 28 | $nono() 29 | ``` 30 | 31 | ### Hygiene modes. 32 | 33 | By default processing is not hygienic, which means that macro's behaviour can 34 | change according to external variance. 35 | 36 | For example, following macro can fail according to inner macro's existence. 37 | 38 | ``` 39 | $define(unsafe=$some_random_macro()) 40 | $unsafe() 41 | === 42 | % If some_random_macro exists it succeeds. If not, if fails. 43 | ``` 44 | 45 | You can configure this behaviour through processor's method with following 46 | variants. 47 | 48 | **Macro hygiene** mode set every further runtime modification as 49 | volatile(temporary). Within hygiene mode, runtime macros gets automatically 50 | deleted after every macro call which resides in first level 51 | 52 | **Input hygiene** mode clears every volatile macro after per input. This is 53 | mostly useful in a context of library. 54 | 55 | **Aseptic** mode disables runtime macro defintion. You cannot define any 56 | runtime macro and only able to use pre-defined runtime macros. 57 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | /// Result alias for storage operation 2 | /// 3 | /// Error is a boxed container for generic error trait. Therefore any kind of errors can be 4 | /// captured by storageresult. 5 | pub type StorageResult = Result>; 6 | 7 | /// Triat for storage interaction 8 | /// 9 | /// Rad can utilizes storage to save given input as modified form and extract data from 10 | /// 11 | /// # Example 12 | /// 13 | /// ```rust 14 | /// use r4d::{RadStorage, RadError, StorageOutput, StorageResult}; 15 | /// 16 | /// pub struct StorageDemo { 17 | /// content: Vec, 18 | /// } 19 | /// 20 | /// impl RadStorage for StorageDemo { 21 | /// fn update(&mut self, args: &[String]) -> StorageResult<()> { 22 | /// if args.is_empty() { 23 | /// return Err(Box::new(RadError::InvalidArgument("Not enough arguments".to_string()))); 24 | /// } 25 | /// self.content.push(args[0].clone()); 26 | /// 27 | /// Ok(()) 28 | /// } 29 | /// fn extract(&mut self, serialize: bool) -> StorageResult> { 30 | /// let result = if serialize { 31 | /// StorageOutput::Binary(self.content.join(",").as_bytes().to_vec()) 32 | /// } else { 33 | /// StorageOutput::Text(self.content.join(",")) 34 | /// }; 35 | /// Ok(Some(result)) 36 | /// } 37 | /// } 38 | /// ``` 39 | pub trait RadStorage { 40 | /// Update storage with given arguments 41 | fn update(&mut self, args: &[String]) -> StorageResult<()>; 42 | /// Extract data from storage. 43 | /// 44 | /// # Args 45 | /// 46 | /// - serialize : whether to serialize storage output or not 47 | fn extract(&mut self, serialize: bool) -> StorageResult>; 48 | } 49 | 50 | #[derive(Debug)] 51 | /// Output that storage creates 52 | pub enum StorageOutput { 53 | /// Binary form of output 54 | Binary(Vec), 55 | /// Text form of output 56 | Text(String), 57 | } 58 | 59 | impl StorageOutput { 60 | /// Convert storage output into a printable string 61 | pub(crate) fn into_printable(self) -> String { 62 | match self { 63 | Self::Binary(bytes) => format!("{:?}", bytes), 64 | Self::Text(text) => text, 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "r4d" 3 | version = "3.2.0-beta.2" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | keywords = ["macro", "cli", "text-processing"] 7 | categories = ["command-line-utilities", "text-processing"] 8 | description = "Text oriented macro processor" 9 | homepage = "https://github.com/simhyeon/r4d" 10 | repository = "https://github.com/simhyeon/r4d" 11 | readme = "README.md" 12 | default-run = "rad" 13 | 14 | [[bin]] 15 | name = "rad" 16 | path = "src/bin/rad.rs" 17 | 18 | [[bin]] 19 | name = "rado" 20 | path = "src/bin/rado.rs" 21 | 22 | [lib] 23 | name = "r4d" 24 | path = "src/lib.rs" 25 | crate-type = ["cdylib","rlib"] 26 | 27 | [features] 28 | # Binary related 29 | # For some reaons that I dont' know, clap cannot be enabled with dep: prefix 30 | basic = ["clap", "dep:filetime","rad_ext_template?/binary"] 31 | binary = ["basic","debug", "full", "signature", "color"] 32 | full = [ "evalexpr", "chrono", "textwrap", "cindex"] 33 | 34 | # ETC 35 | debug = [ "dep:crossterm", "dep:similar" ] 36 | color = [ "dep:colored", "dep:atty" ] 37 | hook = [] 38 | signature = ["dep:serde_json"] 39 | wasm = ["dep:wasm-bindgen", "dep:console_error_panic_hook", "dep:wee_alloc"] 40 | template = ["rad_ext_template"] 41 | 42 | [dependencies] 43 | # Default features 44 | bincode = "1.3.3" 45 | dcsv = { version = "0.3.3-rc2", git="https://github.com/simhyeon/dcsv"} 46 | regex = "1.10.2" 47 | serde = { version = "1.0.193", features = ["derive"]} 48 | trexter = "0.1.1" 49 | once_cell = "1.18.0" 50 | itertools = "0.12.0" 51 | flate2 = "1.0.24" 52 | 53 | # Optioanl features 54 | atty = { version = "0.2.14", optional = true} 55 | chrono = {version = "0.4.31", optional = true} 56 | cindex = {version = "0.5.2-rc1", optional = true, git="https://github.com/simhyeon/cindex", branch="dev"} 57 | clap = { version = "4.4.10", default-features = false, features = [ "std","help", "usage", "error-context", "suggestions", "color" ], optional=true} 58 | colored = {version = "2.0.4", optional = true} 59 | crossterm = {version = "0.27.0", optional = true} 60 | evalexpr = {version = "11.2.0", optional = true} 61 | filetime = { version = "0.2.22", optional = true } 62 | rad_ext_template = { version="0.3.0", optional = true} 63 | serde_json = { version = "1.0.108", optional = true} 64 | similar = { version = "2.3.0", optional = true} 65 | textwrap = { version = "0.16.0", optional = true} 66 | 67 | # Wasm related dependencies 68 | wasm-bindgen = { version = "0.2.74", optional = true} 69 | console_error_panic_hook = { version = "0.1.7", optional = true} 70 | wee_alloc = { version = "0.4.5", optional = true } 71 | 72 | [package.metadata.deb] 73 | features = ["binary"] 74 | maintainer = "Simon creek " 75 | copyright = "2023, Simon creek " 76 | -------------------------------------------------------------------------------- /src/extension.rs: -------------------------------------------------------------------------------- 1 | //! Extension related structs and enums module 2 | 3 | use crate::deterred_map::DFunctionMacroType; 4 | use crate::function_map::FunctionMacroType; 5 | 6 | #[derive(Clone)] 7 | /// Builder struct for extension macros 8 | /// 9 | /// This creates an extension macro without going through tedious processor methods interaction. 10 | /// 11 | /// Use a template feature to utilizes eaiser extension register. 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// let mut processor = r4d::Processor::new(); 17 | /// #[cfg(feature = "template")] 18 | /// processor.add_ext_macro(r4d::ExtMacroBuilder::new("macro_name") 19 | /// .args(&["a1","b2"]) 20 | /// .function(r4d::function_template!( 21 | /// let args = r4d::split_args!(2, false)?; 22 | /// let result = format!("{} + {}", args[0], args[1]); 23 | /// Ok(Some(result)) 24 | /// ))); 25 | /// ``` 26 | pub struct ExtMacroBuilder { 27 | pub(crate) macro_name: String, 28 | pub(crate) macro_type: ExtMacroType, 29 | pub(crate) args: Vec, 30 | pub(crate) macro_body: Option, 31 | pub(crate) macro_desc: Option, 32 | } 33 | 34 | impl ExtMacroBuilder { 35 | /// Creates an empty macro with given macro name 36 | pub fn new(macro_name: &str) -> Self { 37 | Self { 38 | macro_name: macro_name.to_string(), 39 | macro_type: ExtMacroType::Function, 40 | // Empty values 41 | args: vec![], 42 | macro_body: None, 43 | macro_desc: None, 44 | } 45 | } 46 | 47 | /// Set macro's body type as function 48 | pub fn function(mut self, func: FunctionMacroType) -> Self { 49 | self.macro_type = ExtMacroType::Function; 50 | self.macro_body = Some(ExtMacroBody::Function(func)); 51 | self 52 | } 53 | 54 | /// Set macro's body type as deterred 55 | pub fn deterred(mut self, func: DFunctionMacroType) -> Self { 56 | self.macro_type = ExtMacroType::Deterred; 57 | self.macro_body = Some(ExtMacroBody::Deterred(func)); 58 | self 59 | } 60 | 61 | /// Set macro's arguments 62 | pub fn args(mut self, args: &[impl AsRef]) -> Self { 63 | self.args = args.iter().map(|a| a.as_ref().to_string()).collect(); 64 | self 65 | } 66 | 67 | /// Set description of the macro 68 | pub fn desc(mut self, description: &str) -> Self { 69 | self.macro_desc.replace(description.to_string()); 70 | self 71 | } 72 | } 73 | 74 | /// Type of a extension macro 75 | #[derive(Clone)] 76 | pub(crate) enum ExtMacroType { 77 | /// Function macro extension 78 | Function, 79 | /// Deterred macro extension 80 | Deterred, 81 | } 82 | 83 | /// Types of a extension macro's body 84 | #[derive(Clone)] 85 | pub(crate) enum ExtMacroBody { 86 | /// Function macro body 87 | Function(FunctionMacroType), 88 | /// Deterred macro body 89 | Deterred(DFunctionMacroType), 90 | } 91 | -------------------------------------------------------------------------------- /docs/debugger_refactor.md: -------------------------------------------------------------------------------- 1 | # Problems 2 | 3 | - Step inside macro definition panicks 4 | - I fixed it somehow by reverting parse chunk body 5 | - However I cannot get improved information about other things 6 | - THis is because logger lines are inappropriate 7 | 8 | # Approach 9 | 10 | - I made a separate crate named tracks( To be renamed later ) which tracks text 11 | propagation. 12 | - I can now get tracing infomation about text semantic location 13 | 14 | # Yet to be solved 15 | 16 | - I need to merge tracks appropriately to display information properly 17 | - This is also pivotal to make debugging right 18 | 19 | # Next approach I should take 20 | 21 | - Tweak usages of append\_track method and merge methods. 22 | 23 | # Desired goal 24 | 25 | ## elog should print a line and charater number of invoked call after dollar sign 26 | 27 | ``` 28 | 123456 29 | --- 30 | $mac() 31 | 32 | ``` 33 | 34 | - Currently it prints 1,6, but it should print 1,2 35 | - This is because, logger uses line and charater information of **Full track** 36 | which points to current cursor position. ( Because full track merges all track numbers ) 37 | 38 | ## Arg parse should print inner information not a macro invocation order 39 | 40 | ``` 41 | 123456789012 42 | $mac( 43 | $inner() 44 | ) 45 | ``` 46 | 47 | - On previous ok elog, error or inner macro printed as if it were in 1,2 which 48 | is same with mac's position. This is acceptible, but can be improved. 49 | - On current implementation, it prints 3 which is 1 + 2. One from mac's 50 | position and 2 from inner's position. This is due to Argument going 51 | backwards. To solve this you have to merge paths carefully. 52 | 53 | How to merge path varis, but merged path should be, mac's position + inner's 54 | position calculated by new tracker. In other words, new tracker's full track + 55 | parent tracker's full tracks without last track. 56 | 57 | Since evaluate always called before merge\_path. It will be safely assumed 58 | that, range operation can be safely merged. 59 | 60 | Sub-ranging tracks do work ( with high possibility of panicking though ), but 61 | the current problem is that. Parse chunks args should create a new track with 0 62 | value. 63 | 64 | * [x] Check is processed consistency 65 | * [x] Empty name : THis doesn't print erorr but simply panick 66 | * [x] Comment exit 67 | * [x] Exit frag 68 | * [x] Restart : char number is broken 69 | * [ ] I put frag.is processed everywhere, I need to check those are necessary 70 | * [x] Currently logger uses constant LINE_ENDING, is this ok? 71 | - Since this is not formatted to output. It is theoretically ok, but might 72 | not what user would expect 73 | * [x] A bug... is that currently, define always consumes newlines... what? No 74 | it was just that, logm consumed result... like wtf. 75 | 76 | * [x] Strict error code with line numbers are tedious. Can it be changed? 77 | * [x] Logger struct 78 | 79 | * [ ] Clear processor's re-export of logger methods 80 | 81 | # Debugger 82 | 83 | * [x] M command works 84 | * [x] N command works 85 | -------------------------------------------------------------------------------- /src/script.rs: -------------------------------------------------------------------------------- 1 | //! User configurable extension script 2 | //! 3 | //! NOTE 4 | //! 5 | //! Leave imports as it is if you are not sure about it 6 | 7 | #[allow(unused_imports)] 8 | use crate::rad_ext_template::*; 9 | #[allow(unused_imports)] 10 | use crate::AuthType; 11 | #[allow(unused_imports)] 12 | use crate::ExtMacroBuilder; 13 | use crate::Processor; 14 | use crate::RadResult; 15 | 16 | /// Extend a processor with user configured extension macros 17 | /// 18 | /// Refer https://github.com/Simhyeon/r4d/blob/master/docs/ext.md for detailed 19 | /// explanation about macro extensions 20 | #[allow(unused_variables)] 21 | pub fn extend_processor(processor: &mut Processor) -> RadResult<()> { 22 | // --- 23 | // Write your custom extension macros from here 24 | 25 | // NOTE 26 | // Remove surrounding quote /* */ to uncomment 27 | 28 | /* 29 | // Extend function macro 30 | processor.add_ext_macro( 31 | ExtMacroBuilder::new("mac1") 32 | .args(&["a_first", "a_second"]) 33 | .desc("Description comes here") 34 | .function(function_template!( 35 | // NOTE 36 | // Function macro does not need to expand arguments 37 | // Because it is already expanded 38 | 39 | // NOTE 40 | // On split_args macro, 41 | // Function macro should give second argument as "false" 42 | // If true, then it will not strip literal quotes 43 | let args = split_args!(2,false)?; 44 | let result = format!("{} + {}", args[0], args[1]); 45 | Ok(Some(result)) 46 | )), 47 | ); 48 | // Extend deterred macro 49 | processor.add_ext_macro( 50 | ExtMacroBuilder::new("wow") 51 | .args(&["a_first", "a_second"]) 52 | .desc("Description comes here") 53 | .deterred(deterred_template!( 54 | // NOTE 55 | // Audit authentication 56 | // Macro name, and required auth type 57 | audit_auth!("macro_name",AuthType::CMD); 58 | 59 | // NOTE 60 | // On split_args macro, 61 | // Deterred macro should have second argument as "true" 62 | // because deterred should not strip before expansion 63 | let args = split_args!(2,true)?; 64 | 65 | // NOTE 66 | // expand_args macro should be only called on deterred macros 67 | // because it expands arguments and also strip 68 | let result = if expand_args!(&args[0])? == "doit" { 69 | 70 | // NOTE 71 | // You can expand normal expression with expand_args macro 72 | Some(format!("I did it -> {}", expand_args!(&args[1])?)) 73 | } else { None }; 74 | Ok(result) 75 | )), 76 | ); 77 | */ 78 | 79 | // NOTE 80 | // Should return Ok(()) at the end 81 | // --- 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # R4d(rad) 2 | //! R4d is a text oriented macro processor that tries to solve inconveniences of well-known m4 3 | //! macro processor. 4 | //! 5 | //! R4d is provided as both binary and library. Binary includes all features of optional 6 | //! dependencies. Library doesn't provide any features by default so you can set them manually. 7 | //! 8 | //! # Features 9 | //! 10 | //! ```text 11 | //! - evalexpr : eval related macros 12 | //! - chrono : time related macros 13 | //! - textwrap : "wrap" macro 14 | //! - cindex : Query related macros 15 | //! - full : all features above 16 | //! 17 | //! - debug : Enable debug method 18 | //! - color : Enable color prompt 19 | //! - hook : Enable hook macro 20 | //! - signature : Enable signature option 21 | //! ``` 22 | //! 23 | //! # Simple usage 24 | //! 25 | //! **Binary** 26 | //! ```text 27 | //! # Read from file and print to stdout 28 | //! rad input_file.txt 29 | //! # Read from standard input and print to file 30 | //! printf '...text...' | rad -o out_file.txt 31 | //! ``` 32 | //! 33 | //! **Library** 34 | //! ```no_run 35 | //! use r4d::{Processor, RadResult}; 36 | //! use std::path::Path; 37 | //! 38 | //! let mut processor = Processor::new() 39 | //! .purge(true) 40 | //! .write_to_file(Path::new("cache.txt")) 41 | //! .expect("Failed to open a file"); 42 | //! 43 | //! processor.process_file(Path::new("input.txt")) 44 | //! .expect("Failed to process file"); 45 | //! processor.print_result().expect("Failed to print result"); 46 | //! ``` 47 | //! 48 | //! Detailed r4d usage is illustrated in [github 49 | //! page](https://github.com/simhyeon/r4d/blob/master/docs/usage.md) or in 50 | //! [Processor](crate::Processor) 51 | 52 | mod error; 53 | mod package; 54 | mod process; 55 | 56 | mod parser; 57 | pub(crate) use parser::DefineParser; 58 | pub(crate) use parser::{ArgParser, SplitVariant}; 59 | 60 | mod map; 61 | pub(crate) use map::deterred_map; 62 | pub(crate) use map::function_map; 63 | #[cfg(feature = "hook")] 64 | pub(crate) use map::hookmap; 65 | pub(crate) use map::runtime_map; 66 | #[cfg(feature = "signature")] 67 | pub(crate) use map::sigmap; 68 | 69 | pub(crate) mod auth; 70 | pub(crate) mod common; 71 | pub(crate) mod consts; 72 | #[cfg(feature = "debug")] 73 | pub(crate) mod debugger; 74 | pub(crate) mod extension; 75 | pub(crate) mod formatter; 76 | pub(crate) mod lexor; 77 | pub(crate) mod logger; 78 | pub(crate) mod storage; 79 | #[macro_use] 80 | pub(crate) mod utils; 81 | 82 | pub use auth::AuthType; 83 | pub use common::{CommentType, DiffOption, Hygiene, MacroType, RadResult, WriteOption}; 84 | pub use error::RadError; 85 | pub use extension::ExtMacroBuilder; 86 | #[cfg(feature = "hook")] 87 | pub use hookmap::HookType; 88 | pub use logger::WarningType; 89 | pub use process::Processor; 90 | pub use storage::{RadStorage, StorageOutput, StorageResult}; 91 | 92 | // Optional 93 | 94 | // User configurable script execution 95 | #[cfg(feature = "template")] 96 | mod script; 97 | 98 | // Binary option 99 | #[cfg(feature = "basic")] 100 | mod cli; 101 | #[cfg(feature = "basic")] 102 | pub use cli::RadCli; 103 | #[cfg(feature = "basic")] 104 | pub use cli::RadoCli; 105 | 106 | // Re-export macro 107 | #[cfg(feature = "template")] 108 | pub use rad_ext_template; 109 | 110 | #[cfg(feature = "wasm")] 111 | pub mod wasm; 112 | 113 | #[cfg(test)] 114 | mod test; 115 | -------------------------------------------------------------------------------- /docs/file_operation.md: -------------------------------------------------------------------------------- 1 | # File operation 2 | 3 | File operation in r4d works slightly different by its behaivour. Some macros 4 | works as bufread which means it reads file's content as consecutive lines 5 | and send to writer without collecting. While some reads the file as big chunk 6 | of string. 7 | 8 | Basic work flow of file operation works like following. 9 | 10 | ``` 11 | Input -> Expander -> Writer -> Redirector 12 | ``` 13 | 14 | These are not official terms but used to illustrate internal logics. 15 | 16 | ## Include 17 | 18 | Include macro works both way. If macro is in first level, include reads file as 19 | bufread. But if include is called in nested context, it aggregates all data 20 | into a result. 21 | 22 | On a first level, 23 | 24 | ```r4d 25 | % These are technically same operations 26 | $include(file_name.txt) 27 | $readin(file_name.txt) 28 | ``` 29 | 30 | But as nested call. 31 | 32 | ``` 33 | % Whole texts of file_name.txt is sent to outer macro. 34 | $outer($include(file_name.txt)) 35 | ``` 36 | 37 | This means that include macro collects all arguments until the "read" operation 38 | finishes. After expander collects all string, include macro sends them to 39 | writer which may be redirected to other variations. 40 | 41 | ## readin and readto 42 | 43 | Readin and readto enforces bufread. This means that "read" text is not 44 | collected, but gets sent to writer on line based. For example, 45 | 46 | ```r4d 47 | % ASSUME that text.txt's content is "This comes after." 48 | $define(nested,a_text= This should come before text $a_text()) 49 | $nested($readin(text.txt)) 50 | === 51 | This comes after. This should come before text 52 | ``` 53 | 54 | This happens because while argument is being expanded, readin doesn't wait a 55 | result but send file's content to writer. After readin sends all file contents 56 | into a writer, then macros' body is evaluated and sent to writer. Therefore, 57 | file's content was evaluated beforehand. 58 | 59 | ## Include with relay 60 | 61 | There are times when you have to use relay pattern inside a file that you have 62 | to include. However this will not always work by the level of macro's call 63 | because of include macro's nature. ( Reminder, relay doesn't work inside 64 | arguments.) 65 | 66 | Let's assume that file ```inner.txt``` has a content as following 67 | 68 | ```txt 69 | $declare(cont) 70 | $relay(macro,cont) 71 | 1 2 3 4 5 72 | $halt() 73 | ``` 74 | 75 | And we're using a index file with following contents. 76 | 77 | ```r4d 78 | % On first level it works perfectly fine. 79 | $include(inner.txt) 80 | $cont() 81 | === 82 | 1 2 3 4 5 83 | 84 | ``` 85 | 86 | This works as expected because include works as bufread. However, include 87 | inside arguments process file content's as **arguments** 88 | 89 | ``` 90 | $if(true,$include(inner.txt)) 91 | $isempty($cont()) 92 | === 93 | 1 2 3 4 5 94 | 95 | true 96 | ``` 97 | 98 | The text ```1 2 3 4 5``` was returned from if macro. And no texts were relayed 99 | to cont macro. The reason relay didn't work as expected is because relay 100 | doesn't work inside arguments. If you want to use relay inside a file, use 101 | ```readin``` to force bufread. Although this will print file's content before 102 | your macro call evaluates, this technique is useful when you don't mind file's 103 | whole content but only parts of it. 104 | 105 | ``` 106 | $if(true,$readin(inner.txt)) 107 | $isempty($cont()) 108 | === 109 | false 110 | ``` 111 | -------------------------------------------------------------------------------- /docs/newline_rules.md: -------------------------------------------------------------------------------- 1 | ## What newlines? 2 | 3 | While using a rad you might twick macros a little bit because the ouptut 4 | somehow is not what you'd expect. You put some $nl() after macro codes and 5 | simply spam trim attribute to make it expectable. And welp it is totally a sane 6 | thing to do. However newlines within rad processing have **consistent rules**, 7 | although those are often overlooked. 8 | 9 | ## Macro's return value is either nothing or a value with empty content 10 | 11 | Think of a macro as a function. ( Though it really is ) Macro gets arguments 12 | and processes body with given information and finally returns a value. Actually 13 | every macro returns a rust type of ```Result>```. Now any 14 | rustaceans would know the meaning of the title, but for those who are not, this 15 | means macro returns a real text value or nothing. 16 | 17 | **When nothing is returned newline is consumed.** All function macros returns 18 | nothing if it has no text to print. Therefore macros such as define, let or 19 | static removes folloiwng newline. For example, 20 | 21 | ```r4d 22 | $define(test=Test) 23 | === 24 | ``` 25 | 26 | Define leaves nothing in it's place. What if define returned a real value but 27 | empty value? Let's simulate by wrapping define inside a macro. 28 | 29 | ```r4d 30 | $define(my_define,a=$define(b=)) 31 | $my_define() 32 | === 33 | 34 | ``` 35 | 36 | In this case runtime macro ```my_define``` has no contents in it other than 37 | define. Not even spaces or newlines. So it's easy to think that ```my_define``` 38 | would return nothing. However ```my_define``` returns an empty string. A real 39 | value with empty texts. Therefore it leaves an empty string or an empty line in 40 | its place. Consequently **runtime macros never return "no value"** but at least 41 | return an empty value. 42 | 43 | Although you can change this behaviour with help of trim output macro 44 | attribute. Refer, [docs](./macro_syntax.md) for detailed usage. 45 | 46 | ## Hidden newline character at the end of a line 47 | 48 | You might encounter a case like this 49 | 50 | ``` 51 | $define(hmm= 52 | $let(a,b) 53 | $let(c,d) 54 | ) 55 | $hmm() 56 | === 57 | 58 | 59 | ``` 60 | 61 | You wanted to spread a macro definition for readability and thought ```hmm``` would 62 | yield a single newline. Because let would consume folloiwng new lines. The 63 | definition expanded result is technically same with folloiwng. 64 | 65 | ``` 66 | $define(this_is_empty= 67 | ) 68 | $logm(this_is_empty) 69 | === 70 | % cosole 71 | % log: 72 | % --> test:3:2 73 | ``` 74 | 75 | That is a single newline. Then why ```hmm``` prints two newlins? The short 76 | answer is, **because there is a newline after**. But let us dive deeper. If we 77 | substitute ```\n``` to ```\\n```, and assume define is eagerly expanded. The 78 | processing steps would like following. 79 | 80 | ``` 81 | % Source 82 | $define(hmm=\n $let(a,b)\n $let(c,d)\n)\n$hmm()\n 83 | % 1. Expand fisrt let and consume newline 84 | $define(hmm=\n $let(c,d)\n)\n$hmm()\n 85 | % 2. Expand second let and consume newline 86 | $define(hmm=\n)\n$hmm()\n 87 | % 3. Definition also consumes newline 88 | $hmm()\n 89 | % 4. Run a hmm macro function and paste a return value 90 | \n\n 91 | % 5. Ok now that is two newlines :) 92 | ``` 93 | 94 | Every line has a newline character in it's end. You just don't know because our 95 | smart text editor handles newlines as if it were not there. Even the substitued 96 | line had newline character after the last character ```\\n```. It is hard to 97 | write simply null terminated string without trailing newline in text editor. ( 98 | And I'm not sure if it is even possible in some editors) 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### R4d (Rad) 2 | 3 | R4d is a text oriented macro prosessor that is modern, expressive, and easy to 4 | use. 5 | 6 | [Changes](./docs/change.md) 7 | 8 | ### 3.2 is coming 9 | 10 | Currently master branch is not well tested. Please consider using a well tested 11 | crates.io version. 12 | 13 | ### Table of conents 14 | 15 | * [Demo](#demo) 16 | * [Install](#install) 17 | * [Simple usage](#simple-usage) 18 | * [Documentation](#documentation) 19 | 20 | ### Demo 21 | 22 | **Raw texts** 23 | ```text 24 | $define(author=Simon Creek) 25 | $define(title=R4d demo) 26 | --- 27 | title : $title() 28 | author : $author() 29 | --- 30 | My name is $author() and I made r4d to make macros can be used within various 31 | forms of texts. This article was written in $date() $time(). 32 | 33 | $ifdef(test, This should be only printed when I'm testing not in release)$dnl() 34 | 35 | This is some important table automatically formatted according to environment 36 | variable. 37 | 38 | $regcsv(addr,$include(addr.csv))$dnl() 39 | 40 | $static( 41 | queried, 42 | $query( 43 | SELECT id,first_name,address 44 | FROM addr where first_name = John 45 | ) 46 | )$dnl() 47 | 48 | % Comments are disabled by default for better compatibility 49 | % TABLE_FORM == github 50 | $table($env(TABLE_FORM),$queried()) 51 | 52 | $wrap(40,$lipsum(15)) 53 | 54 | Evaluation : $prec($eval( $num(0.1second) + $num(0.2sekunde)),2) 55 | Evaluation : $evalk( 1 + 2 ) 56 | ``` 57 | **Processed texts** 58 | 59 | Ran with ```TABLE_FORM=github rad file_name.md -a env+fin --comment``` 60 | 61 | ``` 62 | --- 63 | title : R4d demo 64 | author : Simon Creek 65 | --- 66 | My name is Simon Creek and I made r4d to make macros can be used within various 67 | forms of texts. This article was written in 2022-01-18 16:38:07. 68 | 69 | This is some important table automatically formatted according to environment 70 | variable. 71 | 72 | |id|first_name|address| 73 | |-|-|-| 74 | |1|John|111-2222| 75 | |2|John|222-3333| 76 | 77 | Lorem ipsum dolor sit amet, consectetur 78 | adipiscing elit, sed do eiusmod tempor 79 | incididunt ut labore. 80 | 81 | Evaluation : 0.30 82 | Evaluation : 1 + 2 = 3 83 | ``` 84 | 85 | ### Install 86 | 87 | [Install rust toolchain for build](https://www.rust-lang.org/tools/install) 88 | 89 | I recommend using ```cargo install``` until I prepare a proper CD 90 | pipeline. 91 | 92 | ```bash 93 | # Binary with full macros support 94 | cargo install r4d --features binary --locked 95 | 96 | # Only include macros that doesnt't require external crates 97 | cargo install r4d --features basic --locked 98 | 99 | # Refer docs.rs or usage section for detailed feature usage 100 | ``` 101 | 102 | ### Simple usage 103 | 104 | **Binary** 105 | 106 | There are two binaries of each **rad** and **rado**. Rad is a main processor 107 | and rado is a wrapper binary. 108 | 109 | ``` 110 | # rad 111 | # Read from a file and print to stdout 112 | rad input_file.txt 113 | # Read from standard input and print to a file 114 | printf '...text...' | rad -o out_file.txt 115 | # Get a simple manual for a macro 116 | rad --man ifdef 117 | 118 | # rado 119 | # Edit a source file 120 | rado edit file_name.txt 121 | # Read a processed file 122 | rado read file_name.txt 123 | # Print environment variables 124 | rado env 125 | ``` 126 | 127 | **Library** 128 | ```rust 129 | use r4d::RadError; 130 | use r4d::Processor; 131 | 132 | let processor = Processor::new() 133 | .write_to_file(PathBuf::from("cache.txt"))?; 134 | 135 | processor.process_file(Path::new("input.txt"))?; 136 | processor.print_result()?; 137 | ``` 138 | 139 | ## Documentation 140 | 141 | [index](./docs/index.md) 142 | 143 | ## Known issues 144 | 145 | [issues](./docs/issues.md) 146 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | //! Multiple constant variables 2 | 3 | use once_cell::sync::Lazy; 4 | use regex::Regex; 5 | 6 | /// Text display wrapper 7 | /// 8 | /// This can be either simple string or "Color" crate's function 9 | #[cfg(feature = "color")] 10 | pub type ColorDisplayFunc = fn(string: &str, to_file: bool) -> Box; 11 | 12 | /// Static source for lorem lipsum 13 | pub const LOREM_SOURCE: &str = "Lorem ipsum dolor sit amet consectetur adipiscing elit. In rhoncus sapien iaculis sapien congue a dictum urna malesuada. In hac habitasse platea dictumst. Quisque dapibus justo a mollis condimentum sapien ligula aliquam massa in vehicula tellus magna vitae enim. Aliquam mattis ligula in enim congue auctor. Pellentesque at sollicitudin velit. Quisque blandit lobortis turpis at malesuada. Donec vitae luctus mauris. Aenean efficitur risus id tortor blandit laoreet. Vestibulum commodo aliquam sapien. Cras aliquam eget leo iaculis cursus. Morbi iaculis justo sed tellus ultrices aliquet. Nam bibendum ut erat quis. "; 14 | 15 | /// Static lorem lipsum vector 16 | pub static LOREM: Lazy> = Lazy::new(|| LOREM_SOURCE.split(' ').collect()); 17 | /// Static lorem lipsum vector's length 18 | pub static LOREM_WIDTH: Lazy = Lazy::new(|| LOREM.len()); 19 | 20 | /// Get macro start character 21 | /// 22 | /// This return custom character if given so 23 | pub(crate) fn macro_start(custom: Option) -> char { 24 | if let Some(start) = custom { 25 | start 26 | } else { 27 | MACRO_START_CHAR 28 | } 29 | } 30 | 31 | /// Get comment start chracter 32 | /// 33 | /// This return custom character if given so 34 | pub(crate) fn comment_start(custom: Option) -> char { 35 | if let Some(start) = custom { 36 | start 37 | } else { 38 | COMMENT_CHAR 39 | } 40 | } 41 | 42 | // Platform agonistic consts 43 | /// Default macro character 44 | const MACRO_START_CHAR: char = '$'; 45 | /// Default comment character 46 | const COMMENT_CHAR: char = '%'; 47 | 48 | /// Escape character 49 | pub const ESCAPE_CHAR: char = '\\'; 50 | /// Literal start character 51 | pub const LIT_CHAR: char = '*'; 52 | /// Default main caller 53 | /// 54 | /// This is default for input 55 | pub const MAIN_CALLER: &str = "@MAIN@"; 56 | 57 | pub const MACRO_SPECIAL_ANON: &str = "_ANON_"; 58 | 59 | // Numbers 60 | // Macro attributes * ^ = - | ~ 61 | // Underscore and reverse slash (\) 62 | // Colon (:) for iterated value 63 | // Exclamation ( ! ) for negation 64 | /// Unallowed regex pattern that is reserved for other purposes 65 | pub static UNALLOWED_CHARS: Lazy = Lazy::new(|| { 66 | Regex::new(r#"[a-zA-Z1-9\\_\*\^\|\(\)-=,:~!]"#).expect("Failed to create regex expression") 67 | }); 68 | 69 | // Diff related 70 | #[cfg(feature = "debug")] 71 | /// Source file for diff operation 72 | pub const DIFF_SOURCE_FILE: &str = "diff.src"; 73 | #[cfg(feature = "debug")] 74 | /// Out file for diff operation 75 | pub const DIFF_OUT_FILE: &str = "diff.out"; 76 | 77 | // LINE ENDING 78 | #[cfg(windows)] 79 | /// Platform specific line ending 80 | pub const LINE_ENDING: &str = "\r\n"; 81 | #[cfg(not(windows))] 82 | /// Platform specific line ending 83 | pub const LINE_ENDING: &str = "\n"; 84 | 85 | // PATH_SEPARATOR 86 | // On windows this should return double forward slash. 87 | // because only double forward slash is guaranteed to be evaluated as single 88 | // forward slash 89 | #[cfg(windows)] 90 | /// Platform specific path separator 91 | pub const PATH_SEPARATOR: &str = "\\\\"; 92 | #[cfg(not(windows))] 93 | /// Platform specific path separator 94 | pub const PATH_SEPARATOR: &str = "/"; 95 | 96 | #[cfg(feature = "debug")] 97 | /// Debug help message string 98 | pub const RDB_HELP: &str = include_str!("debug_help_message.txt"); 99 | 100 | /// Empty String aRray 101 | pub const ESR: [&str; 0] = []; 102 | 103 | /// Define keyword 104 | pub const DEFINE_KEYWORD: &str = "define"; 105 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | //! Authorization(Permission) 2 | //! 3 | //! Permission should be given for some function macro types 4 | //! 5 | //! Currently there are four types of authorization 6 | //! 7 | //! - fin 8 | //! - fout 9 | //! - cmd 10 | //! - env 11 | 12 | use crate::consts::LINE_ENDING; 13 | use std::fmt::Write; 14 | 15 | #[derive(Debug)] 16 | /// Struct that stores auth states 17 | pub(crate) struct AuthFlags { 18 | auths: Vec, 19 | } 20 | 21 | impl AuthFlags { 22 | /// Create a new instance 23 | pub fn new() -> Self { 24 | let mut auths = Vec::new(); 25 | for _ in 0..AuthType::LEN as usize { 26 | auths.push(AuthState::Restricted); 27 | } 28 | 29 | Self { auths } 30 | } 31 | 32 | /// Set auth state 33 | pub fn set_state(&mut self, auth_type: &AuthType, auth_state: AuthState) { 34 | self.auths[*auth_type as usize] = auth_state; 35 | } 36 | 37 | /// Get auth state 38 | pub fn get_state(&self, auth_type: &AuthType) -> &AuthState { 39 | &self.auths[*auth_type as usize] 40 | } 41 | 42 | /// Get auth state but in string 43 | pub fn get_status_string(&self) -> Option { 44 | let mut format = String::new(); 45 | for index in 0..AuthType::LEN as usize { 46 | let auth_type = AuthType::from_usize(index).unwrap(); 47 | match self.get_state(&auth_type) { 48 | &AuthState::Warn | &AuthState::Open => { 49 | // Add newline before 50 | format.push_str(LINE_ENDING); 51 | // This is mostly ok since, auth_type is always valid utf8 character 52 | write!(format, "Auth : {:?} is open.", auth_type).ok(); 53 | } 54 | &AuthState::Restricted => (), 55 | } 56 | } 57 | if !format.is_empty() { 58 | Some(format) 59 | } else { 60 | None 61 | } 62 | } 63 | 64 | /// Clear all auth states 65 | pub fn clear(&mut self) { 66 | *self = Self::new(); 67 | } 68 | } 69 | 70 | #[derive(Debug, Clone, Copy)] 71 | /// Authorization type 72 | /// 73 | /// ```text 74 | /// Each variants means 75 | /// - ENV : environment variable permission 76 | /// - FIN : File in(read) permission 77 | /// - FOUT : File out(write) permission 78 | /// - CMD : System command permission 79 | /// - LEN : This is a functional variant not a real value, namely a length 80 | /// ``` 81 | pub enum AuthType { 82 | /// Environment variable permission 83 | ENV = 0, 84 | /// File in(read) permission 85 | FIN = 1, 86 | /// File out(write) permission 87 | FOUT = 2, 88 | /// System command permission 89 | CMD = 3, 90 | /// This is a functional variant not a real value 91 | LEN = 4, 92 | } 93 | 94 | impl std::fmt::Display for AuthType { 95 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 96 | let string = match self { 97 | Self::ENV => "ENV", 98 | Self::FIN => "FIN", 99 | Self::FOUT => "FOUT", 100 | Self::CMD => "CMD", 101 | Self::LEN => "LEN", 102 | }; 103 | 104 | write!(f, "{}", string) 105 | } 106 | } 107 | 108 | impl AuthType { 109 | /// Convert str slice into AuthType 110 | pub fn from(string: &str) -> Option { 111 | match string.to_lowercase().as_str() { 112 | "env" => Some(Self::ENV), 113 | "fin" => Some(Self::FIN), 114 | "fout" => Some(Self::FOUT), 115 | "cmd" => Some(Self::CMD), 116 | _ => None, 117 | } 118 | } 119 | 120 | /// Convert usize integer into a auth type 121 | pub fn from_usize(number: usize) -> Option { 122 | match number { 123 | 0 => Some(Self::ENV), 124 | 1 => Some(Self::FIN), 125 | 2 => Some(Self::FOUT), 126 | 3 => Some(Self::CMD), 127 | _ => None, 128 | } 129 | } 130 | } 131 | 132 | #[derive(Clone, Copy, Debug)] 133 | /// Current state authorization 134 | pub(crate) enum AuthState { 135 | /// Not allowed 136 | Restricted, 137 | /// Allowed but wans user 138 | Warn, 139 | /// Openly allowed 140 | Open, 141 | } 142 | -------------------------------------------------------------------------------- /docs/3_0.md: -------------------------------------------------------------------------------- 1 | # 3.0 changes 2 | 3 | 3.0 has many breaking and other qualify of life changes. Therefore some 2.0 4 | macro codes will not work with 3.0 version of r4d. 5 | 6 | ## Library name changes 7 | 8 | Library name has changed from rad to **r4d**. This is due to a cargo 9 | recommenation(or say warning) to separate library and binary names. Previously 10 | the names were both rad. 11 | 12 | ## New rado binary for better experience 13 | 14 | New binary rado is now included in procects. Rado's goal is to provide easier 15 | way to edit and read macro files. You can edit a file as unprocessed form and 16 | read a file as a processed form. 17 | 18 | Refer [usage](./usage.md) for rado usage. 19 | 20 | ## Sensible comma parsing inside parenthesis 21 | 22 | Prevoiusly an argument parser interpreted any commas as delimter. From 3.0, it 23 | has changed into sensible default behaviour by ignoring them within pair of 24 | parenthesis. For example the following commands didn't work in 2.0. 25 | 26 | ``` 27 | $ifelse(true, 28 | $index(1,a,b,c) 29 | $index(2,a,b,c) 30 | ) 31 | === 32 | % This didn't work becuase ifelse thought '$index(1' was a second argument 33 | ``` 34 | 35 | To avoid this, explicit literal quote was required and yet it didn't work any 36 | better. For the release candidate stage, a new macro strip was added to 37 | circumvent this ( which is differnt from previous strip macro ). However to 38 | achieve general user's expectance, argument parser's logic has changed. Now the 39 | previous code will work without literal quote. 40 | 41 | ## Anonymous function 42 | 43 | Now you can define anonymous function that is used only for a scope which is 44 | handy for some cases such as one liner macro execution. 45 | 46 | ```bash 47 | cat test | rad --pipe -L '$mapl($anon(l=$cut(:,4,$l())$nl()),$-())' 48 | ``` 49 | 50 | ## "For" macro ergonomics 51 | 52 | This was already possible with "for\_macro" feature. From 3.0, for macros 53 | register a local macro named ```:``` which can be invoked with ```$:()```. 54 | Prevoiusly, for macro replaced $: string with iterated values, which worked 55 | pretty well but nested for loop was inconsistent. With newer implemenation, 56 | local macro ```:``` is executed according to local context thus enabling 57 | consistent nested loop. 58 | 59 | ``` 60 | $forloop^($:() - \*$forloop($:(),1,5)*\$nl(),1,5) 61 | === 62 | 1 - 12345 63 | 2 - 12345 64 | 3 - 12345 65 | 4 - 12345 66 | 5 - 12345 67 | ``` 68 | 69 | ## Non-existent file input is an error 70 | 71 | Previously rad binary automatcially expanded input argument as literal text 72 | when the given input was not a valid file. Now this triggers an error and 73 | explicit flag ```--literal, -L``` is required to interpret argument as literal 74 | text. 75 | 76 | ## Error behaviours ( Nopanic flag removed ) 77 | 78 | Previously error levels were divided into strict,lenient,purge,nopanic. However 79 | logics in between was not consistent and made error logs unexpectable. From 3.0 80 | nopanic flag is removed, and purge or lenient will prevent panics from both 81 | runtime and function macros. 82 | 83 | There are misc changes to error loggings and strict panics, ETC. 84 | 85 | ## Macro execution order 86 | 87 | Previously macro execution order was following, 88 | 89 | - Deterred 90 | - Local 91 | - Runtime 92 | - Function 93 | 94 | From 3.0, macro execution order follows new order 95 | 96 | - Local 97 | - Runtime 98 | - Deterred 99 | - Function 100 | 101 | So that user can easily override deterred macros if they want. 102 | 103 | ## Feature simplification 104 | 105 | Many features are now hidden from user, because those features were enabled by 106 | other features anyway while making docs harder to read. 107 | 108 | Some features such as csv or lipsum has been ditched in favor of other simpler 109 | crates or pure in-built implemenation. 110 | 111 | Storage has been merged to default r4d logic because it had very low overhead 112 | and made code maintenance hard with litle gain. There is also a possbility to 113 | add "hook" as default feature but it adds some overhead so not decidable at the 114 | moment. 115 | 116 | ## Storage method signature changed 117 | 118 | Previously storage accepted Vector of string, not it accepts array of string 119 | (&[String]) for better compatibility; 120 | 121 | ## Many breaking macro changes 122 | 123 | With 3.0, many macros' arguments order changed, some macros were removed and 124 | some macros names' were changed. 125 | 126 | The rationals are, 127 | 128 | - An argument that can highly contain commas should come the last for usability 129 | - Macro name that doesn't represent its behaviour should be changed to avoid 130 | confusion 131 | - Macros that are rarely used and are mostly a name "squtting" should be 132 | removed 133 | 134 | ## Misc 135 | 136 | There are also many miscellaneous changes. Such as 137 | 138 | - Many new macros 139 | - New rad feature 140 | - Detailed documentation 141 | - New library methods 142 | - Bug fixes 143 | -------------------------------------------------------------------------------- /docs/ext.md: -------------------------------------------------------------------------------- 1 | ### Macro extension 2 | 3 | You can extend macros easily with processor interface which is enabled by 4 | **template** feature. 5 | 6 | Refer last part of this documentation if you want to extend macros as binary 7 | target. 8 | 9 | **Basic demo** 10 | 11 | ```toml 12 | [dependencies] 13 | r4d = {version="*", features = ["template"]} 14 | ``` 15 | 16 | ```rust 17 | use r4d::ExtMacroBuilder; 18 | use r4d::ext_template::*; 19 | use r4d::AuthType; 20 | use r4d::Processor; 21 | use r4d::RadResult; 22 | 23 | // --- 24 | // Processor creation precedes... 25 | // --- 26 | 27 | // DEMOS 28 | // Extend function macro 29 | processor.add_ext_macro( 30 | ExtMacroBuilder::new("mac1") 31 | .args(&["a_first", "a_second"]) 32 | .desc("Description comes here") 33 | .function(function_template!( 34 | // NOTE 35 | // Function macro does not need to expand arguments 36 | // Because it is already expanded 37 | 38 | // NOTE 39 | // On split_args macro, 40 | // Function macro should give second argument as "false" 41 | // If true, then it will not strip literal quotes 42 | let args = split_args!(2,false)?; 43 | let result = format!("{} + {}", args[0], args[1]); 44 | Ok(Some(result)) 45 | )), 46 | ); 47 | // Extend deterred macro 48 | processor.add_ext_macro( 49 | ExtMacroBuilder::new("mac2") 50 | .args(&["a_first", "a_second"]) 51 | .desc("Description comes here") 52 | .deterred(deterred_template!( 53 | // NOTE 54 | // Audit authentication 55 | // Macro name, and required auth type 56 | audit_auth!("mac2",AuthType::CMD); 57 | 58 | // NOTE 59 | // On split_args macro, 60 | // Deterred macro should have second argument as "true" 61 | // because deterred should not strip before expansion 62 | let args = split_args!(2,true)?; 63 | 64 | // NOTE 65 | // expand_args macro should be only called on deterred macros 66 | // because it expands arguments and also strip 67 | let result = if expand_args!(&args[0])? == "doit" { 68 | 69 | // NOTE 70 | // You can expand normal expression with expand_args macro 71 | Some(format!("I did it -> {}", expand_args!(&args[1])?)) 72 | } else { None }; 73 | Ok(result) 74 | )), 75 | ); 76 | ``` 77 | 78 | **More about codes** 79 | 80 | These macros are actually a convenience for trivial typings. Actual code under 81 | the hood are expanded as closure. 82 | 83 | ```rust 84 | // Original function macro's type 85 | pub(crate) type FunctionMacroType = fn(&str, &mut Processor) -> RadResult>; 86 | 87 | // function_template!( Your code ) expands to 88 | |args: &str, processor: &mut Processor| -> RadResult> { 89 | Your code 90 | } 91 | 92 | // Original deterred macro's type 93 | pub(crate) type DFunctionMacroType = fn(&str, usize, &mut Processor) -> RadResult>; 94 | 95 | // deterred_template!( Your code ) expands to 96 | |args: &str, level: usize, processor: &mut Processor| -> RadResult> { 97 | Your code 98 | } 99 | 100 | // split_args is equivalent to 101 | processor.split_arguments("text,to,parse", args, false) 102 | 103 | // expand_args macro is equivalent to 104 | processor.expand(level,"text_to_parse", true) 105 | 106 | // expand_expr macro is equivalent to 107 | processor.expand(level,"text_to_parse", false) 108 | ``` 109 | 110 | You can also simply send your function as argument instead of using template macros. 111 | 112 | ### Extend macros as binary with help of script.rs file 113 | 114 | You can also extend rad macros by manually editing script.rs file inside src 115 | directory. 116 | 117 | [Script file](../src/script.rs) doesn't do anything by default and actually 118 | doesn't get included without template feature. 119 | 120 | You can modify the file and make it included by compiling with ```template``` 121 | feature. 122 | 123 | ```bash 124 | # You need to work in a local copy of a project e.g) git clone 125 | git clone https://github.com/simhyeon/r4d 126 | 127 | # Build within r4d procject 128 | cargo build --release --features binary,template 129 | 130 | # Built binary is located in "target/release" directory 131 | ``` 132 | 133 | ### Some general examples for deterred macro extension 134 | 135 | By default, deterred macro's argument is not expanded at all. Therefore the 136 | user has to manually set all expansion rules. This is cumbersome but useful 137 | when you need a contextual information about macro expansion. R4d's default 138 | "if" macro variants really represent such cases well. 139 | 140 | You, as an end user can also profit by using hand written deterred macro when 141 | you need to capture a context but don't need early expansion. For example, 142 | ```radroff``` which converts macro codes into a intermediate manual struct 143 | simply passes raw text arguments into a data structure. While an expansion 144 | occurs when the user decides a final format to print. With help of deterred 145 | macro, r4d knows a context of a macro call and able to redirect raw information 146 | to internal struct. 147 | -------------------------------------------------------------------------------- /src/formatter.rs: -------------------------------------------------------------------------------- 1 | //! Formatter for various formats 2 | 3 | use crate::error::RadError; 4 | use crate::RadResult; 5 | use dcsv::VirtualArray; 6 | use itertools::Itertools; 7 | use std::fmt::Write; 8 | 9 | /// Formatter that constructs multiple text formats 10 | pub(crate) struct Formatter; 11 | 12 | impl Formatter { 13 | /// Convert csv to corresponding format table 14 | /// 15 | /// Available formats are 16 | /// - github 17 | /// - wikitext 18 | /// - html 19 | pub fn csv_to_table(table_format: &str, data: &str, newline: &str) -> RadResult { 20 | let data = dcsv::Reader::new() 21 | .trim(true) 22 | .ignore_empty_row(true) 23 | .has_header(false) 24 | .array_from_stream(data.as_bytes())?; 25 | if data.rows.is_empty() { 26 | return Err(RadError::InvalidArgument( 27 | "Table cannot be constructed from empty value".to_string(), 28 | )); 29 | } 30 | let table = match table_format { 31 | "github" => Formatter::gfm_table(&data, newline)?, 32 | "wikitext" => Formatter::wikitext_table(&data, newline)?, 33 | "html" => Formatter::html_table(&data, newline)?, 34 | _ => { 35 | return Err(RadError::UnsupportedTableFormat(format!( 36 | "Unsupported table format : {}", 37 | table_format 38 | ))) 39 | } 40 | }; 41 | Ok(table) 42 | } 43 | 44 | // ---------- 45 | // Formatting methods start 46 | // 47 | /// Execute sequence of macros from csv data 48 | pub fn csv_to_macros(macro_name: &str, data: &str, newline: &str) -> RadResult { 49 | let data = dcsv::Reader::new() 50 | .has_header(false) 51 | .array_from_stream(data.as_bytes())?; 52 | let mut exec = String::new(); 53 | let mut iter = data.rows.iter().peekable(); 54 | while let Some(row) = iter.next() { 55 | write!(exec, "${}({})", macro_name, row.iter().join(","))?; 56 | if iter.peek().is_some() { 57 | exec.push_str(newline); 58 | } 59 | } 60 | Ok(exec) 61 | } 62 | 63 | /// Format csv into github formatted table 64 | fn gfm_table(data: &VirtualArray, newline: &str) -> RadResult { 65 | let mut table = String::new(); 66 | let mut data_iter = data.rows.iter(); 67 | let header = data_iter.next(); 68 | if header.is_none() { 69 | return Err(RadError::InvalidArgument( 70 | "Table cannot be constructed from empty value".to_string(), 71 | )); 72 | } 73 | let header = header.unwrap(); 74 | table.push('|'); 75 | let header_count = header.len(); 76 | for h in header { 77 | write!(table, "{}|", h)?; 78 | } 79 | 80 | // Add separator 81 | write!(table, "{}|", newline)?; 82 | for _ in 0..header_count { 83 | table.push_str("-|"); 84 | } 85 | 86 | for row in data_iter { 87 | write!(table, "{}|", newline)?; 88 | for value in row { 89 | write!(table, "{}|", value)?; 90 | } 91 | } 92 | 93 | Ok(table) 94 | } 95 | 96 | /// Format csv into wikitext formatted table 97 | fn wikitext_table(data: &VirtualArray, newline: &str) -> RadResult { 98 | let mut table = String::new(); 99 | let mut data_iter = data.rows.iter(); 100 | let header = data_iter.next(); 101 | if header.is_none() { 102 | return Err(RadError::InvalidArgument( 103 | "Table cannot be constructed from empty value".to_string(), 104 | )); 105 | } 106 | let header = header.unwrap(); 107 | 108 | // Add header 109 | write!(table, "{{| class=\"wikitable\"{}", newline)?; 110 | // ! Header text !! Header text !! Header text 111 | // |- 112 | for h in header { 113 | write!(table, "!{}{}", h, newline)?; 114 | } 115 | // Header separator 116 | write!(table, "|-{}", newline)?; 117 | for row in data_iter { 118 | for value in row { 119 | write!(table, "|{}{}", value, newline)?; 120 | } 121 | write!(table, "|-{}", newline)?; 122 | } 123 | table.push_str("|}"); 124 | Ok(table) 125 | } 126 | 127 | /// Format csv into html formatted table 128 | fn html_table(data: &VirtualArray, newline: &str) -> RadResult { 129 | let mut table = String::new(); 130 | let mut data_iter = data.rows.iter(); 131 | let header = data_iter.next(); 132 | if header.is_none() { 133 | return Err(RadError::InvalidArgument( 134 | "Table cannot be constructed from empty value".to_string(), 135 | )); 136 | } 137 | let header = header.unwrap(); 138 | // Add header parts 139 | write!(table, "{0}\t{0}\t\t{0}", newline)?; 140 | for h in header { 141 | write!(table, "\t\t\t{}", h, newline)?; 142 | } 143 | write!(table, "\t\t{0}\t{0}\t{0}", newline)?; 144 | for row in data_iter { 145 | write!(table, "\t\t{}", newline)?; 146 | for value in row { 147 | write!(table, "\t\t\t", value)?; 148 | } 149 | write!(table, "\t\t{}", newline)?; 150 | } 151 | write!(table, "\t{}
{}
{}
", newline)?; 152 | Ok(table) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /docs/macro_types.md: -------------------------------------------------------------------------------- 1 | # TOC 2 | 3 | - [Types of macro](#types-of-macro) 4 | - [Local and global](#local-and-global) 5 | - [Text and operation](#text-and-operation) 6 | - [Hook macro](#hook-macro) 7 | 8 | ## Types of macro 9 | 10 | There are technically two types of macros. A function macro and a runtime macro. 11 | Function macros are macros that trigger rust functions. Runtime macros are 12 | macros that trigger text substitution that possibly include other function 13 | macros. Both macro types are syntactically same but has difference in that 14 | tokenized arguments are mapped to argumens in runtime macro and function macro 15 | can manipulate argumens maually or even interrupt such tokenizing behaviour. 16 | 17 | Function macros are mostly built-in macros provided by r4d itself. However you 18 | can also extend function macros with r4d's library interface. [How to 19 | extend](./ext.md) 20 | 21 | There is a special type of function macro that is called deterred macro. 22 | Arguments of function macros are eagerly expanded while deterred macro 23 | interrupt such expansion and can control the entire behaviour. 24 | 25 | Not every macro that markded as deterred deters an argument expansion. If you 26 | want to know the exact order of expansion refer a manual with ```--man``` flag. 27 | 28 | ## Local and global 29 | 30 | There are two macro types in terms of macro scope. Local macro and global 31 | macro. Local macro is a macro that only persists for certain period or scope. 32 | While global macro can be accessed within whole processing. 33 | 34 | Local macro consists of two cases. First is automatically bound by processor 35 | and second is set by user. When processor processes texts, macro's argumens are 36 | expanded, tokenized and then mapped to local arguments. 37 | 38 | ``` 39 | $define(arg1=ARG1) 40 | $define(macro,arg1 arg2=$arg1() + $arg2()) 41 | % | | 42 | % Theses are the local macros and argument macros 43 | $arg1() 44 | $macro(first, second) 45 | % You cannot use local macro outside of the macro 46 | $arg2() 47 | === 48 | ARG1 49 | first + second 50 | Invalid macro name 51 | = Failed to invoke a macro : "arg2" 52 | ``` 53 | 54 | The other way to utilize a local macro is to use ```let``` macro. Let macro 55 | creates local macro that only persists for scope of the macro that used a let 56 | macro. 57 | 58 | ``` 59 | $define=( 60 | macro, 61 | a_arg1 a_arg2 62 | = 63 | $let(arg1,$a_arg1() is first) 64 | $let(arg2,$a_arg2() is second) 65 | % arg1 and arg2 is shadowed by new let binding 66 | $arg1() 67 | $arg2() 68 | ) 69 | $macro^(1,2) 70 | === 71 | 1 is first 72 | 2 is second 73 | ``` 74 | 75 | **Local macro is not expanded** 76 | 77 | The most important trait of local macro is that **local macro is not 78 | expanded**. Processor simply returns indexed body. Therefore if a local macro 79 | has it's content as a form of macro expressions, it will be printed as if it 80 | were escaped. 81 | 82 | ``` 83 | $define(demo,a_boolean=$if($a_boolean(),It is true)) 84 | $demo(\*$not(false)*\) 85 | === 86 | error: Invalid argument 87 | = If requires either true/false or zero/nonzero integer but given "$not(false)" 88 | --> test:2:2~~ 89 | ``` 90 | 91 | When "demo" was invked, it had bound ```\*$not(false)*\``` into an argument 92 | ```a_boolean```. The binding is technically same with let syntax. Let expands 93 | the target expression and assign it to a macro. 94 | 95 | In this case, text ```$not(false)``` was bound to a local macro ```a_boolean``` 96 | and inner macro ```if``` received it as a literal text not a expanded value of 97 | ```true```. 98 | 99 | On the other hand, global macros are macros that can be invoked from anywhere. 100 | Built-in macros, runtime macros are all global macros. 101 | 102 | ``` 103 | % These are all global macros 104 | $define(name=NAME) 105 | $name(/home/path) 106 | ``` 107 | 108 | ## Text and operation 109 | 110 | Some macros are text macro which expands to text while some are operational 111 | macros that changes a behaviour of processor. 112 | 113 | For example; hygiene, pause or relay are operational macros. Keep in mind that 114 | operational macros might not work as **procedurally** because operation macro 115 | applies before text is retrieved from macro call. 116 | 117 | ## Hook macro 118 | 119 | **Libary usage only** 120 | 121 | Hook macro is a macro that is called automatically. There are two types of hook 122 | macro. One is a ```macro hook``` and the other is ```char hook```. 123 | 124 | Macro hooks are called upon macro calls and char hooks are called upon char 125 | encounters. However char hook is only applied for plain texts which are not 126 | enclosed by a macro call. 127 | 128 | Hook macro should be registered before processing and can be enabled with 129 | ```hookon``` macro. 130 | 131 | for example, 132 | 133 | ```rust 134 | let processor = Processor::new(); 135 | 136 | processor.register_hook( 137 | HookType::Macro, // Macro type 138 | trigger_macro, // Macro that triggers 139 | "hook_div", // Macro to be executed 140 | 1, // target count 141 | false // Resetable 142 | ); 143 | processor.register_hook( 144 | HookType::Char, // Macro type 145 | '#', // Char that triggers 146 | "icon", // Macro to be executed 147 | 2, // target count 148 | true // Resetable 149 | ); 150 | ``` 151 | 152 | to enable this hook macro 153 | 154 | ```r4d 155 | $define(trigger_macro=Trigger) 156 | $define(hook_div= 157 |
I'm created
) 158 | $define(icon= ) 159 | $hookon(macro,trigger_macro) 160 | $hookon(char,#) 161 | $trigger_macro() 162 | ## I'm second header 163 | ## I'm another second header 164 | === 165 | Trigger 166 |
I'm created
167 | ## I'm second header 168 | ## I'm another second header 169 | ``` 170 | -------------------------------------------------------------------------------- /src/map/hookmap.rs: -------------------------------------------------------------------------------- 1 | //! Hook related data colletino structs 2 | 3 | use crate::{RadError, RadResult}; 4 | use std::collections::HashMap; 5 | 6 | /// Main hook collection map 7 | #[derive(Debug)] 8 | pub struct HookMap { 9 | macro_hook: HashMap, 10 | char_hook: HashMap, 11 | } 12 | 13 | impl HookMap { 14 | /// Create a new instance 15 | pub fn new() -> Self { 16 | Self { 17 | macro_hook: HashMap::new(), 18 | char_hook: HashMap::new(), 19 | } 20 | } 21 | 22 | /// Add macro count 23 | pub fn add_macro_count(&mut self, macro_name: &str) -> Option { 24 | if let Some(hook_state) = self.macro_hook.get_mut(macro_name) { 25 | if hook_state.enabled { 26 | hook_state.current_count += 1; 27 | if hook_state.current_count == hook_state.target_count { 28 | hook_state.current_count = 0; // reset count 29 | if !hook_state.resetable { 30 | hook_state.enabled = false; 31 | } 32 | return Some(hook_state.target_macro.clone()); 33 | } 34 | } 35 | } 36 | None 37 | } 38 | 39 | /// Add character count 40 | pub fn add_char_count(&mut self, target: char) -> Option { 41 | if let Some(hook_state) = self.char_hook.get_mut(&target) { 42 | if hook_state.enabled { 43 | hook_state.current_count += 1; 44 | if hook_state.current_count == hook_state.target_count { 45 | hook_state.current_count = 0; // reset count 46 | if !hook_state.resetable { 47 | hook_state.enabled = false; 48 | } 49 | return Some(hook_state.target_macro.clone()); 50 | } 51 | } 52 | } 53 | None 54 | } 55 | 56 | /// Switch a hook on/off 57 | pub fn switch_hook(&mut self, hook_type: HookType, index: &str, switch: bool) -> RadResult<()> { 58 | match hook_type { 59 | HookType::Macro => { 60 | if let Some(state) = self.macro_hook.get_mut(index) { 61 | state.enabled = switch 62 | } else { 63 | return Err(RadError::InvalidArgument(format!( 64 | "Hook trigger \"{}\" is not registered as macro hook", 65 | index 66 | ))); 67 | } 68 | } 69 | HookType::Char => { 70 | let index_char = if let Some(ch) = index.chars().next() { 71 | ch 72 | } else { 73 | return Err(RadError::HookMacroFail("Index is empty".to_owned())); 74 | }; 75 | 76 | if let Some(state) = self.char_hook.get_mut(&index_char) { 77 | state.enabled = switch 78 | } else { 79 | return Err(RadError::HookMacroFail(format!( 80 | "Hook trigger \"{}\" is not registered as character hook", 81 | index 82 | ))); 83 | } 84 | } 85 | }; 86 | Ok(()) 87 | } 88 | 89 | /// Add a new hook 90 | pub fn add_hook( 91 | &mut self, 92 | hook_type: HookType, 93 | target: &str, 94 | invoke_macro: &str, 95 | target_count: usize, 96 | resetable: bool, 97 | ) -> RadResult<()> { 98 | let hook_state = HookState::new(invoke_macro.to_owned(), target_count, resetable); 99 | match hook_type { 100 | HookType::Macro => { 101 | self.macro_hook.insert(target.to_owned(), hook_state); 102 | } 103 | HookType::Char => { 104 | let index_char = if let Some(ch) = target.chars().next() { 105 | ch 106 | } else { 107 | return Err(RadError::HookMacroFail("Index is empty".to_owned())); 108 | }; 109 | self.char_hook.insert(index_char, hook_state); 110 | } 111 | }; 112 | Ok(()) 113 | } 114 | 115 | /// Delete a hook 116 | pub fn del_hook(&mut self, hook_type: HookType, index: &str) -> RadResult<()> { 117 | match hook_type { 118 | HookType::Char => { 119 | self.macro_hook.remove(index); 120 | } 121 | HookType::Macro => { 122 | let index_char = if let Some(ch) = index.chars().next() { 123 | ch 124 | } else { 125 | return Err(RadError::HookMacroFail("Index is empty".to_owned())); 126 | }; 127 | self.char_hook.remove(&index_char); 128 | } 129 | }; 130 | Ok(()) 131 | } 132 | } 133 | 134 | /// Hook type 135 | #[derive(Debug)] 136 | pub enum HookType { 137 | Macro, 138 | Char, 139 | } 140 | 141 | impl std::str::FromStr for HookType { 142 | type Err = RadError; 143 | fn from_str(hook_type: &str) -> Result { 144 | let var = match hook_type.to_lowercase().as_str() { 145 | "macro" => Self::Macro, 146 | "char" => Self::Char, 147 | _ => { 148 | return Err(RadError::InvalidConversion(format!( 149 | "Invalid hook type \"{}\"", 150 | hook_type 151 | ))) 152 | } 153 | }; 154 | 155 | Ok(var) 156 | } 157 | } 158 | 159 | /// State of a hook macro 160 | #[derive(Debug)] 161 | pub struct HookState { 162 | enabled: bool, 163 | resetable: bool, 164 | target_macro: String, 165 | current_count: usize, 166 | target_count: usize, 167 | } 168 | 169 | impl HookState { 170 | /// Create a new instance 171 | pub fn new(target: String, target_count: usize, resetable: bool) -> Self { 172 | Self { 173 | target_macro: target, 174 | enabled: false, 175 | resetable, 176 | current_count: 0usize, 177 | target_count, 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/process/state.rs: -------------------------------------------------------------------------------- 1 | //! State struct of a processing 2 | 3 | use crate::auth::AuthFlags; 4 | #[cfg(not(feature = "wasm"))] 5 | use crate::common::FileTarget; 6 | use crate::common::RadResult; 7 | use crate::common::{ 8 | CommentType, ErrorBehaviour, FlowControl, Hygiene, ProcessInput, ProcessType, RelayTarget, 9 | }; 10 | use crate::consts::LINE_ENDING; 11 | use crate::RadError; 12 | use regex::Regex; 13 | use std::collections::{HashMap, HashSet}; 14 | #[cfg(not(feature = "wasm"))] 15 | use std::path::Path; 16 | use std::path::PathBuf; 17 | 18 | /// Processors processing state 19 | pub(crate) struct ProcessorState { 20 | // Current_input is either "stdin" or currently being read file's name thus it should not be a 21 | // path derivative 22 | pub auth_flags: AuthFlags, 23 | pub current_input: ProcessInput, 24 | pub input_stack: HashSet, 25 | pub newline: String, 26 | pub paused: bool, 27 | pub error_cache: Option, 28 | // This is reserved for hygienic execution 29 | pub hygiene: Hygiene, 30 | pub pipe_truncate: bool, 31 | pub pipe_map: HashMap, 32 | pub relay: Vec, 33 | pub sandbox: bool, 34 | pub behaviour: ErrorBehaviour, 35 | pub process_type: ProcessType, 36 | pub comment_type: CommentType, 37 | pub stream_state: StreamState, 38 | // Temp target needs to save both path and file 39 | // because file doesn't necessarily have path. 40 | // Especially in unix, this is not so an unique case 41 | #[cfg(not(feature = "wasm"))] 42 | pub temp_target: FileTarget, 43 | pub comment_char: Option, 44 | pub macro_char: Option, 45 | pub flow_control: FlowControl, 46 | pub deny_newline: bool, // This deny next-next newline 47 | pub consume_newline: bool, // This consumes newline if the line was only empty 48 | pub escape_newline: bool, // This escapes right next newline 49 | pub queued: Vec, 50 | pub regex_cache: RegexCache, 51 | pub lexor_escape_blanks: bool, 52 | } 53 | 54 | impl ProcessorState { 55 | /// Create a new instance 56 | pub fn new() -> Self { 57 | Self { 58 | current_input: ProcessInput::Stdin, 59 | input_stack: HashSet::new(), 60 | auth_flags: AuthFlags::new(), 61 | newline: LINE_ENDING.to_owned(), 62 | pipe_truncate: true, 63 | pipe_map: HashMap::new(), 64 | paused: false, 65 | error_cache: None, 66 | hygiene: Hygiene::None, 67 | relay: vec![], 68 | behaviour: ErrorBehaviour::Strict, 69 | process_type: ProcessType::Expand, 70 | comment_type: CommentType::None, 71 | stream_state: StreamState::default(), 72 | sandbox: false, 73 | #[cfg(not(feature = "wasm"))] 74 | temp_target: FileTarget::with_truncate(&std::env::temp_dir().join("rad.txt")).unwrap(), 75 | comment_char: None, 76 | macro_char: None, 77 | flow_control: FlowControl::None, 78 | deny_newline: false, 79 | consume_newline: false, 80 | escape_newline: false, 81 | queued: vec![], 82 | regex_cache: RegexCache::new(), 83 | lexor_escape_blanks: false, 84 | } 85 | } 86 | 87 | #[cfg(not(feature = "wasm"))] 88 | /// Internal method for setting temp target 89 | pub(crate) fn set_temp_target(&mut self, path: &Path) -> RadResult<()> { 90 | if path.exists() { 91 | std::fs::remove_file(path)?; 92 | } 93 | let new_target = FileTarget::with_truncate(path)?; 94 | self.temp_target = new_target; 95 | Ok(()) 96 | } 97 | 98 | /// Add a pipe with name 99 | /// 100 | /// THis will update the value if already exists 101 | pub fn add_pipe(&mut self, name: Option<&str>, value: String) { 102 | if let Some(name) = name { 103 | self.pipe_map.insert(name.to_owned(), value); 104 | } else { 105 | self.pipe_map.insert("-".to_owned(), value); 106 | } 107 | } 108 | 109 | /// Get a pipe with key 110 | pub fn get_pipe(&mut self, key: &str, ignore_truncate: bool) -> Option { 111 | if self.pipe_truncate && !ignore_truncate { 112 | self.pipe_map.remove(key) 113 | } else { 114 | self.pipe_map.get(key).map(|s| s.to_owned()) 115 | } 116 | } 117 | } 118 | 119 | /// Cache for regex compilation 120 | pub(crate) struct RegexCache { 121 | cache: HashMap, 122 | register: HashMap, 123 | } 124 | 125 | impl RegexCache { 126 | /// Create a new instance 127 | pub fn new() -> Self { 128 | Self { 129 | cache: HashMap::new(), 130 | register: HashMap::new(), 131 | } 132 | } 133 | 134 | /// Check if cache contains a key 135 | pub fn contains(&self, name: &str) -> bool { 136 | self.cache.contains_key(name) 137 | } 138 | 139 | /// Register a regex 140 | /// 141 | /// Registered regex is not cleared 142 | pub fn register(&mut self, name: &str, source: &str) -> RadResult<()> { 143 | self.cache.insert(name.to_string(), Regex::new(source)?); 144 | Ok(()) 145 | } 146 | 147 | /// Append a regex to cache 148 | pub fn append(&mut self, src: &str) -> RadResult<&Regex> { 149 | // Set hard capacity of 100 150 | if self.cache.len() > 100 { 151 | self.cache.clear(); 152 | } 153 | self.cache.insert(src.to_string(), Regex::new(src)?); 154 | Ok(self.get(src).unwrap()) 155 | } 156 | 157 | /// Get a regex with name 158 | pub fn get(&self, src: &str) -> Option<&Regex> { 159 | if self.register.get(src).is_some() { 160 | self.register.get(src) 161 | } else { 162 | self.cache.get(src) 163 | } 164 | } 165 | } 166 | 167 | #[derive(Debug, Default)] 168 | pub(crate) struct StreamState { 169 | pub on_stream: bool, 170 | pub as_lines: bool, 171 | pub macro_name_src: String, 172 | } 173 | 174 | impl StreamState { 175 | pub fn clear(&mut self) { 176 | self.on_stream = false; 177 | self.as_lines = false; 178 | self.macro_name_src.clear(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/parser/define_parser.rs: -------------------------------------------------------------------------------- 1 | //! Parser that processes macro definition 2 | 3 | use crate::utils::Utils; 4 | 5 | /// Struct for deinition parsing 6 | pub struct DefineParser { 7 | arg_cursor: DefineCursor, 8 | name: String, 9 | args: String, 10 | body: String, 11 | bind: bool, 12 | container: String, 13 | } 14 | 15 | impl DefineParser { 16 | /// Create a new instance 17 | pub fn new() -> Self { 18 | Self { 19 | arg_cursor: DefineCursor::Name, 20 | name: String::new(), 21 | args: String::new(), 22 | body: String::new(), 23 | bind: false, 24 | container: String::new(), 25 | } 26 | } 27 | 28 | /// Clear state 29 | fn clear(&mut self) { 30 | self.arg_cursor = DefineCursor::Name; 31 | self.name.clear(); 32 | self.args.clear(); 33 | self.body.clear(); 34 | self.bind = false; 35 | self.container.clear(); 36 | } 37 | 38 | /// Parse macro definition body 39 | /// 40 | /// NOTE: This method expects valid form of macro invocation 41 | /// which means given value should be presented without outer prentheses 42 | /// e.g. ) name,a1 a2=body text 43 | /// 44 | /// If definition doesn't comply with naming rules or syntaxes, if returnes "None" 45 | pub(crate) fn parse_define(&mut self, text: &str) -> Option<(String, String, String)> { 46 | self.clear(); // Start in fresh state 47 | let char_iter = text.chars().peekable(); 48 | for ch in char_iter { 49 | match self.arg_cursor { 50 | DefineCursor::Name => { 51 | if let ParseIgnore::Ignore = self.branch_name(ch) { 52 | continue; 53 | } 54 | // If not valid name return None 55 | if !self.is_valid_char(ch) { 56 | return None; 57 | } 58 | } 59 | DefineCursor::Args => { 60 | if let ParseIgnore::Ignore = self.branch_args(ch) { 61 | continue; 62 | } 63 | // If not valid name return None 64 | if !self.is_valid_char(ch) { 65 | return None; 66 | } 67 | } 68 | // Add everything 69 | DefineCursor::Body => (), 70 | } 71 | self.container.push(ch); 72 | } 73 | 74 | // This means pattern such as 75 | // $define(test,Test) 76 | // -> This is not a valid pattern 77 | // self.args.len() is 0, because 78 | // args are added only after equal(=) sign is detected 79 | if self.args.is_empty() && !self.bind { 80 | return None; 81 | } 82 | 83 | // End of body 84 | self.body.push_str(&self.container); 85 | Some((self.name.clone(), self.args.clone(), self.body.clone())) 86 | } 87 | 88 | /// Check if char complies with naming rule 89 | fn is_valid_char(&self, ch: char) -> bool { 90 | if self.container.is_empty() { 91 | // Start of string 92 | // Not alphabetic 93 | // $define( 1name ) -> Not valid 94 | if !ch.is_alphabetic() { 95 | return false; 96 | } 97 | } else { 98 | // middle of string 99 | // Not alphanumeric and not underscore 100 | // $define( na*1me ) -> Not valid 101 | // $define( na_1me ) -> Valid 102 | if !ch.is_alphanumeric() && ch != '_' { 103 | return false; 104 | } 105 | } 106 | true 107 | } 108 | 109 | // --------- 110 | // Start of branche methods 111 | // 112 | 113 | /// Branch on none 114 | fn branch_name(&mut self, ch: char) -> ParseIgnore { 115 | // $define(variable=something) 116 | // Don't set argument but directly bind variable to body 117 | if ch == '=' { 118 | self.name.push_str(&self.container); 119 | self.container.clear(); 120 | self.arg_cursor = DefineCursor::Body; 121 | self.bind = true; 122 | ParseIgnore::Ignore 123 | } else if Utils::is_blank_char(ch) { 124 | // This means pattern like this 125 | // $define( name ) -> name is registered 126 | // $define( na me ) -> na is ignored and take me instead 127 | if !self.name.is_empty() { 128 | self.container.clear(); 129 | ParseIgnore::None 130 | } else { 131 | // Ignore 132 | ParseIgnore::Ignore 133 | } 134 | } 135 | // Comma go to args 136 | else if ch == ',' { 137 | self.name.push_str(&self.container); 138 | self.container.clear(); 139 | self.arg_cursor = DefineCursor::Args; 140 | ParseIgnore::Ignore 141 | } else { 142 | ParseIgnore::None 143 | } 144 | } 145 | 146 | /// Branch on arguments 147 | fn branch_args(&mut self, ch: char) -> ParseIgnore { 148 | // Blank space separates arguments 149 | // TODO: Why check name's length? Is it necessary? 150 | if Utils::is_blank_char(ch) && !self.name.is_empty() { 151 | if !self.container.is_empty() { 152 | self.args.push_str(&self.container); 153 | self.args.push(' '); 154 | self.container.clear(); 155 | } 156 | ParseIgnore::Ignore 157 | } 158 | // Go to body 159 | else if ch == '=' { 160 | self.args.push_str(&self.container); 161 | self.container.clear(); 162 | self.arg_cursor = DefineCursor::Body; 163 | ParseIgnore::Ignore 164 | } 165 | // Others 166 | else { 167 | ParseIgnore::None 168 | } 169 | } 170 | 171 | // End of branche methods 172 | // 173 | // --------- 174 | } 175 | 176 | /// Cursor for definition parsing state 177 | pub(crate) enum DefineCursor { 178 | Name, 179 | Args, 180 | Body, 181 | } 182 | 183 | /// A state indicates whether to ignore a character or not 184 | enum ParseIgnore { 185 | Ignore, 186 | None, 187 | } 188 | -------------------------------------------------------------------------------- /docs/change.md: -------------------------------------------------------------------------------- 1 | # 3.1.1-rc1 2 | 3 | - Upgraded misc crate versions 4 | - Finally ditched lazy_static 5 | 6 | # 3.1 7 | 8 | * [x] Macros 9 | * [x] Strip macros 10 | * [x] Macro ergonomics 11 | * [x] Dryrun as passthrough 12 | * [x] Log flag to respect pipe input + print full attributes 13 | * [x] Mapf trims file name 14 | * [x] Bug fix 15 | * [x] Fixed trexter logger bug 16 | * [x] Fixed invlaid include directory 17 | * [x] You cannot safely use debug flag with pipe command. Added invalid error for stdin usage 18 | * [x] No such macro error is not properly printed 19 | * [x] Feature 20 | * [x] Passthrough 21 | * [x] Expose update method for radstorage 22 | * [x] Enable literal parenthesis for body : $lp() $rp() 23 | 24 | # 3.0.1 stable 25 | 26 | - New macro anon for anonymous macro 27 | - New macro scut for spaces cut 28 | - Now comma inside parenthesis is not treated as delimiter. 29 | - Strip has been merged with expand due to argument parsing logic change. 30 | - Changed name splitc -> cut 31 | 32 | # 3.0.0 ~~stable~~ 33 | 34 | - BUG : Fixed wrong initial input backtrace 35 | - CHG : Include behaviour 36 | - CHG : readto and readin behaviour 37 | - ERG : Now path macro converts to platform specific path separator 38 | - FET : New macros 39 | - Exist 40 | - Expand 41 | 42 | # 3.0.0-rc.6 43 | 44 | **rad** 45 | 46 | - BUG : Stdin input panicked 47 | - CHG : Now exit code is properly emmited on panic 48 | - CHG : Module changes 49 | - CHG : Now static macro is not expanded 50 | - ERG : Ditched cmp and added multiple comparision macros 51 | - ERG : Now documentation has 80 caps on description. 52 | - ERG : Warn readin on relaying 53 | - FET : Dry run 54 | - FET : Freeze flag refactored 55 | - FET : Packaging flag 56 | - FET : Negate macro attribute 57 | - FET : Pipe input macro attribute 58 | - FET : New macros 59 | - dump 60 | - map variants : map, mapf, mapl 61 | - grepmap 62 | - meta-processing related 63 | - require 64 | - strict 65 | - comment 66 | - sep 67 | - after 68 | - until 69 | - capture 70 | - indexl 71 | - splitc 72 | - gt, gte 73 | - lt, lte 74 | 75 | **rado** 76 | 77 | - FET : Package subcommand 78 | - FET : Execute subcommand 79 | 80 | # 3.0.0-rc.5 81 | 82 | **rad** 83 | 84 | - CHANGE : Refactored logger logics with crate trexter 85 | - CHANGE : Solved debugging regression in terms of functionality 86 | - CHANGE : Changed from's name to spread 87 | - CHANGE : Deterred macros' expansion order is not consistent with function macros 88 | - CHANGE : Removed ieval because counter replaces it 89 | - CHANGE : Signature and color is included default into a binary feature 90 | - ERGO : Append can get optional trailer argument 91 | - ERGO : Append now also appends to local macro 92 | - ERGO : Enable logm to print any local macros 93 | - ERGO : No Breakpoint warning 94 | - ERGO : Now foreach and forline get data as trimmed 95 | - ERGO : Queue to be insert as no stripped. 96 | - ERGO : Silent flag's default value is "any" 97 | - ERGO : Trim output now consumes new line if result is empty 98 | - FET : New macros 99 | - chars 100 | - cmp 101 | - comma 102 | - ftime 103 | - isempty 104 | - istype 105 | - iszero 106 | - loge 107 | - slice 108 | - squash 109 | - ssplit 110 | 111 | - Bug fix 112 | - Nested literal rule was not properly stripped 113 | - Setting an error option resetted a logger entirely 114 | - File operation was able to write to self 115 | - Fixed consume newline was not properly respected 116 | 117 | **rado** 118 | 119 | - Edit in place flag 120 | 121 | # 3.0.0-rc.4 122 | 123 | - FET : New macros 124 | - Escape blanks 125 | - Grep && Grepl 126 | - strip ( differnt from previous ) 127 | - Regexpr 128 | - Input 129 | - Temp 130 | - Trimla ( Trim line amount ) 131 | - Indent ( Indent lines ) 132 | - read\_to read\_in 133 | - join, joinl 134 | - notat 135 | - letr, staticr 136 | - counter 137 | - align 138 | - Tab && space && empty 139 | - CHG : Macro ergonomics 140 | - For variatns order changed backwards 141 | - Static trims value 142 | - Halt is queued by default 143 | - Changed fileout's argument order 144 | - Renamed arr to spilt 145 | - Removed sep macro because 146 | - Removed queries macro 147 | - Removed strip and stripl 148 | - Removed cnl 149 | - CHG : Changed argument parsing behaviour frome lexor and arg parser 150 | - CHG : Made formatter respect processor line ending 151 | - CHG : Now define macro also detects trim input attribute 152 | - CHG : Rad now deletes temp file before start 153 | - ERG : Improved descriptions a lot 154 | - ERG : Now comment can start in between line with any type 155 | - FET : METHOD > Set both comment and macro char at the same time 156 | - FET : New macro attribute "=" 157 | - Bug : Assert mode panicked 158 | - Bug : Error message cascaded as much as nested level 159 | - Bug : Exit macro yieled error and broke from entier rad process 160 | - Bug : Include's containder had high priority over relay target 161 | - Bug : Fasssert treated success as fail and vice versa 162 | 163 | 164 | # 3.0.0-rc.3 165 | 166 | ### Breaking 167 | 168 | - Changed syntax of regex macro 169 | - Ditched many Option parameters and made it intuitive 170 | 171 | ### Else 172 | 173 | - CHG : Changed a parsing logic little bit 174 | - CHG : Applied new clippy fix 175 | - FET : Manual flag 176 | - FET : RegexCache for better regex performance 177 | - FET : New macros 178 | - Find 179 | - FIndm 180 | 181 | # 3.0.0-rc.2 182 | 183 | - ERG : Many rustdoc improvement 184 | - FET : Extension macro configuration with script.rs file 185 | - BUG : Exit status handling 186 | - CHG : New template macro ```audit_auth``` 187 | - CHG : Moved from ```Vec<_>``` into ```&[]``` 188 | 189 | # 3.0.0-rc.1 190 | 191 | - ERG : All documentations for built-in macros 192 | - BUG : Forline macro fix 193 | 194 | # 3.0.0-rc.0 195 | 196 | [3.0 Changes](./3_0.md) 197 | 198 | ### ETC 199 | 200 | - New macros : import, source, cnl, listdir 201 | - Changed "enl" behaviour 202 | 203 | # 2.1.3 204 | 205 | Removed features are still included as empty placeholder for compatibility 206 | which will be removed in 3.0 207 | 208 | - BugFix : Hid unnecessary extra features from users 209 | - BugFix : ExtMacroBuilder's export has been feature gated by storage, 210 | - Ergono : Ditched avoidable dependencies 211 | - Thiserror 212 | - Csv 213 | - Lipsum 214 | - Ergono : Remove feature gates for better maintainability 215 | - Storage 216 | 217 | # 2.1.2 218 | 219 | - New macros 220 | - For loop nested mechanics with $:() macro 221 | - Changed macro concepts 222 | -------------------------------------------------------------------------------- /src/map/runtime_map.rs: -------------------------------------------------------------------------------- 1 | //! Runtime macro map 2 | //! 3 | //! Runtime macro is defined on runtime. 4 | 5 | use crate::common::Hygiene; 6 | use serde::{Deserialize, Serialize}; 7 | use std::collections::HashMap; 8 | 9 | /// Runtime macro 10 | #[derive(Clone, Deserialize, Serialize, Debug)] 11 | pub struct RuntimeMacro { 12 | pub name: String, 13 | pub args: Vec, 14 | pub body: String, 15 | pub desc: Option, 16 | pub is_static: bool, 17 | } 18 | 19 | impl RuntimeMacro { 20 | /// Create a new macro 21 | pub fn new(name: &str, args: &str, body: &str, is_static: bool) -> Self { 22 | // Empty args are no args 23 | let mut args: Vec = args 24 | .split_whitespace() 25 | .map(|item| item.to_owned()) 26 | .collect(); 27 | if args.len() == 1 && args[0].is_empty() { 28 | args = vec![] 29 | } 30 | 31 | RuntimeMacro { 32 | name: name.to_owned(), 33 | args, 34 | body: body.to_owned(), 35 | desc: None, 36 | is_static, 37 | } 38 | } 39 | } 40 | 41 | impl std::fmt::Display for RuntimeMacro { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | let mut inner = self 44 | .args 45 | .iter() 46 | .fold(String::new(), |acc, arg| acc + arg + ","); 47 | // This removes last "," character 48 | inner.pop(); 49 | write!(f, "${}({})", self.name, inner) 50 | } 51 | } 52 | 53 | #[cfg(feature = "signature")] 54 | impl From<&RuntimeMacro> for crate::sigmap::MacroSignature { 55 | fn from(mac: &RuntimeMacro) -> Self { 56 | let variant = if mac.is_static { 57 | crate::sigmap::MacroVariant::Static 58 | } else { 59 | crate::sigmap::MacroVariant::Runtime 60 | }; 61 | Self { 62 | variant, 63 | name: mac.name.to_owned(), 64 | args: mac.args.to_owned(), 65 | expr: mac.to_string(), 66 | desc: mac.desc.clone(), 67 | } 68 | } 69 | } 70 | 71 | /// Macro map for runtime macros 72 | #[derive(Clone, Debug)] 73 | pub(crate) struct RuntimeMacroMap { 74 | pub(crate) macros: HashMap, 75 | pub(crate) volatile: HashMap, 76 | } 77 | 78 | impl RuntimeMacroMap { 79 | /// Create a new instance 80 | pub fn new() -> Self { 81 | Self { 82 | macros: HashMap::new(), 83 | volatile: HashMap::new(), 84 | } 85 | } 86 | 87 | /// Clear runtime macros 88 | pub fn clear_runtime_macros(&mut self, volatile: bool) { 89 | if volatile { 90 | self.volatile.clear(); 91 | } else { 92 | self.macros.clear(); 93 | } 94 | } 95 | 96 | /// Check if macro exists 97 | pub fn contains(&self, key: &str, hygiene_type: Hygiene) -> bool { 98 | match hygiene_type { 99 | Hygiene::Aseptic => self.macros.contains_key(key), 100 | _ => self.macros.contains_key(key) || self.volatile.contains_key(key), 101 | } 102 | } 103 | 104 | /// Get macro by name 105 | pub fn get(&self, key: &str, hygiene_type: Hygiene) -> Option<&RuntimeMacro> { 106 | match hygiene_type { 107 | Hygiene::Aseptic => self.macros.get(key), 108 | _ => { 109 | let vol_runtime = self.volatile.get(key); 110 | 111 | if vol_runtime.is_none() { 112 | self.macros.get(key) 113 | } else { 114 | vol_runtime 115 | } 116 | } 117 | } 118 | } 119 | 120 | /// Get macro by name as mutable 121 | pub fn get_mut(&mut self, key: &str, hygiene_type: Hygiene) -> Option<&mut RuntimeMacro> { 122 | match hygiene_type { 123 | Hygiene::Aseptic => self.macros.get_mut(key), 124 | _ => { 125 | let vol_runtime = self.volatile.get_mut(key); 126 | 127 | if vol_runtime.is_none() { 128 | self.macros.get_mut(key) 129 | } else { 130 | vol_runtime 131 | } 132 | } 133 | } 134 | } 135 | 136 | /// Append a new macro to a map 137 | pub fn new_macro(&mut self, name: &str, mac: RuntimeMacro, hygiene_type: Hygiene) { 138 | if hygiene_type == Hygiene::None { 139 | self.macros.insert(name.to_string(), mac); 140 | } else { 141 | // If hygiene, insert into volatile 142 | self.volatile.insert(name.to_string(), mac); 143 | } 144 | } 145 | 146 | /// Remove a macro from a map 147 | pub fn undefine(&mut self, name: &str, hygiene_type: Hygiene) -> Option { 148 | if hygiene_type == Hygiene::None { 149 | self.macros.remove(name) 150 | } else { 151 | // If hygiene, insert into volatile 152 | self.volatile.remove(name) 153 | } 154 | } 155 | 156 | /// Rename a macro 157 | pub fn rename(&mut self, name: &str, new_name: &str, hygiene_type: Hygiene) -> bool { 158 | if hygiene_type == Hygiene::None { 159 | if let Some(mac) = self.macros.remove(name) { 160 | self.macros.insert(new_name.to_string(), mac); 161 | return true; 162 | } 163 | } else if let Some(mac) = self.volatile.remove(name) { 164 | self.volatile.insert(new_name.to_string(), mac); 165 | return true; 166 | } 167 | false 168 | } 169 | 170 | /// Append content to a macro 171 | pub fn append_macro(&mut self, name: &str, target: &str, hygiene_type: Hygiene) { 172 | if hygiene_type == Hygiene::None { 173 | if let Some(mac) = self.macros.get_mut(name) { 174 | mac.body.push_str(target); 175 | } 176 | } else if let Some(mac) = self.volatile.get_mut(name) { 177 | mac.body.push_str(target); 178 | } 179 | } 180 | 181 | /// Replace macro with new name 182 | pub fn replace_macro(&mut self, name: &str, target: &str, hygiene_type: Hygiene) { 183 | if hygiene_type == Hygiene::None { 184 | if let Some(mac) = self.macros.get_mut(name) { 185 | mac.body = target.to_string(); 186 | } 187 | } else if let Some(mac) = self.volatile.get_mut(name) { 188 | mac.body.push_str(target) 189 | } 190 | } 191 | 192 | /// Extend map with other hashmap 193 | pub fn extend_map(&mut self, map: HashMap, hygiene_type: Hygiene) { 194 | if hygiene_type == Hygiene::None { 195 | self.macros.extend(map) 196 | } else { 197 | self.volatile.extend(map) 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! R4d' error types 2 | //! 3 | //! R4d can have many errors because it utilizes multiple functions and 4 | //! external crates at the same time. 5 | 6 | use crate::AuthType; 7 | #[cfg(feature = "cindex")] 8 | use cindex::CIndexError; 9 | 10 | /// Blank implementation for error trait 11 | impl std::error::Error for RadError {} 12 | 13 | /// R4d's error type 14 | #[derive(Debug)] 15 | pub enum RadError { 16 | Interrupt, 17 | HookMacroFail(String), 18 | InvalidConversion(String), 19 | UnallowedChar(String), 20 | AssertFail, 21 | UnsoundExecution(String), 22 | InvalidExecution(String), 23 | InvalidCommandOption(String), 24 | EnvError(std::env::VarError), 25 | InvalidMacroReference(String), 26 | NoSuchMacroName(String, Option), 27 | InvalidMacroDefinition(String), 28 | InvalidRegex(regex::Error), 29 | #[cfg(feature = "evalexpr")] 30 | InvalidFormula(evalexpr::EvalexprError), 31 | InvalidArgument(String), 32 | InvalidArgInt(std::num::ParseIntError), 33 | InvalidArgBoolean(std::str::ParseBoolError), 34 | InvalidFile(String), 35 | StdIo(std::io::Error), 36 | FmtError(std::fmt::Error), 37 | Utf8Err(std::string::FromUtf8Error), 38 | UnsupportedTableFormat(String), 39 | BincodeError(String), 40 | PermissionDenied(String, AuthType), 41 | StrictPanic, 42 | ManualPanic(String), 43 | StorageError(String), 44 | #[cfg(feature = "cindex")] 45 | CIndexError(CIndexError), 46 | UnallowedMacroExecution(String), 47 | DcsvError(dcsv::DcsvError), 48 | #[cfg(feature = "clap")] 49 | RadoError(String), 50 | } 51 | impl std::fmt::Display for RadError { 52 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 53 | let text = match self { 54 | Self::Interrupt => String::from("Process Interrupted"), 55 | Self::HookMacroFail(txt) => format!("Hook macro error \n= {}", txt), 56 | Self::InvalidConversion(txt) => format!("Invalid conversion \n= {}", txt), 57 | Self::UnallowedChar(txt) => format!("Unallowed character \n= {}", txt), 58 | Self::AssertFail => "Assert failed".to_string(), 59 | Self::UnsoundExecution(err) => format!("Critical unsound execution error \n= {}", err), 60 | Self::InvalidExecution(err) => format!("Invalid execution error \n= {}", err), 61 | Self::InvalidCommandOption(command) => format!("Invalid command option\n= {}", command), 62 | Self::EnvError(env) => format!("Invalid environment name\n= {}", env), 63 | Self::InvalidMacroReference(err) => format!("Invalid macro reference\n= {}", err), 64 | Self::NoSuchMacroName(given, candidate) => match candidate { 65 | Some(cand) => { 66 | format!("No such macro name as \"{given}\", Did you mean \"{cand}\" ?") 67 | } 68 | None => { 69 | format!("No such macro name as \"{given}\"") 70 | } 71 | }, 72 | Self::InvalidMacroDefinition(err) => format!("Invalid macro definition\n= {}", err), 73 | Self::InvalidRegex(err) => format!("Failed regex operation\n= {}", err), 74 | #[cfg(feature = "evalexpr")] 75 | Self::InvalidFormula(err) => format!("Invalid formula\n= {}", err), 76 | Self::InvalidArgument(arg) => format!("Invalid argument\n= {}", arg), 77 | Self::InvalidArgInt(err) => format!("Invalid argument type\n= {}", err), 78 | Self::InvalidArgBoolean(err) => format!("Invalid argument type\n= {}", err), 79 | Self::InvalidFile(file) => format!("File,\"{}\", does not exist", file), 80 | Self::StdIo(err) => format!("Standard IO error\n= {}", err), 81 | Self::FmtError(err) => format!("Formatting error\n= {}", err), 82 | Self::Utf8Err(err) => format!("Failed to convert to utf8 string\n= {}", err), 83 | Self::UnsupportedTableFormat(txt) => format!("Unsupported table format\n= {}", txt), 84 | Self::BincodeError(txt) => format!("Failed frozen operation\n= {}", txt), 85 | Self::PermissionDenied(txt, atype) => format!( 86 | "Permission denied for \"{0}\". Use a flag \"-a {1:?}\" to allow this macro.", 87 | txt, atype 88 | ), 89 | Self::StrictPanic => "Every error is panicking in strict mode".to_string(), 90 | Self::ManualPanic(txt) => format!("Panic triggered with message\n^^^ {} ^^^", txt), 91 | Self::StorageError(txt) => format!("Storage error with message\n= {0}", txt), 92 | #[cfg(feature = "cindex")] 93 | Self::CIndexError(err) => err.to_string(), 94 | Self::UnallowedMacroExecution(txt) => { 95 | format!("Macro execution is not allowed\n= {0}", txt) 96 | } 97 | Self::DcsvError(err) => format!("{}", err), 98 | #[cfg(feature = "clap")] 99 | Self::RadoError(err) => format!("Rado error \n= {}", err), 100 | }; 101 | write!(f, "{}", text) 102 | } 103 | } 104 | 105 | // ========== 106 | // Start of Convert variations 107 | // 108 | impl From for RadError { 109 | fn from(err: regex::Error) -> Self { 110 | Self::InvalidRegex(err) 111 | } 112 | } 113 | 114 | impl From for RadError { 115 | fn from(err: dcsv::DcsvError) -> Self { 116 | Self::DcsvError(err) 117 | } 118 | } 119 | 120 | #[cfg(feature = "evalexpr")] 121 | impl From for RadError { 122 | fn from(err: evalexpr::EvalexprError) -> Self { 123 | Self::InvalidFormula(err) 124 | } 125 | } 126 | 127 | impl From for RadError { 128 | fn from(err: std::num::ParseIntError) -> Self { 129 | Self::InvalidArgInt(err) 130 | } 131 | } 132 | 133 | impl From for RadError { 134 | fn from(err: std::str::ParseBoolError) -> Self { 135 | Self::InvalidArgBoolean(err) 136 | } 137 | } 138 | 139 | impl From for RadError { 140 | fn from(err: std::io::Error) -> Self { 141 | Self::StdIo(err) 142 | } 143 | } 144 | 145 | impl From for RadError { 146 | fn from(err: std::fmt::Error) -> Self { 147 | Self::FmtError(err) 148 | } 149 | } 150 | 151 | impl From for RadError { 152 | fn from(err: std::string::FromUtf8Error) -> Self { 153 | Self::Utf8Err(err) 154 | } 155 | } 156 | 157 | impl From for RadError { 158 | fn from(err: std::env::VarError) -> Self { 159 | Self::EnvError(err) 160 | } 161 | } 162 | 163 | #[cfg(feature = "cindex")] 164 | impl From for RadError { 165 | fn from(err: CIndexError) -> Self { 166 | Self::CIndexError(err) 167 | } 168 | } 169 | // End of convert variations 170 | // 171 | // ---------- 172 | -------------------------------------------------------------------------------- /docs/r4d_internal.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | 3 | These internals are all based on 3.0 version. 4 | 5 | # Table of Contents 6 | 7 | - [R4d recursively finds and expands 8 | macros](#r4d-recursively-finds-and-expands-macros) 9 | - [Nature of macro types](#nature-of-macro-types) 10 | - [Runtime macro](#Runtime-macro) 11 | - [Function macro](#function-macro) 12 | - [Deterred macro](#deterred-macro) 13 | - [Errors](#errors) 14 | 15 | # R4d recursively finds and expands macros 16 | 17 | R4d's main logic is a method called ```process_buffer```. The method iterates 18 | lines from a given buffer and checks if matching macro syntax is detected. If 19 | texts are not part of macro syntax, r4d simply prints the line without any 20 | processing. 21 | 22 | When a macro's syntax is detected, r4d saves partial data as a macro fragment. 23 | If the macro fragment becomes complete without any exit condition, then r4d 24 | tries to expand the macro. R4d basically indexs macro name from internal 25 | hashmap which contains all information about macros. If no such entry was 26 | found, r4d yields error. After a macro name and a arguements was successfully 27 | retrieved, r4d first expands macro's argument. And of course, the arguments can 28 | have macro syntax inside. Therefore the expansion process is a recursive 29 | procedure. 30 | 31 | ```txt 32 | $macro_name( -> On dollar character, a framgent start 33 | Text argment -> Values are all saved into a framgent 34 | ) -> A fragment completes on ending parenthesis 35 | ``` 36 | 37 | Expanded arguments' direction differs by macro types. If a macro is a runtime 38 | macro, the arguments are split by a length which is defined by a user, and then 39 | maps each into a local macro with a name from parameters. For a function macro, 40 | burden of argument spliting is transferred to each function because the length 41 | is not defined at constnat time. Deterred macro is also not different. 42 | 43 | ```r4d 44 | $define(macro,a b c=$a() $b() $c()) 45 | $define(arg=1,2,3) 46 | $macro($arg()) 47 | 48 | % Expanded arguments are mapped to parameters 49 | % $arg() == 1,2,3 50 | % | | | 51 | % a b c 52 | ``` 53 | 54 | Unlike other macros local macro is not expanded on invocation, because local 55 | macros are mapped with expanded arguments. This might not look reasonable in 56 | some cases but very much plausible. If a local macro was expanded, then macro 57 | arguments will consequently doubly expanded. 58 | 59 | # Nature of macro types 60 | 61 | A runtime macro is technically an expressive and featureful format macro. A 62 | runtime macro defines a final output as a macro body with punch holes. Those 63 | punch holes, or namely other macros, are expanded on runtime, thus called a 64 | runtime macro. 65 | 66 | In contrast, function macros are higher wrapper around function pointers. R4d 67 | is loaded with many built-in macros which means there are equal amount of 68 | functions mapped to those macros. Since function macro is a rust function, it 69 | can extensively benefit from rust's functionality or ecosystem. 70 | 71 | A deterred macro, works similary in a sesne that it is mapped to a function 72 | pointer. However it's internal logic is quite different. A deterred macro 73 | prevents expansin of arguments and gladly bears a burden to expand by itself. 74 | In exchange of complexity, deterred macro gains a power to optionally expand 75 | arguments or even dynamic contents. 76 | 77 | # Macro expansion 78 | 79 | ## Runtime macro 80 | 81 | After arguments are mapped to each parameters, a body of a runtime macro is 82 | then expanded. Since body of definition is not expanded on declaration, local 83 | macro is expanded properly. The expanded body is returned to invocated 84 | position. Runtime macro's body is a type of ```String``` which means that a 85 | runtime macro cannot return nothing but always a container with empty value. 86 | For the reason, a runtime macro that seemingly returns nothing leaves a newline 87 | in its place. 88 | 89 | ```txt 90 | % Test leaves empty line 91 | % while define leaves nothing in its place 92 | $define(test=) 93 | $test() 94 | === 95 | 96 | ``` 97 | 98 | ## Function macro 99 | 100 | Function macro's expansion is totally dependent on mapped function's behaviour. 101 | For example, regex macro splits arguments with comma and treat first argument 102 | as expression, second argument as substitute text, and finally third argument 103 | as a source text to process. Regex expression is compiled into a regex program, 104 | or retrieved from cache if the expression was compiled before. And finally 105 | function redirects each components to regex's replace methods. Technically 106 | regex macro does the following pseudo code. 107 | 108 | ```rust 109 | let (expr,sub,source) = split_and_get_arguments(&args); 110 | let regex = try_get_cache_or_compile(&expr); 111 | let result = regex.replace_all(source,sub) 112 | 113 | return Some(result); 114 | ``` 115 | 116 | After the macro function does its job, it returns an option of string. Which 117 | can be either ```Some(value)``` or ```None```. Therefore function macro can 118 | return namely null value. In turn, function macro can leaves nothing behind. 119 | 120 | ```r4d 121 | % Sequences of macros leaves literally nothing 122 | $define(test=) 123 | $clear() 124 | $rename(test,TEST) 125 | $undef(TEST) 126 | === 127 | ``` 128 | 129 | Meanwhile, a standard argument split procedure strips literal quotes from given 130 | arguments. So that a function macro can process arguments' pure form without 131 | unncessary literal quotes. The same rule is applied to deterred macro. 132 | Arguments are expanded and then stripped regardless of expansion order. ( 133 | Although there are special cases like que macro ) 134 | 135 | ## Deterred macro 136 | 137 | Deterred macro has more powers over simple function macros. Deterred can 138 | capture context of a macro was invoked. Such ability enables deterred macro to 139 | determine whether a macro is valid in a context of invocation nested level, 140 | expand a dyanimc expression which is created inside of a function, and 141 | retreive information about parent's local macro. There is no way for a function 142 | macro to understand a local macro, while deterred macro can directly index, 143 | add, and modify them. 144 | 145 | Deterred macro can decide an order of expansion. Every if macro variants are 146 | deterred macro, because it needs to optionally expand macro arguments. ```If``` 147 | macro will epxand other arguments only when given conition is met. If the macro 148 | was implemented as a function macro, the expression would have been executed 149 | anyway which is not desirable for user experience. This is especially 150 | troublesome when the expression includes operating macros such as include or 151 | fileout. 152 | 153 | As stated above, deterred macro can register a new local macro. ```For``` macro 154 | variants gets a benefit from this functionality. ```For``` macro has a common 155 | denominator of splitting contents. A rule varies by a macro, but all of them 156 | creates an iterable object. After split, the macro registers a local macro 157 | named ```:``` and expand a given body text. This is the reason why users can 158 | use an undefined dynamic local macro inside for macro variants' body argument. 159 | Think of it as a capture group of regular expressions. 160 | 161 | Dynamic expression "can" be expanded by a function macro, but the macro cannot 162 | capture a context of an invocation. Any expression can include local macros 163 | which are scope specific and also not known to a function macro. As a result, 164 | macros like exec or spread are deterred macro to enable a sane macro expansion. 165 | 166 | # Errors 167 | 168 | There are two types errors in r4d. One is an unallowed logic error which is 169 | decided by r4d processor. Such error wouldn't always panick a program but 170 | either has a probability to or susceptible to unintended result. The other is 171 | panicking error, which is mosty occured by internal usage of panickable 172 | functions. Those errors are not handled by r4d because there are all sorts of 173 | functions thus the erorr is simpled redirected with ? operator. Logic error 174 | tries to express an error with details but panick errors sometimes can yield 175 | errors that is not so helpful. 176 | -------------------------------------------------------------------------------- /src/lexor.rs: -------------------------------------------------------------------------------- 1 | //! # Lexor module 2 | //! 3 | //! This is not about lexing(compiler) but a character validation. 4 | //! 5 | //! There might be conceptual resemblence however I had never learnt compiler before. 6 | //! 7 | //! Lexor carries lexor cursor(state) and validates if given character is valid and whether the 8 | //! character should be saved as a fragment of macro. 9 | 10 | use crate::common::CommentType; 11 | use crate::consts::*; 12 | use crate::utils::Utils; 13 | 14 | /// Struct that validats a given character 15 | pub struct Lexor { 16 | previous_char: Option, 17 | inner_parse: bool, 18 | cursor: Cursor, 19 | literal_count: usize, // Literal nest level 20 | parenthesis_count: usize, // Parenthesis nest level 21 | macro_char: char, 22 | comment_char: Option, 23 | consume_previous: bool, 24 | consume_blank: bool, 25 | } 26 | 27 | impl Lexor { 28 | /// Create a new instance 29 | pub fn new(macro_char: char, comment_char: char, comment_type: &CommentType) -> Self { 30 | let comment_char = if let CommentType::Any = comment_type { 31 | Some(comment_char) 32 | } else { 33 | None 34 | }; 35 | Lexor { 36 | previous_char: None, 37 | inner_parse: false, 38 | cursor: Cursor::None, 39 | literal_count: 0, 40 | parenthesis_count: 0, 41 | macro_char, 42 | comment_char, 43 | consume_previous: false, 44 | consume_blank: false, 45 | } 46 | } 47 | 48 | /// Consume following blank characters 49 | pub fn consume_blank(&mut self) { 50 | self.consume_blank = true; 51 | } 52 | 53 | /// This sets inner rules 54 | pub fn set_inner(&mut self) { 55 | self.inner_parse = true; 56 | } 57 | 58 | /// Reset lexor state 59 | pub fn reset(&mut self) { 60 | self.previous_char = None; 61 | self.cursor = Cursor::None; 62 | self.parenthesis_count = 0; 63 | self.consume_previous = false; 64 | } 65 | 66 | /// Validate the character 67 | pub fn lex(&mut self, ch: char) -> LexResult { 68 | if self.consume_blank && Utils::is_blank_char(ch) { 69 | return LexResult::Ignore; 70 | } else if self.consume_blank { 71 | self.consume_blank = false; 72 | } 73 | // Literal related 74 | if self.start_literal(ch) || self.end_literal(ch) { 75 | self.previous_char.replace('0'); 76 | return LexResult::Literal(self.cursor); 77 | } else if self.literal_count > 0 { 78 | self.previous_char.replace(ch); 79 | return LexResult::Literal(self.cursor); 80 | } 81 | 82 | // Exit if comment_type is configured 83 | // cch == comment char 84 | if let Some(cch) = self.comment_char { 85 | if cch == ch { 86 | self.reset(); 87 | return LexResult::CommentExit; 88 | } 89 | } 90 | 91 | // Non literal related logics 92 | let result = match self.cursor { 93 | Cursor::None => self.branch_none(ch), 94 | Cursor::Name => self.branch_name(ch), 95 | Cursor::Arg => self.branch_arg(ch), 96 | }; // end arg match 97 | 98 | if self.consume_previous { 99 | self.previous_char.replace(' '); 100 | self.consume_previous = false; 101 | } else { 102 | let replace = ch; 103 | self.previous_char.replace(replace); 104 | } 105 | result 106 | } 107 | 108 | // ---------- 109 | // 110 | // Branch methods start 111 | 112 | /// Branch on none state 113 | fn branch_none(&mut self, ch: char) -> LexResult { 114 | let result: LexResult; 115 | if ch == self.macro_char && self.previous_char.unwrap_or('0') != ESCAPE_CHAR { 116 | self.cursor = Cursor::Name; 117 | result = LexResult::Ignore; 118 | } else if self.inner_parse 119 | && ch == ESCAPE_CHAR 120 | && self.previous_char.unwrap_or(' ') == ESCAPE_CHAR 121 | { 122 | // On inner parse, \\* is interpreted as \* 123 | // If current ch is \ and previous was also \ consume previous and paste it 124 | self.consume_previous = true; 125 | result = LexResult::AddToRemainder; 126 | } 127 | // Characters other than newline means other characters has been introduced 128 | else { 129 | result = LexResult::AddToRemainder; 130 | } 131 | result 132 | } 133 | 134 | /// Branch on name state 135 | fn branch_name(&mut self, ch: char) -> LexResult { 136 | let mut result: LexResult; 137 | 138 | // Blank characters are invalid 139 | if Utils::is_blank_char(ch) { 140 | self.cursor = Cursor::None; 141 | result = LexResult::ExitFrag; 142 | } 143 | // Left parenthesis trigger macro invocation 144 | else if ch == '(' { 145 | self.cursor = Cursor::Arg; 146 | self.parenthesis_count = 1; 147 | result = LexResult::StartFrag; 148 | // Empty name 149 | if self.previous_char.unwrap_or('0') == self.macro_char { 150 | result = LexResult::EmptyName; 151 | } 152 | } else if ch == self.macro_char { 153 | result = LexResult::RestartName; 154 | } else { 155 | result = LexResult::AddToFrag(Cursor::Name); 156 | } 157 | result 158 | } 159 | 160 | /// Branch on arg state 161 | fn branch_arg(&mut self, ch: char) -> LexResult { 162 | let mut result: LexResult = LexResult::AddToFrag(Cursor::Arg); 163 | // Escape parenthesis doesn't end macro fragment. 164 | if self.previous_char.unwrap_or('0') == ESCAPE_CHAR && (ch == ')' || ch == '(') { 165 | return result; 166 | } 167 | // Right paren decreases paren_count 168 | if ch == ')' { 169 | self.parenthesis_count -= 1; 170 | if self.parenthesis_count == 0 { 171 | self.cursor = Cursor::None; 172 | result = LexResult::EndFrag; 173 | } 174 | } 175 | // Left paren increases paren_count 176 | else if ch == '(' { 177 | self.parenthesis_count += 1; 178 | } else if ch == ESCAPE_CHAR && self.previous_char.unwrap_or(' ') == ESCAPE_CHAR { 179 | // If current ch is \ and previous was also \ consume previous and paste it 180 | self.consume_previous = true; 181 | } 182 | // Other characters are added normally 183 | result 184 | } 185 | 186 | // End of branch methods 187 | // 188 | // ---------- 189 | 190 | /// Check if given character set starts a literal state 191 | fn start_literal(&mut self, ch: char) -> bool { 192 | // if given value is literal character and preceding character is escape 193 | if ch == LIT_CHAR && self.previous_char.unwrap_or('0') == ESCAPE_CHAR { 194 | self.literal_count += 1; 195 | true 196 | } else { 197 | false 198 | } 199 | } 200 | 201 | /// Check if given character set end a literal state 202 | fn end_literal(&mut self, ch: char) -> bool { 203 | // if given value is literal character and preceding character is escape 204 | if ch == ESCAPE_CHAR && self.previous_char.unwrap_or('0') == LIT_CHAR { 205 | if self.literal_count > 0 { 206 | self.literal_count -= 1; 207 | } // else it is simply a *\ without starting \* 208 | true 209 | } else { 210 | false 211 | } 212 | } 213 | 214 | /// Check if lexor is on literal 215 | pub fn on_literal(&self) -> bool { 216 | self.literal_count > 0 217 | } 218 | } 219 | 220 | /// Result of a lex operation 221 | #[derive(Debug)] 222 | pub enum LexResult { 223 | Ignore, 224 | AddToRemainder, 225 | StartFrag, 226 | EmptyName, 227 | RestartName, 228 | AddToFrag(Cursor), 229 | EndFrag, 230 | ExitFrag, 231 | Literal(Cursor), 232 | CommentExit, 233 | } 234 | 235 | /// Cursor that carries state information of lexor 236 | #[derive(Clone, Copy, Debug)] 237 | pub enum Cursor { 238 | None, 239 | Name, 240 | Arg, 241 | } 242 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ### Rad binary 4 | 5 | ```bash 6 | # Usage : rad [OPTIONS] [FILE]... 7 | 8 | # Read from file and save to file 9 | rad input_file.txt -o out_file.txt 10 | 11 | # Read from file and print to stdout 12 | rad input_file.txt 13 | 14 | # Read from standard input and print to file 15 | printf 'text' | rad -o out_file.txt 16 | 17 | # Read from stdin and print to stdout 18 | printf 'text' | rad 19 | 20 | # Print simple manual 21 | rad --man 22 | rad --man ifdef 23 | 24 | # Use comment in input texts 25 | # Comment character is '%' 26 | # Refer macro_syntax for further information 27 | rad --comment 28 | rad --comment any 29 | 30 | # Some macros need permission to process 31 | # use following options to grant permission. 32 | # Permission argument is case insensitive 33 | -a env # Give environment permission 34 | -a cmd # Give syscmd permission 35 | -a fin+fout # give both file read and file write permission 36 | -A # Give all permission. this is same with '-a env+cmd+fin+fout' 37 | -w env # Give permission but warn when macro is used 38 | -W # Same with '-A' but for warning 39 | 40 | # Use following options to decide error behaviours 41 | # default is stderr 42 | -e, --err # Log error to 43 | -s, --silent