Capture Atoms
Extract named values from parsed input using capture atoms. Like named groups in regex, but integrated into the parsing grammar.
Key Concepts
- named captures
- zero-copy extraction
- capture inspection API
- cross-backend support
Why Parse This Format?
Captures allow extracting specific fields from parsed input without building a full AST. This is more efficient for simple extraction tasks.
Use Cases
- Extracting email addresses from text
- Parsing log files for specific fields (IP, status, timestamp)
- Parsing CSV exports for data migration
- Field extraction without full AST construction
Input Format
Any structured text with fields to extract.
user@example.com, 192.168.1.1 - - [timestamp] "GET /path" 200Examples
user@example.comVALIDEmail address to parse
192.168.1.1 - - [10/Oct/2000:13:55:36] "GET /apache_pb.gif HTTP/1.0" 200 2326VALIDApache log line
Output Format
Named captures accessible via the capture inspection API.
Structure
capture_namesList of all capture names
get_captureGet text of a named capture
capturesAll captures as HashMap
How the Code Works
This section explains how the parsing rules map to the format semantics.
Basic Capture
let grammar = GrammarBuilder::new()
.rule("greeting", capture("greeting", str("hello")))
.build();
let result = parser.parse("hello world")?;
if let Some(text) = result.get_capture("greeting", input) {
println!("Captured: {}", text); // "hello"
}Wrap any parslet with `capture()` to name the matched text: The captured text is extracted without copying the input string.
Nested Captures
let grammar = GrammarBuilder::new()
.rule("email", capture("email",
sequence([
capture("local", re(r"[a-zA-Z0-9._%+-]+")),
str("@"),
capture("domain", re(r"[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")),
])
))
.build();Captures can be nested to capture both the whole and parts: This captures: - `email`: the full email address - `local`: the part before @ - `domain`: the part after @
Log Line Parsing
let grammar = GrammarBuilder::new()
.rule("log_line", sequence([
capture("ip", re(r"\d+\.\d+\.\d+\.\d+")),
str(" - - ["),
capture("timestamp", re(r"[^\]]+")),
str("] \""),
capture("method", re(r"[A-Z]+")),
str(" "),
capture("path", re(r"[^\s]+")),
str("\" "),
capture("status", re(r"\d+")),
]))
.build();Real-world example parsing Apache log format: Each field is captured by name for easy access.
Inspection API
// Get all capture names
let names: Vec<String> = result.capture_names();
// Get specific capture
let value: Option<&str> = result.get_capture("name", input);
// Get all captures as HashMap
let all: HashMap<&str, &str> = result.captures(input);Raw Grammar API
let mut grammar = Grammar::new();
let name = grammar.add_atom(Atom::Regex { pattern: r"[a-zA-Z]+".into() });
let name_capture = grammar.add_atom(Atom::Capture {
name: "name".into(),
atom: name,
});For more control, use the raw `Grammar` API:
Raw Grammar API
let mut grammar = Grammar::new();
let name = grammar.add_atom(Atom::Regex { pattern: r"[a-zA-Z]+".into() });
let name_capture = grammar.add_atom(Atom::Capture {
name: "name".into(),
atom: name,
});For more control, use the raw `Grammar` API:
Source Code
//! Capture Atoms Example
//!
//! Demonstrates how to extract named values from parsed input using capture atoms.
//! Captures work like named groups in regular expressions, but are integrated
//! into the parsing grammar and work across all backends.
//!
//! Run with: cargo run --example captures --no-default-features
#![allow(clippy::print_literal)]
use parsanol::portable::{
parser_dsl::{capture, dynamic, re, seq, str, GrammarBuilder},
AstArena, Atom, Grammar, PortableParser,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Capture Atoms Example");
println!("=====================\n");
// =========================================================================
// Example 1: Basic Capture
// =========================================================================
println!("--- Example 1: Basic Capture ---\n");
let grammar = GrammarBuilder::new()
.rule("greeting", capture("greeting", dynamic(str("hello"))))
.build();
let input = "hello world";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
// Extract the captured value
if let Some(text) = result.get_capture("greeting", input) {
println!(" Captured 'greeting': {:?}", text); // "hello"
}
// =========================================================================
// Example 2: Email Parsing with Nested Captures
// =========================================================================
println!("\n--- Example 2: Email Parsing with Nested Captures ---\n");
let grammar = GrammarBuilder::new()
.rule(
"email",
capture(
"email",
seq(vec![
dynamic(capture("local", dynamic(re(r"[a-zA-Z0-9._%+-]+")))),
dynamic(str("@")),
dynamic(capture(
"domain",
dynamic(re(r"[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")),
)),
]),
),
)
.build();
let input = "user@example.com";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
println!(" Full email: {:?}", result.get_capture("email", input));
println!(" Local part: {:?}", result.get_capture("local", input));
println!(" Domain: {:?}", result.get_capture("domain", input));
// =========================================================================
// Example 3: Inspecting All Captures
// =========================================================================
println!("\n--- Example 3: Inspecting All Captures ---\n");
// Get all capture names
println!(" Capture names: {:?}", result.capture_names());
// Get all captures as HashMap
let all_captures = result.captures(input);
println!(" All captures:");
for (name, value) in &all_captures {
println!(" {} = {:?}", name, value);
}
// =========================================================================
// Example 4: Using Raw Grammar API
// =========================================================================
println!("\n--- Example 4: Using Raw Grammar API ---\n");
let mut grammar = Grammar::new();
let name = grammar.add_atom(Atom::Re {
pattern: r"[a-zA-Z]+".into(),
});
let name_capture = grammar.add_atom(Atom::Capture {
name: "name".into(),
atom: name,
});
let comma = grammar.add_atom(Atom::Str {
pattern: ", ".into(),
});
let age = grammar.add_atom(Atom::Re {
pattern: r"\d+".into(),
});
let age_capture = grammar.add_atom(Atom::Capture {
name: "age".into(),
atom: age,
});
let person = grammar.add_atom(Atom::Sequence {
atoms: vec![name_capture, comma, age_capture],
});
grammar.root = person;
let input = "Alice, 30";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
println!(" Name: {:?}", result.get_capture("name", input));
println!(" Age: {:?}", result.get_capture("age", input));
// =========================================================================
// Example 5: Accessing Capture State Directly
// =========================================================================
println!("\n--- Example 5: Accessing Capture State Directly ---\n");
let grammar = GrammarBuilder::new()
.rule("item", capture("item", dynamic(re(r"[a-z]+"))))
.build();
let input = "apple";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
parser.parse()?;
// Access capture state directly from parser
let captures = parser.capture_state();
let names: Vec<_> = captures.names().collect();
println!(" Capture names from parser: {:?}", names);
if let Some(value) = captures.get("item") {
println!(" Item: {:?}", value.get_text(input));
}
// =========================================================================
// Summary
// =========================================================================
println!("\n--- Benefits of Capture Atoms ---");
println!("* Zero-copy: captures store offsets, not strings");
println!("* Works across all backends (Packrat, Bytecode, Streaming)");
println!("* Clean API: capture_names(), get_capture(), captures()");
println!("* No AST construction needed for simple extraction");
println!("* DSL helpers: capture(\"name\", parslet)");
println!("\n--- Performance Notes ---");
println!("* Captures add minimal overhead (~5%% for heavy use)");
println!("* Capture lookup is O(n) where n = number of captures");
println!("* Consider scope atoms for nested contexts");
println!("\n--- API Summary ---");
println!(" parser.parse_from_pos(0) -> ParseResult with captures");
println!(" parser.capture_state() -> &CaptureState");
println!(" result.get_capture(name, input) -> Option<&str>");
Ok(())
}
# frozen_string_literal: true
#
# Capture Atoms Example
#
# Demonstrates how to extract named values from parsed input using capture atoms.
# Captures work like named groups in regular expressions, but are integrated
# into the parsing grammar.
# NOTE: This example focuses on demonstrating the capture API.
# For production use, please refer to the existing capture example
# and the FFI documentation for more advanced patterns.
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol/parslet"
require "pp"
include Parsanol::Parslet
puts "Capture Atoms Example"
puts "=====================\n"
# ===========================================================================
# Example 1: Basic Capture
# ===========================================================================
puts "--- Example 1: Basic Capture ---\n"
# Simple capture: match 'hello' and capture it
parser = str("hello").capture(:greeting)
input = "hello"
result = parser.parse(input)
puts " Input: #{input.inspect}"
puts " Result: #{result.inspect} (a Slice)"
puts " Result.to_s: #{result.to_s.inspect}"
# ===========================================================================
# Example 2: Multiple Captures in Sequence
# ===========================================================================
puts "\n--- Example 2: Multiple Captures in Sequence ---\n"
# Parse key=value pairs
kv_parser = match("[a-z]+").capture(:key) >>
str("=") >>
match("[a-zA-Z0-9]+").capture(:value)
input = "name=Alice"
result = kv_parser.parse(input)
puts " Input: #{input.inspect}"
puts " Result: #{result.inspect}"
puts " Key: #{result[:key]}"
puts " Value: #{result[:value]}"
# ===========================================================================
# Example 3: Captures with Dynamic
# ===========================================================================
puts "\n--- Example 3: Captures with Dynamic ---\n"
# Use captured value in dynamic block
class TypeParser < Parsanol::Parser
include Parsanol::Parslet
root :declaration
rule(:type) { match("[a-z]+").capture(:type) }
rule(:value) do
dynamic do |ctx|
# Get captured type to determine value parser
type_val = ctx[:type].to_s
case type_val
when "int" then match('\d+')
when "str" then match("[a-z]+")
when "bool" then str("true") | str("false")
else match("[a-z]+") # fallback
end.capture(:value)
end
end
rule(:declaration) do
type >> str(":") >> match("[a-z]+").capture(:name) >> str("=") >> value
end
end
test_cases = [
["int:count=42", "int"],
["str:message=hello", "str"],
["bool:enabled=true", "bool"],
]
puts "\nTesting type-driven parsing:"
test_cases.each_key do |input|
puts " Input: #{input.inspect}"
parser = TypeParser.new
result = parser.parse(input)
puts " ✓ Parsed successfully"
puts " type: #{result[:type]}"
puts " name: #{result[:name]}"
puts " value: #{result[:value]}"
end
# ===========================================================================
# Summary
# ===========================================================================
puts "\n--- Benefits of Capture Atoms ---"
puts "* Zero-copy: captures store offsets, not strings"
puts "* Works across all backends (Packrat, Streaming)"
puts "* Clean API: capture(name) method on atoms"
puts "* No AST construction needed for simple extraction"
puts "\n--- Performance Notes ---"
puts "* Captures add minimal overhead (~5% for heavy use)"
puts "* Capture lookup is O(n) where n = number of captures"
puts "\n--- API Summary ---"
puts " atom.capture(:name) -> captures match result"
puts " result[:name] -> retrieves captured value (Slice or Hash)"
puts " result[:name].to_s -> converts Slice to String"
puts " context.captures[:name] -> in dynamic blocks"