use super::model::{
ClassDiagram, Direction, Line, Marker, Member, MemberKind, Namespace, Note, ParseError,
Relation,
};
use crate::preview::mermaid::flowchart::preprocess::preprocess;
use crate::preview::mermaid::flowchart::text::decode_label;
const MAX_RELATIONS: usize = 500;
#[cfg(test)]
pub const MAX_RELATIONS_FOR_TESTS: usize = MAX_RELATIONS;
const MAX_DEPTH: usize = 32;
pub fn parse(src: &str) -> Result<ClassDiagram, ParseError> {
let pre = preprocess(src);
let lines: Vec<&str> = pre.text.split('\n').collect();
let header = find_header(&lines)?;
let mut scanner = Scanner::new(pre.title);
scanner.line(&header.trailing, header.line_index + 1)?;
for (i, line) in lines.iter().enumerate().skip(header.line_index + 1) {
scanner.line(line, i + 1)?;
}
scanner.finish()
}
pub fn is_class_diagram(src: &str) -> bool {
let pre = preprocess(src);
let lines: Vec<&str> = pre.text.split('\n').collect();
find_header(&lines).is_ok()
}
struct Header {
line_index: usize,
trailing: String,
}
const CLASS_KEYWORDS: &[&str] = &["classDiagram-v2", "classDiagram"];
fn find_header(lines: &[&str]) -> Result<Header, ParseError> {
for (i, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
continue;
}
let t = line.trim_start();
for kw in CLASS_KEYWORDS {
let Some(rest) = t.strip_prefix(kw) else {
continue;
};
if rest.chars().next().is_some_and(is_plain_id_char) {
continue;
}
return Ok(Header {
line_index: i,
trailing: rest.to_string(),
});
}
let header: String = t
.chars()
.take_while(|c| !c.is_whitespace())
.take(40)
.collect();
return Err(ParseError::NotAClassDiagram { header });
}
Err(ParseError::Empty)
}
fn is_plain_id_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
enum Frame {
Namespace { id: String, line: usize },
Body { id: String, line: usize },
Transparent { line: usize },
}
struct Scanner {
out: ClassDiagram,
stack: Vec<Frame>,
acc_descr_block: bool,
relations: usize,
notes: usize,
claimed: std::collections::HashSet<String>,
}
impl Scanner {
fn new(title: Option<String>) -> Scanner {
Scanner {
out: ClassDiagram::new(Direction::default(), title),
stack: Vec::new(),
acc_descr_block: false,
relations: 0,
notes: 0,
claimed: std::collections::HashSet::new(),
}
}
fn namespace(&self) -> Option<String> {
self.stack.iter().rev().find_map(|f| match f {
Frame::Namespace { id, .. } => Some(id.clone()),
_ => None,
})
}
fn body(&self) -> Option<String> {
match self.stack.last() {
Some(Frame::Body { id, .. }) => Some(id.clone()),
_ => None,
}
}
fn line(&mut self, raw: &str, line: usize) -> Result<(), ParseError> {
if self.acc_descr_block {
match raw.find('}') {
Some(end) => {
self.push_acc_descr(&raw[..end]);
self.acc_descr_block = false;
}
None => self.push_acc_descr(raw),
}
return Ok(());
}
if let Some(id) = self.body() {
match raw.find('}') {
Some(end) => {
self.member(&id, &raw[..end]);
self.stack.pop();
let rest = raw[end + 1..].to_string();
return self.line(&rest, line);
}
None => {
self.member(&id, raw);
return Ok(());
}
}
}
let text = strip_comment(raw);
if let Some(rest) = acc_descr_block_start(&text) {
match rest.find('}') {
Some(end) => self.push_acc_descr(&rest[..end]),
None => {
self.push_acc_descr(rest);
self.acc_descr_block = true;
}
}
return Ok(());
}
for seg in segments(&text, line)? {
match seg {
Segment::Statement(s) => match self.body() {
Some(id) => self.member(&id, &s),
None => self.statement(&s, line)?,
},
Segment::Open(head) => self.open(&head, line),
Segment::Close => {
self.stack.pop();
}
}
}
Ok(())
}
fn push_acc_descr(&mut self, text: &str) {
let t = text.trim();
if t.is_empty() {
return;
}
match &mut self.out.acc_descr {
Some(existing) => {
existing.push('\n');
existing.push_str(t);
}
None => self.out.acc_descr = Some(t.to_string()),
}
}
fn open(&mut self, head: &str, line: usize) {
let t = head.trim();
if self.stack.len() > MAX_DEPTH {
self.stack.push(Frame::Transparent { line });
return;
}
if let Some(rest) = keyword(t, "namespace") {
let (name, label) = read_class_label(rest.trim());
let id = self.add_namespace(&name, label.as_deref());
self.stack.push(Frame::Namespace { id, line });
return;
}
if let Some(rest) = keyword(t, "class") {
let id = self.declare_class(rest);
match id {
Some(id) => self.stack.push(Frame::Body { id, line }),
None => self.stack.push(Frame::Transparent { line }),
}
return;
}
self.stack.push(Frame::Transparent { line });
}
fn finish(mut self) -> Result<ClassDiagram, ParseError> {
if let Some(frame) = self.stack.first() {
let (id, line) = match frame {
Frame::Namespace { id, line } => (format!("namespace {id}"), *line),
Frame::Body { id, line } => (format!("class {id}"), *line),
Frame::Transparent { line } => ("{".to_string(), *line),
};
return Err(ParseError::UnclosedBody { id, line });
}
let ids: Vec<String> = self.out.classes.iter().map(|c| c.id.clone()).collect();
for note in &mut self.out.notes {
if note.target.as_ref().is_some_and(|t| !ids.contains(t)) {
note.target = None;
}
}
let known: Vec<String> = self.out.classes.iter().map(|c| c.id.clone()).collect();
let namespaces: Vec<String> = self.out.namespaces.iter().map(|n| n.id.clone()).collect();
let placed =
|id: &String| known.iter().any(|k| k == id) || namespaces.iter().any(|n| n == id);
self.out
.relations
.retain(|r| placed(&r.from) && placed(&r.to));
if self.out.classes.is_empty() && self.out.notes.is_empty() {
return Err(ParseError::NoClasses);
}
Ok(self.out)
}
fn statement(&mut self, s: &str, _line: usize) -> Result<(), ParseError> {
let t = s.trim();
if t.is_empty() {
return Ok(());
}
if let Some(dir) = direction_statement(t) {
if self.namespace().is_none() {
self.out.direction = dir;
}
return Ok(());
}
if let Some(v) = keyword_value(t, "accTitle") {
self.out.acc_title = Some(v);
return Ok(());
}
if let Some(v) = keyword_value(t, "accDescr") {
self.out.acc_descr = Some(v);
return Ok(());
}
if t == "accDescr" || t.starts_with("accDescr ") {
return Ok(());
}
if is_ignored_statement(t) {
return Ok(());
}
if let Some(rest) = keyword(t, "note for") {
let (target, text) = split_note_target(rest);
self.add_note(text, Some(target));
return Ok(());
}
if let Some(rest) = keyword(t, "note") {
self.add_note(quoted(rest.trim()).unwrap_or_default(), None);
return Ok(());
}
if let Some(rest) = keyword(t, "namespace") {
let _ = rest;
return Ok(());
}
if let Some(rest) = keyword(t, "class") {
self.declare_class(rest);
return Ok(());
}
if let Some(rest) = t.strip_prefix("<<") {
if let Some((annotation, tail)) = rest.split_once(">>") {
let name = tail.trim();
if !name.is_empty() {
let (id, generic) = split_generic(&decode_name(name));
self.out.intern(&id, &generic);
self.place(&id);
if let Some(c) = self.out.class_mut(&id) {
c.annotations.push(annotation.trim().to_string());
}
}
return Ok(());
}
}
if let Some(relation) = read_relation(t) {
self.add_relation(relation);
return Ok(());
}
if let Some(i) = label_colon(t) {
let name = t[..i].trim();
let member = t[i + 1..].trim();
if !name.is_empty() {
let (name, css) = split_style_separator(name);
let (id, generic) = split_generic(&decode_name(&name));
if !id.is_empty() {
self.out.intern(&id, &generic);
self.place(&id);
self.css(&id, &css);
self.member(&id, member);
}
}
return Ok(());
}
Ok(())
}
fn declare_class(&mut self, rest: &str) -> Option<String> {
let (mut name, label) = read_class_label(rest.trim());
let mut annotation = None;
if let Some(start) = name.find("<<") {
if let Some(end) = name[start..].find(">>") {
annotation = Some(name[start + 2..start + end].trim().to_string());
let mut without = name[..start].to_string();
without.push_str(&name[start + end + 2..]);
name = without.trim().to_string();
}
}
let (name, css) = split_style_separator(&name);
let (id, generic) = split_generic(&decode_name(&name));
if id.is_empty() {
return None;
}
self.out.intern(&id, &generic);
self.place(&id);
self.css(&id, &css);
if let Some(label) = label {
if let Some(c) = self.out.class_mut(&id) {
c.label = decode_label(&label);
}
}
if let Some(a) = annotation {
if !a.is_empty() {
if let Some(c) = self.out.class_mut(&id) {
c.annotations.push(a);
}
}
}
Some(id)
}
fn member(&mut self, class_id: &str, raw: &str) {
let text = raw.trim();
if text.is_empty() {
return;
}
if let Some(inner) = text.strip_prefix("<<").and_then(|r| r.strip_suffix(">>")) {
if let Some(c) = self.out.class_mut(class_id) {
c.annotations.push(inner.trim().to_string());
}
return;
}
let is_method = text.find(')').is_some_and(|i| i > 0);
let member = parse_member(text, is_method);
if let Some(c) = self.out.class_mut(class_id) {
if is_method {
c.methods.push(member);
} else {
c.attributes.push(member);
}
}
}
fn css(&mut self, id: &str, classes: &[String]) {
if classes.is_empty() {
return;
}
if let Some(c) = self.out.class_mut(id) {
for name in classes {
if !c.css_classes.contains(name) {
c.css_classes.push(name.clone());
}
}
}
}
fn place(&mut self, id: &str) {
if self.claimed.contains(id) {
return;
}
self.claimed.insert(id.to_string());
let Some(ns) = self.namespace() else { return };
if let Some(c) = self.out.class_mut(id) {
c.parent = Some(ns.clone());
}
if let Some(n) = self.out.namespaces.iter_mut().find(|n| n.id == ns) {
n.members.push(id.to_string());
}
}
fn add_namespace(&mut self, name: &str, label: Option<&str>) -> String {
let name = decode_name(name);
let qualified = match self.namespace() {
Some(prefix) => format!("{prefix}.{name}"),
None => name.clone(),
};
let parts: Vec<&str> = qualified.split('.').collect();
let mut id = String::new();
let mut parent: Option<String> = None;
for (i, part) in parts.iter().enumerate() {
if i > 0 {
id.push('.');
}
id.push_str(part);
let leaf = i == parts.len() - 1;
let text = match (leaf, label) {
(true, Some(l)) => decode_label(l),
_ => (*part).to_string(),
};
if !self.out.namespaces.iter().any(|n| n.id == id) {
self.out.namespaces.push(Namespace {
id: id.clone(),
label: text,
members: Vec::new(),
parent: parent.clone(),
});
if let Some(p) = &parent {
let child = id.clone();
if let Some(n) = self.out.namespaces.iter_mut().find(|n| &n.id == p) {
if !n.members.contains(&child) {
n.members.push(child);
}
}
}
} else if leaf {
if let Some(l) = label {
let text = decode_label(l);
if let Some(n) = self.out.namespaces.iter_mut().find(|n| n.id == id) {
n.label = text;
}
}
}
parent = Some(id.clone());
}
qualified
}
fn add_note(&mut self, text: String, target: Option<String>) {
let text = decode_label(&text);
if text.is_empty() {
return;
}
let target = target.map(|t| split_generic(&decode_name(t.trim())).0);
self.notes += 1;
let id = format!("note{}", self.notes - 1);
let parent = self.namespace();
if let Some(ns) = &parent {
let note_id = id.clone();
if let Some(n) = self.out.namespaces.iter_mut().find(|n| &n.id == ns) {
n.members.push(note_id);
}
}
self.out.notes.push(Note {
id,
text,
target,
parent,
});
}
fn add_relation(&mut self, mut r: Relation) {
if self.relations >= MAX_RELATIONS {
return;
}
let (from_id, from_generic) = split_generic(&r.from);
let (to_id, to_generic) = split_generic(&r.to);
if from_id.is_empty() || to_id.is_empty() {
return;
}
self.out.intern(&from_id, &from_generic);
self.place(&from_id);
self.out.intern(&to_id, &to_generic);
self.place(&to_id);
r.id = format!("r{}", self.relations);
r.from = from_id;
r.to = to_id;
self.relations += 1;
self.out.relations.push(r);
}
}
fn is_ignored_statement(t: &str) -> bool {
for kw in ["classDef", "cssClass", "style", "click", "callback", "link"] {
if keyword(t, kw).is_some() {
return true;
}
}
false
}
fn keyword<'a>(s: &'a str, kw: &str) -> Option<&'a str> {
let rest = s.strip_prefix(kw)?;
if rest.is_empty() || !rest.starts_with(char::is_whitespace) {
return None;
}
Some(rest)
}
fn keyword_value(s: &str, kw: &str) -> Option<String> {
let rest = s.strip_prefix(kw)?;
let rest = rest.trim_start();
let rest = rest.strip_prefix(':')?;
Some(rest.trim().to_string())
}
fn direction_statement(s: &str) -> Option<Direction> {
let rest = keyword(s, "direction")?;
Direction::parse(rest.trim())
}
fn split_style_separator(s: &str) -> (String, Vec<String>) {
match s.find(":::") {
Some(i) => {
let classes: Vec<String> = s[i + 3..]
.split(',')
.map(|c| c.trim().to_string())
.filter(|c| !c.is_empty())
.collect();
(s[..i].trim().to_string(), classes)
}
None => (s.trim().to_string(), Vec::new()),
}
}
pub fn split_generic(id: &str) -> (String, String) {
match id.find('~') {
Some(i) if i > 0 => {
let generic = id[i + 1..].trim_end_matches('~').trim().to_string();
(id[..i].trim().to_string(), generic)
}
_ => (id.trim().to_string(), String::new()),
}
}
pub fn decode_name(raw: &str) -> String {
let t = raw.trim();
let t = match (t.strip_prefix('`'), t.strip_suffix('`')) {
(Some(_), Some(_)) if t.len() >= 2 => &t[1..t.len() - 1],
_ => t,
};
decode_label(t)
}
fn read_class_label(s: &str) -> (String, Option<String>) {
let t = s.trim();
let Some(open) = t.find('[') else {
return (t.to_string(), None);
};
let Some(close) = t.rfind(']') else {
return (t.to_string(), None);
};
if close < open {
return (t.to_string(), None);
}
let inner = t[open + 1..close].trim();
let label = quoted(inner).unwrap_or_else(|| inner.to_string());
(t[..open].trim().to_string(), Some(label))
}
fn quoted(s: &str) -> Option<String> {
let t = s.trim();
let inner = t.strip_prefix('"')?.strip_suffix('"')?;
Some(inner.to_string())
}
fn split_note_target(rest: &str) -> (String, String) {
let t = rest.trim();
match t.find('"') {
Some(i) => {
let target = t[..i].trim().to_string();
let text = quoted(&t[i..]).unwrap_or_else(|| t[i + 1..].to_string());
(target, text)
}
None => {
let mut parts = t.splitn(2, char::is_whitespace);
(
parts.next().unwrap_or("").to_string(),
parts.next().unwrap_or("").to_string(),
)
}
}
}
pub fn read_relation(t: &str) -> Option<Relation> {
let limit = label_colon(t).unwrap_or(t.len());
let head = &t[..limit];
let (line_start, line_end, line) = find_line_token(head)?;
let (start, left_end) = read_start_marker(head, line_start);
let (end, right_start) = read_end_marker(head, line_end);
let (from_raw, start_cardinality) = split_trailing_quoted(head[..left_end].trim());
let (end_cardinality, to_raw) = split_leading_quoted(head[right_start..].trim());
let (from, _) = split_style_separator(&from_raw);
let (to, _) = split_style_separator(&to_raw);
let from = decode_name(&from);
let to = decode_name(&to);
if from.is_empty() || to.is_empty() {
return None;
}
let label = if limit < t.len() {
let l = decode_label(t[limit + 1..].trim());
(!l.is_empty()).then_some(l)
} else {
None
};
Some(Relation {
id: String::new(),
from,
to,
start,
end,
line,
label,
start_cardinality,
end_cardinality,
})
}
fn find_line_token(s: &str) -> Option<(usize, usize, Line)> {
let bytes = s.as_bytes();
let mut i = 0;
let mut quoted = false;
let mut ticked = false;
while i + 1 < bytes.len() {
match bytes[i] {
b'"' if !ticked => quoted = !quoted,
b'`' if !quoted => ticked = !ticked,
b'-' | b'.' if !quoted && !ticked && bytes[i + 1] == bytes[i] => {
let c = bytes[i];
let mut j = i;
while j < bytes.len() && bytes[j] == c {
j += 1;
}
let kind = if c == b'-' { Line::Solid } else { Line::Dotted };
return Some((i, j, kind));
}
_ => {}
}
i += 1;
}
None
}
fn read_start_marker(s: &str, line_start: usize) -> (Marker, usize) {
let head = &s[..line_start];
let trimmed = head.trim_end();
let base = trimmed.len();
for (token, marker) in MARKERS {
if let Some(rest) = trimmed.strip_suffix(*token) {
if *token == "o" && rest.chars().next_back().is_some_and(is_plain_id_char) {
continue;
}
if rest.trim().is_empty() {
continue;
}
return (*marker, base - token.len());
}
}
(Marker::None, base)
}
fn read_end_marker(s: &str, line_end: usize) -> (Marker, usize) {
let tail = &s[line_end..];
for (token, marker) in MARKERS {
if let Some(rest) = tail.strip_prefix(*token) {
if *token == "o" && rest.chars().next().is_some_and(is_plain_id_char) {
continue;
}
if rest.trim().is_empty() {
continue;
}
return (*marker, line_end + token.len());
}
}
(Marker::None, line_end)
}
const MARKERS: &[(&str, Marker)] = &[
("<|", Marker::Extension),
("|>", Marker::Extension),
("()", Marker::Lollipop),
("<", Marker::Dependency),
(">", Marker::Dependency),
("*", Marker::Composition),
("o", Marker::Aggregation),
];
fn split_trailing_quoted(s: &str) -> (String, Option<String>) {
let t = s.trim_end();
let Some(inner) = t.strip_suffix('"') else {
return (t.to_string(), None);
};
let Some(open) = inner.rfind('"') else {
return (t.to_string(), None);
};
let card = inner[open + 1..].to_string();
(
t[..open].trim().to_string(),
(!card.trim().is_empty()).then_some(card),
)
}
fn split_leading_quoted(s: &str) -> (Option<String>, String) {
let t = s.trim_start();
let Some(rest) = t.strip_prefix('"') else {
return (None, t.to_string());
};
let Some(close) = rest.find('"') else {
return (None, t.to_string());
};
let card = rest[..close].to_string();
(
(!card.trim().is_empty()).then_some(card),
rest[close + 1..].trim().to_string(),
)
}
pub fn parse_member(input: &str, is_method: bool) -> Member {
let text = input.trim();
let mut visibility = None;
let mut rest = text;
if let Some(first) = text.chars().next() {
if matches!(first, '+' | '-' | '#' | '~') {
visibility = Some(first);
rest = &text[first.len_utf8()..];
}
}
if !is_method {
let mut classifier = None;
let mut id = rest;
if let Some(last) = rest.chars().next_back() {
if matches!(last, '$' | '*') {
classifier = Some(last);
id = &rest[..rest.len() - last.len_utf8()];
}
}
return Member {
visibility,
name: parse_generic_types(id.trim()),
kind: MemberKind::Attribute,
parameters: String::new(),
return_type: String::new(),
classifier,
};
}
let (name, parameters, tail) = match rest.rfind(')') {
Some(close) => match rest[..close].rfind('(') {
Some(open) => (
rest[..open].to_string(),
rest[open + 1..close].to_string(),
rest[close + 1..].to_string(),
),
None => (rest.to_string(), String::new(), String::new()),
},
None => (rest.to_string(), String::new(), String::new()),
};
let mut chars = tail.chars();
let mut classifier = match chars.next() {
Some(c) if c.is_whitespace() => None,
Some(c @ ('$' | '*')) => Some(c),
_ => None,
};
let return_type = match tail.chars().next() {
Some(c) if c.is_whitespace() || matches!(c, '$' | '*') => tail[c.len_utf8()..].trim(),
_ => tail.trim(),
};
let mut return_type = return_type.to_string();
if classifier.is_none() {
if let Some(last) = return_type.chars().next_back() {
if matches!(last, '$' | '*') {
classifier = Some(last);
return_type.truncate(return_type.len() - last.len_utf8());
}
}
}
Member {
visibility,
name: parse_generic_types(name.trim()),
kind: MemberKind::Method,
parameters: parse_generic_types(parameters.trim()),
return_type: parse_generic_types(return_type.trim()),
classifier,
}
}
pub fn parse_generic_types(input: &str) -> String {
let parts: Vec<&str> = split_keeping_commas(input);
let mut out = String::new();
let mut i = 0;
while i < parts.len() {
let mut set = parts[i].to_string();
if parts[i] == "," && i > 0 && i + 1 < parts.len() {
let prev = parts[i - 1];
let next = parts[i + 1];
if count(prev, '~') == 1 && count(next, '~') == 1 {
set = format!("{prev},{next}");
i += 1;
let previous = process_set(prev);
out.truncate(out.len() - previous.len());
}
}
out.push_str(&process_set(&set));
i += 1;
}
out
}
fn split_keeping_commas(input: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut start = 0;
for (i, c) in input.char_indices() {
if c == ',' {
out.push(&input[start..i]);
out.push(&input[i..i + 1]);
start = i + 1;
}
}
out.push(&input[start..]);
out
}
fn count(s: &str, c: char) -> usize {
s.chars().filter(|x| *x == c).count()
}
fn process_set(input: &str) -> String {
if count(input, '~') <= 1 {
return input.to_string();
}
let mut leading = false;
let mut work = input;
if !count(input, '~').is_multiple_of(2) && input.starts_with('~') {
work = &input[1..];
leading = true;
}
let mut chars: Vec<char> = work.chars().collect();
loop {
let first = chars.iter().position(|c| *c == '~');
let last = chars.iter().rposition(|c| *c == '~');
match (first, last) {
(Some(f), Some(l)) if f != l => {
chars[f] = '<';
chars[l] = '>';
}
_ => break,
}
}
let body: String = chars.into_iter().collect();
if leading {
format!("~{body}")
} else {
body
}
}
enum Segment {
Statement(String),
Open(String),
Close,
}
fn segments(line: &str, line_no: usize) -> Result<Vec<Segment>, ParseError> {
let mut out = Vec::new();
let bytes = line.as_bytes();
let mut i = 0;
let mut start = 0;
while i < bytes.len() {
match bytes[i] {
b'"' => {
let Some(end) = line[i + 1..].find('"') else {
return Err(ParseError::UnclosedString { line: line_no });
};
i += 1 + end + 1;
continue;
}
b'`' => {
let Some(end) = line[i + 1..].find('`') else {
return Err(ParseError::UnclosedString { line: line_no });
};
i += 1 + end + 1;
continue;
}
b':' => {
if line[i..].starts_with(":::") {
i += 3;
continue;
}
break;
}
b'{' => {
out.push(Segment::Open(line[start..i].to_string()));
i += 1;
start = i;
continue;
}
b'}' => {
out.push(Segment::Statement(line[start..i].to_string()));
out.push(Segment::Close);
i += 1;
start = i;
continue;
}
_ => {}
}
i += 1;
}
out.push(Segment::Statement(line[start..].to_string()));
Ok(out)
}
pub fn label_colon(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
let mut i = 0;
let mut quoted = false;
let mut ticked = false;
while i < bytes.len() {
match bytes[i] {
b'"' if !ticked => quoted = !quoted,
b'`' if !quoted => ticked = !ticked,
b':' if !quoted && !ticked => {
if s[i..].starts_with(":::") {
i += 3;
continue;
}
return Some(i);
}
_ => {}
}
i += 1;
}
None
}
fn acc_descr_block_start(line: &str) -> Option<&str> {
let t = line.trim_start();
let rest = t.strip_prefix("accDescr")?;
let rest = rest.trim_start();
rest.strip_prefix('{')
}
fn strip_comment(line: &str) -> String {
let limit = label_colon(line).unwrap_or(line.len());
let bytes = line.as_bytes();
let mut i = 0;
let mut quoted = false;
while i + 1 < limit {
match bytes[i] {
b'"' => quoted = !quoted,
b'%' if !quoted && bytes[i + 1] == b'%' && !line[i..].starts_with("%%{") => {
return line[..i].to_string();
}
_ => {}
}
i += 1;
}
line.to_string()
}