summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/highlighter.rs126
-rw-r--r--src/lib.rs7
-rw-r--r--src/span.rs11
-rw-r--r--src/template.rs25
4 files changed, 169 insertions, 0 deletions
diff --git a/src/highlighter.rs b/src/highlighter.rs
new file mode 100644
index 0000000..40e618e
--- /dev/null
+++ b/src/highlighter.rs
@@ -0,0 +1,126 @@
+use crate::*;
+
+use std::collections::{HashMap, VecDeque};
+
+
+pub struct Highlighter {
+ pub templates: Vec<Template>,
+}
+
+impl Highlighter {
+ pub fn from_str(template_set: &str) -> Self {
+ let mut variables = HashMap::new();
+ let mut templates = Vec::new();
+
+ for line in template_set.lines().map(|l| l.trim()) {
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+ let (left, right) = line.split_once('=').expect("Missing '=' character");
+ let name = left.trim().to_string();
+ let pattern = replace_variables(right.trim(), &variables);
+ if name.starts_with('<') && name.ends_with('>') {
+ variables.insert(name, pattern);
+ } else {
+ templates.push(Template::from_str(&pattern, name));
+ }
+ }
+ Self { templates }
+ }
+
+ pub fn highlight(&self, text: &str) -> Vec<Span> {
+ let mut remaining = &text[..];
+ let mut accumulator = String::new();
+ let mut spans = Vec::new();
+
+ 'outer: while !remaining.is_empty() {
+ // Check all templates.
+ for template in &self.templates {
+ if let Some(captures) = template.expression.captures(remaining).unwrap() {
+ if captures[0].len() > 0 {
+ // Bank the accumulator as an untagged span.
+ if !accumulator.is_empty() {
+ let text = std::mem::take(&mut accumulator);
+ spans.push(Span { tag: String::new(), text });
+ }
+ // Tag each capture group.
+ let mut i = 0;
+ let mut groups = captures.iter().filter_map(|c| c);
+ groups.next();
+ for (tag_i, group) in groups.enumerate() {
+ // Tag the text before this capture group.
+ if group.start() > i {
+ let text = captures[0][i..group.start()].to_string();
+ spans.push(Span { tag: template.tag.clone(), text })
+ }
+ // Tag the text in this capture group.
+ let text = captures[0][group.start()..group.end()].to_string();
+ let tag = match template.subtags.get(tag_i) {
+ Some(tag) => tag.clone(),
+ None => template.tag.clone(),
+ };
+ spans.push(Span { tag, text });
+ i = group.end();
+ }
+ // Tag the remaining text.
+ if captures[0].len() > i {
+ let text = captures[0][i..].to_string();
+ spans.push(Span { tag: template.tag.clone(), text })
+ }
+ // Continue to the next match.
+ remaining = &remaining[captures[0].len()..];
+ continue 'outer;
+ }
+ }
+ }
+ // Pop the first character into accumulator.
+ if let Some(c) = remaining.chars().nth(0) {
+ remaining = &remaining[c.len_utf8()..];
+ accumulator.push(c);
+ }
+ }
+ // Bank the accumulator as an untagged span.
+ if !accumulator.is_empty() {
+ let text = std::mem::take(&mut accumulator);
+ spans.push(Span { tag: String::new(), text });
+ }
+
+ // Merge adjacent spans that have the same tag.
+ let mut spans = VecDeque::from(spans);
+ let mut merged_spans: Vec<Span> = Vec::new();
+ while let Some(span) = spans.pop_front() {
+ if let Some(last) = merged_spans.last_mut() {
+ if span.tag == last.tag {
+ last.text.push_str(&span.text);
+ continue;
+ }
+ }
+ merged_spans.push(span);
+ }
+
+ return merged_spans;
+ }
+}
+
+
+fn replace_variables(pattern: &str, variables: &HashMap<String, String>) -> String {
+ let mut output = String::new();
+ let mut chars = pattern.chars();
+ while let Some(c) = chars.next() {
+ if c == '<' {
+ let mut name = String::from('<');
+ loop {
+ match chars.next() {
+ Some('>') => { name.push('>'); break; }
+ Some(c) => name.push(c),
+ None => panic!("Missing '>' character"),
+ }
+ }
+ let pattern = variables.get(&name).expect(&format!("Missing definition for {name:?}"));
+ output.push_str(&format!("(?:{pattern})"));
+ } else {
+ output.push(c);
+ }
+ }
+ return output;
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..7a40c94
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,7 @@
+mod span;
+mod template;
+mod highlighter;
+
+pub use span::*;
+pub use template::*;
+pub use highlighter::*;
diff --git a/src/span.rs b/src/span.rs
new file mode 100644
index 0000000..5282cae
--- /dev/null
+++ b/src/span.rs
@@ -0,0 +1,11 @@
+pub struct Span {
+ pub tag: String,
+ pub text: String,
+}
+
+impl std::fmt::Debug for Span {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ let tag = format!("{:?}", self.tag);
+ write!(f, "{tag:>20}: {:?}", self.text)
+ }
+}
diff --git a/src/template.rs b/src/template.rs
new file mode 100644
index 0000000..67eaceb
--- /dev/null
+++ b/src/template.rs
@@ -0,0 +1,25 @@
+use fancy_regex::*;
+
+
+pub struct Template {
+ pub tag: String,
+ pub subtags: Vec<String>,
+ pub expression: Regex,
+}
+
+impl Template {
+ pub fn from_str(pattern: &str, tag: String) -> Self {
+ let pattern = format!("^(?:{pattern})");
+ let expression = Regex::new(&pattern).unwrap();
+
+ if let Some((head, tail)) = tag.split_once('(') {
+ if let Some(tail) = tail.strip_suffix(')') {
+ let tag = head.trim().to_string();
+ let subtags = tail.split(',').map(|t| t.trim().to_string()).collect();
+ return Self { tag, subtags, expression };
+ }
+ }
+ let subtags = Vec::new();
+ return Self { tag, subtags, expression };
+ }
+}