Skip to content

Commit 149cd2e

Browse files
committed
Support rendering with a context
1 parent f091eaa commit 149cd2e

3 files changed

Lines changed: 190 additions & 24 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "structex"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2024"
55
authors = ["sminez <innes.andersonmorrison@gmail.com>"]
66
license = "MIT"

src/template.rs

Lines changed: 188 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
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;
@@ -42,6 +44,9 @@
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
//!
@@ -50,9 +55,6 @@
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
@@ -104,6 +106,69 @@
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+
107172
use 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)]
287419
mod 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\nbar"; "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\nbar"; "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

Comments
 (0)