55//! extracted by a [Structex][crate::Structex].
66//!
77//! # Syntax
8- //! The syntax supported for templating is extremely minimal and focuses solely on injecting
9- //! submatches into a user provided template. In its simplest form a template is simply a string
10- //! literal, but the syntax also supports referencing submatches by their index. To inject the
11- //! contents of a submatch at a particular point within a template, place the capture index inside
12- //! of curly braces: `"submatch 1 is {1} and submatch 2 is {2}"`.
8+ //! The syntax supported for templating is extremely minimal and focuses on injecting submatches
9+ //! into a user provided template. In its simplest form a template is simply a string literal, but
10+ //! the syntax also supports referencing submatches by their index. To inject the contents of a
11+ //! submatch at a particular point within a template, place the capture index inside of curly
12+ //! braces: `"submatch 1 is {1} and submatch 2 is {2}"`. It is also permitted to reference
13+ //! arbitrary variables within a template provided they are rendered with either the
14+ //! [render_with_context][Template::render_with_context] or
15+ //! [render_with_context_to][Template::render_with_context_to] methods.
1316//!
1417//! ## Syntax Errors
15- //! It is an error to have an unclosed `{}` pair within a template or to place something other than
16- //! a capture index inside of curly braces. To escape a curly brace, place a `\` before the opening
17- //! brace. Closing braces do not need to be escaped.
18+ //! It is an error to have an unclosed `{}` pair within a template. To escape a curly brace, place
19+ //! a `\` before the opening brace. Closing braces do not need to be escaped.
1820//!
1921//! ```
2022//! use structex::template::Template;
4244//! // The same reference multiple times
4345//! assert!(Template::parse("{1}, {1}!").is_ok());
4446//!
47+ //! // Something other than a capture index as a reference
48+ //! assert!(Template::parse("{foo}").is_ok());
49+ //!
4550//!
4651//! // The following are all invalid templates
4752//!
5055//!
5156//! // An unknown escape sequence
5257//! assert!(Template::parse("\\[").is_err());
53- //!
54- //! // Something other than a capture index as a reference
55- //! assert!(Template::parse("{foo}").is_err());
5658//! ```
5759//!
5860//! # Usage
104106//! ]
105107//! );
106108//! ```
109+ //!
110+ //! To render values other than captures from a Structex, implement the [Context] trait:
111+ //! ```
112+ //! use structex::{Structex, template::{Context, Template}};
113+ //! use regex::Regex;
114+ //! use std::io::{self, Write};
115+ //!
116+ //! struct Ctx;
117+ //!
118+ //! impl Context for Ctx {
119+ //! fn render_var<W>(&self, var: &str, w: &mut W) -> io::Result<usize>
120+ //! where
121+ //! W: io::Write
122+ //! {
123+ //! match var {
124+ //! "foo" => w.write(b"last word"),
125+ //! "bar" => w.write(b"first word"),
126+ //! _ => Err(io::Error::other("unknown variable"))
127+ //! }
128+ //! }
129+ //! }
130+ //!
131+ //! let haystack = r#"This is a multi-line
132+ //! string that mentions peoples names.
133+ //! People like Alice and Bob. People
134+ //! like Claire and David, but really
135+ //! we're here to talk about Alice.
136+ //! Alice is everyone's friend."#;
137+ //!
138+ //! let se: Structex<Regex> = Structex::new(r#"
139+ //! x/(.|\n)*?\./ {
140+ //! g/Alice/ n/(\w+)\./ p/The {foo} is '{1}'/;
141+ //! v/Alice/ n/(\w+)/ p/The {bar} is '{1}'/;
142+ //! }
143+ //! "#).unwrap();
144+ //!
145+ //! // Parse and register the templates
146+ //! let templates: Vec<Template> = se
147+ //! .actions()
148+ //! .iter()
149+ //! .map(|action| Template::parse(action.arg().unwrap()).unwrap())
150+ //! .collect();
151+ //!
152+ //!
153+ //! let output: Vec<String> = se
154+ //! .iter_tagged_captures(haystack)
155+ //! .map(|caps| {
156+ //! let id = caps.id().unwrap();
157+ //! templates[id].render_with_context(&caps, &Ctx).unwrap()
158+ //! })
159+ //! .collect();
160+ //!
161+ //! assert_eq!(
162+ //! &output,
163+ //! &[
164+ //! "The first word is 'This'",
165+ //! "The last word is 'Bob'",
166+ //! "The last word is 'Alice'",
167+ //! "The last word is 'friend'",
168+ //! ]
169+ //! );
170+ //! ```
171+
107172use crate :: {
108173 parse:: { self , ParseInput } ,
109174 re:: { Sliceable , Writable } ,
@@ -154,6 +219,8 @@ enum Fragment {
154219 Esc ( char ) ,
155220 /// Sub-string of the raw template that should be rendered as-is
156221 Lit ( usize , usize ) ,
222+ /// Sub-string of the raw template that should be passed to a context for rendering
223+ Var ( usize , usize ) ,
157224}
158225
159226/// A parsed string template that can be rendered for a given [TaggedCaptures].
@@ -205,14 +272,16 @@ impl Template {
205272 '{' => {
206273 push_lit ( offset, & input, & mut fragments) ;
207274 input. advance ( ) ;
275+ offset = input. offset ( ) ;
208276
209277 match input. read_until ( '}' ) {
210278 Some ( s) => {
211- let i: usize = s
212- . parse ( )
213- . map_err ( |_| error ( ErrorKind :: InvalidVariable ( s. to_string ( ) ) ) ) ?;
279+ let frag = match s. parse :: < usize > ( ) {
280+ Ok ( i) => Fragment :: Cap ( i) ,
281+ Err ( _) => Fragment :: Var ( offset, input. offset ( ) ) ,
282+ } ;
214283
215- fragments. push ( Fragment :: Cap ( i ) ) ;
284+ fragments. push ( frag ) ;
216285 }
217286 None => return Err ( error ( ErrorKind :: UnexpectedEof ) ) ,
218287 }
@@ -270,11 +339,68 @@ impl Template {
270339 to - from
271340 }
272341 Fragment :: Esc ( ch) => w. write ( ch. encode_utf8 ( & mut [ 0 ; 1 ] ) . as_bytes ( ) ) ?,
273- Fragment :: Cap ( n) => {
274- match caps. submatch_text ( * n) {
275- Some ( slice) => slice. write_to ( w) ?,
276- None => 0 , // drop unknown capture references as ""
277- }
342+ Fragment :: Cap ( n) => match caps. submatch_text ( * n) {
343+ Some ( slice) => slice. write_to ( w) ?,
344+ None => w. write ( format ! ( "{{{n}}}" ) . as_bytes ( ) ) ?,
345+ } ,
346+ Fragment :: Var ( from, to) => {
347+ w. write ( format ! ( "{{{}}}" , & self . raw[ * from..* to] ) . as_bytes ( ) ) ?
348+ }
349+ } ;
350+ }
351+
352+ Ok ( n)
353+ }
354+
355+ /// Render this template directly to a newly created [String] using the given [TaggedCaptures]
356+ /// and context.
357+ ///
358+ /// Returns an error if any of the underlying write calls fail.
359+ ///
360+ /// To render to an arbitrary writer instead of a string, see the
361+ /// [render_to][Template::render_to] method.
362+ pub fn render_with_context < H , C > ( & self , caps : & TaggedCaptures < H > , ctx : & C ) -> io:: Result < String >
363+ where
364+ H : Sliceable ,
365+ C : Context ,
366+ {
367+ let mut buf = Vec :: with_capacity ( self . raw . len ( ) * 2 ) ;
368+ self . render_with_context_to ( & mut buf, caps, ctx) ?;
369+
370+ Ok ( String :: from_utf8 ( buf) . unwrap ( ) )
371+ }
372+
373+ /// Render this template to the provided writer using the given [TaggedCaptures] and Context.
374+ ///
375+ /// Returns an error if any of the underlying write calls fail.
376+ ///
377+ /// To render directly to a [String], see the [render][Template::render] method.
378+ pub fn render_with_context_to < H , W , C > (
379+ & self ,
380+ w : & mut W ,
381+ caps : & TaggedCaptures < H > ,
382+ ctx : & C ,
383+ ) -> io:: Result < usize >
384+ where
385+ H : Sliceable ,
386+ W : Write ,
387+ C : Context ,
388+ {
389+ let mut n = 0 ;
390+ for frag in self . fragments . iter ( ) {
391+ n += match frag {
392+ Fragment :: Lit ( from, to) => {
393+ w. write_all ( & self . raw . as_bytes ( ) [ * from..* to] ) ?;
394+ to - from
395+ }
396+ Fragment :: Esc ( ch) => w. write ( ch. encode_utf8 ( & mut [ 0 ; 1 ] ) . as_bytes ( ) ) ?,
397+ Fragment :: Cap ( n) => match caps. submatch_text ( * n) {
398+ Some ( slice) => slice. write_to ( w) ?,
399+ None => w. write ( format ! ( "{{{n}}}" ) . as_bytes ( ) ) ?,
400+ } ,
401+ Fragment :: Var ( from, to) => {
402+ let var = & self . raw [ * from..* to] ;
403+ ctx. render_var ( var, w) ?
278404 }
279405 } ;
280406 }
@@ -283,6 +409,12 @@ impl Template {
283409 }
284410}
285411
412+ pub trait Context {
413+ fn render_var < W > ( & self , var : & str , w : & mut W ) -> io:: Result < usize >
414+ where
415+ W : Write ;
416+ }
417+
286418#[ cfg( test) ]
287419mod tests {
288420 use super :: * ;
@@ -294,6 +426,7 @@ mod tests {
294426 Cap ( usize ) ,
295427 Lit ( & ' a str ) ,
296428 Esc ( char ) ,
429+ Var ( & ' a str ) ,
297430 }
298431
299432 use Tag :: * ;
@@ -305,6 +438,7 @@ mod tests {
305438 Fragment :: Lit ( from, to) => Tag :: Lit ( & t. raw [ * from..* to] ) ,
306439 Fragment :: Cap ( n) => Tag :: Cap ( * n) ,
307440 Fragment :: Esc ( ch) => Tag :: Esc ( * ch) ,
441+ Fragment :: Var ( from, to) => Tag :: Var ( & t. raw [ * from..* to] ) ,
308442 } )
309443 . collect ( )
310444 }
@@ -320,8 +454,8 @@ mod tests {
320454 "escape sequences"
321455 ) ]
322456 #[ test_case(
323- "foo {1} {2}" ,
324- & [ Lit ( "foo " ) , Cap ( 1 ) , Lit ( " " ) , Cap ( 2 ) ] ;
457+ "foo {1} {2} {bar} " ,
458+ & [ Lit ( "foo " ) , Cap ( 1 ) , Lit ( " " ) , Cap ( 2 ) , Lit ( " " ) , Var ( "bar" ) ] ;
325459 "variable references"
326460 ) ]
327461 #[ test]
@@ -337,6 +471,8 @@ mod tests {
337471 #[ test_case( "{1} {2}" , "foo bar" ; "both submatches" ) ]
338472 #[ test_case( "{2} {1}" , "bar foo" ; "flipped submatches" ) ]
339473 #[ test_case( "{1}\\ n{2}" , "foo\n bar" ; "submatches and newline" ) ]
474+ #[ test_case( "{3}" , "{3}" ; "unknown capture" ) ]
475+ #[ test_case( "{unknown}" , "{unknown}" ; "variable without context" ) ]
340476 #[ test]
341477 fn render_works ( s : & str , expected : & str ) {
342478 let caps: TaggedCaptures < & str > = TaggedCaptures {
@@ -349,4 +485,34 @@ mod tests {
349485
350486 assert_eq ! ( rendered, expected) ;
351487 }
488+
489+ #[ test_case( "just a raw string" , "just a raw string" ; "raw string" ) ]
490+ #[ test_case( ">{0}<" , ">foo bar<" ; "full match" ) ]
491+ #[ test_case( "{1} {2}" , "foo bar" ; "both submatches" ) ]
492+ #[ test_case( "{2} {1}" , "bar foo" ; "flipped submatches" ) ]
493+ #[ test_case( "{1}\\ n{2}" , "foo\n bar" ; "submatches and newline" ) ]
494+ #[ test_case( "{3}" , "{3}" ; "unknown capture" ) ]
495+ #[ test_case( "{unknown}" , "from context" ; "variable without context" ) ]
496+ #[ test]
497+ fn render_with_context_works ( s : & str , expected : & str ) {
498+ let caps: TaggedCaptures < & str > = TaggedCaptures {
499+ captures : Captures :: new ( "foo bar" , vec ! [ Some ( ( 0 , 7 ) ) , Some ( ( 0 , 3 ) ) , Some ( ( 4 , 7 ) ) ] ) ,
500+ action : None ,
501+ } ;
502+
503+ struct Ctx ;
504+ impl Context for Ctx {
505+ fn render_var < W > ( & self , _: & str , w : & mut W ) -> io:: Result < usize >
506+ where
507+ W : Write ,
508+ {
509+ w. write ( b"from context" )
510+ }
511+ }
512+
513+ let t = Template :: parse ( s) . unwrap ( ) ;
514+ let rendered = t. render_with_context ( & caps, & Ctx ) . unwrap ( ) ;
515+
516+ assert_eq ! ( rendered, expected) ;
517+ }
352518}
0 commit comments