diff --git a/examples/markdown.svg b/examples/markdown.svg
new file mode 100644
index 0000000..3059d9b
--- /dev/null
+++ b/examples/markdown.svg
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/examples/markdown.xml b/examples/markdown.xml
new file mode 100644
index 0000000..ca073bb
--- /dev/null
+++ b/examples/markdown.xml
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/elements/element.rs b/src/elements/element.rs
index afd6323..71800fa 100644
--- a/src/elements/element.rs
+++ b/src/elements/element.rs
@@ -436,7 +436,7 @@ impl SvgElement {
// TODO: refactor this method to handle text event gen better
let phantom = matches!(self.name(), "point" | "box");
- if self.has_attr("text") {
+ if self.has_attr("text") || self.has_attr("md") {
let (orig_elem, text_elements) = process_text_attr(self)?;
if orig_elem.name != "text" && !phantom {
diff --git a/src/elements/markdown.rs b/src/elements/markdown.rs
new file mode 100644
index 0000000..5eaf668
--- /dev/null
+++ b/src/elements/markdown.rs
@@ -0,0 +1,734 @@
+use super::SvgElement;
+
+pub fn get_md_value(element: &mut SvgElement) -> Vec {
+ let text_value = if let Some(tv) = element.pop_attr("md") {
+ tv
+ } else if let Some(tv) = element.pop_attr("text") {
+ tv
+ } else {
+ return vec![];
+ };
+
+ // parse into spans and data about style
+ let (spans, span_data) = md_parse(&text_value);
+
+ let mut md_spans: Vec = spans
+ .iter()
+ .map(|s| MdSpan {
+ code: false,
+ bold: false,
+ italic: false,
+ text: s.to_string(),
+ })
+ .collect();
+
+ for s in span_data {
+ let class = s.code_bold_italic;
+ for i in md_spans.iter_mut().take(s.end_idx).skip(s.start_idx) {
+ match class {
+ SpanStyleEnum::Code => i.code = true,
+ SpanStyleEnum::Bold => i.bold = true,
+ SpanStyleEnum::Italic => i.italic = true,
+ }
+ }
+ }
+
+ // merge equal style spans together
+ let mut result = vec![];
+ let mut md_span_iter = md_spans.iter();
+ if let Some(first) = md_span_iter.next() {
+ result.push(first.clone());
+ }
+ for span in md_span_iter {
+ if result[result.len() - 1].bold != span.bold
+ || result[result.len() - 1].code != span.code
+ || result[result.len() - 1].italic != span.italic
+ {
+ result.push(MdSpan {
+ code: span.code,
+ bold: span.bold,
+ italic: span.italic,
+ text: String::new(),
+ });
+ }
+ let last_ind = result.len() - 1;
+ result[last_ind].text += &span.text;
+ }
+
+ result
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct MdSpan {
+ pub code: bool,
+ pub bold: bool,
+ pub italic: bool,
+ pub text: String,
+}
+
+#[derive(Debug, PartialEq)]
+enum SpanStyleEnum {
+ Code,
+ Bold,
+ Italic,
+}
+
+#[derive(Debug, PartialEq)]
+struct SpanData {
+ start_idx: usize,
+ end_idx: usize,
+ code_bold_italic: SpanStyleEnum,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum DelimiterType {
+ Null,
+ Asterisk,
+ Escape,
+ UnderScore,
+ Tick,
+}
+
+impl DelimiterType {
+ fn to_char(&self) -> char {
+ match self {
+ DelimiterType::Null => ' ',
+ DelimiterType::Asterisk => '*',
+ DelimiterType::Escape => '\\',
+ DelimiterType::UnderScore => '_',
+ DelimiterType::Tick => '`',
+ }
+ }
+ fn from_char(c: char) -> Self {
+ match c {
+ '*' => DelimiterType::Asterisk,
+ '\\' => DelimiterType::Escape,
+ '_' => DelimiterType::UnderScore,
+ '`' => DelimiterType::Tick,
+ _ => DelimiterType::Null,
+ }
+ }
+}
+
+// based on the commonmark implementation https://spec.commonmark.org/0.31.2/
+#[derive(Debug, Clone)]
+struct DelimiterData {
+ ind: usize, // goes just before this char
+ char_type: DelimiterType,
+ num_delimiters: usize,
+ is_active: bool,
+ could_open: bool,
+ could_close: bool,
+}
+
+fn md_parse_delimiters(text_value: &str) -> (Vec, Vec) {
+ let mut result = vec![];
+ let mut delimiters = vec![DelimiterData {
+ ind: 0,
+ char_type: DelimiterType::Null,
+ num_delimiters: 0,
+ is_active: false,
+ could_open: false,
+ could_close: false,
+ }];
+
+ let mut current_span = String::new();
+
+ // first pass find delimiters
+ for c in text_value.chars() {
+ match DelimiterType::from_char(c) {
+ DelimiterType::Null => current_span.push(c),
+ // the delimiters and escape
+ _ => {
+ if !current_span.is_empty() {
+ result.push(current_span);
+ current_span = String::new();
+ }
+ let last = delimiters.last_mut().expect("guaranteed not to be empty");
+ if DelimiterType::from_char(c) == last.char_type && last.ind == result.len() {
+ // is a continuation
+ last.num_delimiters += 1;
+ } else {
+ delimiters.push(DelimiterData {
+ ind: result.len(),
+ char_type: DelimiterType::from_char(c),
+ num_delimiters: 1,
+ is_active: true,
+ could_open: true,
+ could_close: true,
+ });
+ }
+ }
+ }
+ }
+ if !current_span.is_empty() {
+ result.push(current_span);
+ }
+
+ (result, delimiters)
+}
+
+fn md_parse_code_blocks(
+ result: Vec,
+ delimiters: &mut Vec,
+) -> (Vec, Vec) {
+ let mut new_result = vec![];
+ let mut spans = vec![];
+ let mut res_ind = 0;
+ let mut del_ind = 0;
+ let mut removed_spans = 0;
+
+ let mut current_span = String::new();
+ while res_ind <= result.len() {
+ while del_ind < delimiters.len() && delimiters[del_ind].ind <= res_ind {
+ if delimiters[del_ind].char_type == DelimiterType::Tick {
+ // if previous delimiter is \ and is right before and is odd number of \
+ // then reduce by 1 and re_add it if it does make a pair
+ // need to acount for all previous delimiters have been moved by re_added letters
+ let escaped = del_ind != 0
+ && delimiters[del_ind - 1].ind + removed_spans == delimiters[del_ind].ind
+ && delimiters[del_ind - 1].char_type == DelimiterType::Escape
+ && delimiters[del_ind - 1].num_delimiters % 2 != 0;
+ let needed_len = match escaped {
+ false => delimiters[del_ind].num_delimiters,
+ true => delimiters[del_ind].num_delimiters - 1,
+ };
+
+ for closer_ind in (del_ind + 1)..delimiters.len() {
+ if delimiters[del_ind].char_type == delimiters[closer_ind].char_type
+ && delimiters[closer_ind].num_delimiters == needed_len
+ {
+ // it is a span
+
+ // disable both
+ delimiters[del_ind].is_active = false;
+ delimiters[closer_ind].is_active = false;
+ delimiters[del_ind].num_delimiters = 0;
+ delimiters[closer_ind].num_delimiters = 0;
+
+ // readd escaped tick
+ if escaped {
+ delimiters[del_ind - 1].num_delimiters -= 1;
+ current_span.push(delimiters[del_ind].char_type.to_char());
+ }
+
+ if !current_span.is_empty() {
+ new_result.push(current_span);
+ current_span = String::new();
+ }
+
+ let start_ind = new_result.len();
+
+ // to make easy to remove edge spaces if any
+ let mut has_none_space = false;
+
+ del_ind += 1;
+ while res_ind <= delimiters[closer_ind].ind {
+ while del_ind < closer_ind && delimiters[del_ind].ind <= res_ind {
+ // if there is a delimiter it will not be a space
+ has_none_space |= delimiters[del_ind].num_delimiters != 0;
+
+ // readd delimiter literal
+ current_span += &delimiters[del_ind]
+ .char_type
+ .to_char()
+ .to_string()
+ .repeat(delimiters[del_ind].num_delimiters);
+ // mark for removal
+ delimiters[del_ind].num_delimiters = 0;
+ del_ind += 1;
+ }
+ if res_ind != delimiters[closer_ind].ind {
+ // one fewer span
+ removed_spans += 1;
+ // check if has non space
+ has_none_space |= result[res_ind].contains(|c| c != ' ');
+ // merge
+ current_span += &result[res_ind];
+ res_ind += 1;
+ } else {
+ break;
+ }
+ }
+
+ // if the span starts and ends with a space
+ // and is not all space then remove first and last space
+ if has_none_space
+ && current_span.len() > 1
+ && current_span.starts_with(' ')
+ && current_span.ends_with(' ')
+ {
+ current_span = current_span[1..current_span.len() - 1].to_string();
+ // chop off each end
+ }
+ // adding a new span
+ if !current_span.is_empty() {
+ removed_spans -= 1;
+ new_result.push(current_span);
+ current_span = String::new();
+ }
+
+ let end_ind = new_result.len();
+
+ // add style span data
+ spans.push(SpanData {
+ start_idx: start_ind,
+ end_idx: end_ind,
+ code_bold_italic: SpanStyleEnum::Code,
+ });
+
+ break;
+ }
+ }
+ }
+ delimiters[del_ind].ind -= removed_spans;
+
+ del_ind += 1;
+ }
+
+ if res_ind != result.len() {
+ new_result.push(result[res_ind].clone());
+ }
+
+ res_ind += 1;
+ }
+
+ // remove all 0 length delimiters except for the null one
+ delimiters[0].num_delimiters = 1;
+ delimiters.retain(|d| d.num_delimiters != 0);
+ delimiters[0].num_delimiters = 0;
+
+ (new_result, spans)
+}
+
+fn md_parse_escapes(
+ result: Vec,
+ delimiters: &mut [DelimiterData],
+) -> (Vec, Vec) {
+ let mut new_result = vec![];
+ let mut new_delimiters = vec![];
+ let mut added_spans = 0;
+ let mut del_ind = 0;
+ let mut res_ind = 0;
+
+ let mut current_span = String::new();
+ while res_ind <= result.len() {
+ while del_ind < delimiters.len() && delimiters[del_ind].ind <= res_ind {
+ match delimiters[del_ind].char_type {
+ DelimiterType::Escape => {
+ if !current_span.is_empty() && delimiters[del_ind].num_delimiters != 0 {
+ new_result.push(current_span);
+ current_span = String::new();
+ }
+
+ // readd 1 '\' for every 2 rounded down
+ current_span += &delimiters[del_ind]
+ .char_type
+ .to_char()
+ .to_string()
+ .repeat(delimiters[del_ind].num_delimiters / 2);
+
+ // if escapes dont all cancel out
+ if delimiters[del_ind].num_delimiters % 2 != 0 {
+ if del_ind != delimiters.len() - 1
+ && delimiters[del_ind + 1].ind == delimiters[del_ind].ind
+ {
+ match delimiters[del_ind + 1].char_type {
+ DelimiterType::Tick
+ | DelimiterType::Asterisk
+ | DelimiterType::UnderScore => {
+ added_spans += 1;
+ current_span.push(delimiters[del_ind + 1].char_type.to_char());
+ delimiters[del_ind + 1].num_delimiters -= 1;
+ }
+ // escapes if adjacent should merge and null is only first
+ _ => panic!("\\ => should merge"),
+ }
+ } else {
+ // letter specific values
+ match result[delimiters[del_ind].ind].chars().next() {
+ Some('n') => {
+ current_span.push('\n');
+ current_span += &result[delimiters[del_ind].ind][1..];
+ res_ind += 1;
+ delimiters[del_ind].num_delimiters -= 1;
+ }
+ // not escapable so put \ back
+ _ => {
+ current_span.push(delimiters[del_ind].char_type.to_char());
+ current_span += &result[delimiters[del_ind].ind];
+ res_ind += 1;
+ delimiters[del_ind].num_delimiters -= 1;
+ }
+ }
+ }
+ }
+ }
+ DelimiterType::Tick => {
+ // unused from previous stage readd to string
+ current_span += &delimiters[del_ind]
+ .char_type
+ .to_char()
+ .to_string()
+ .repeat(delimiters[del_ind].num_delimiters);
+ }
+ DelimiterType::Null | DelimiterType::Asterisk | DelimiterType::UnderScore => {
+ // future stages assume no 0 len delimiters
+ if delimiters[del_ind].char_type == DelimiterType::Null
+ || delimiters[del_ind].num_delimiters != 0
+ {
+ new_delimiters.push(delimiters[del_ind].clone());
+ let last_ind = new_delimiters.len() - 1;
+ new_delimiters[last_ind].ind += added_spans;
+ }
+ }
+ }
+
+ del_ind += 1;
+ }
+ if !current_span.is_empty() {
+ new_result.push(current_span);
+ current_span = String::new();
+ }
+
+ if res_ind != result.len() {
+ current_span += &result[res_ind].clone();
+ }
+ res_ind += 1;
+ }
+
+ if !current_span.is_empty() {
+ new_result.push(current_span);
+ }
+
+ (new_result, new_delimiters)
+}
+
+fn md_parse_set_delimiter_open_close(result: &[String], delimiters: &mut [DelimiterData]) {
+ // set could open/close
+ for i in 0..delimiters.len() {
+ // find which char was before and after it
+ let prev_char;
+ let next_char;
+ if i != 0 && delimiters[i - 1].ind == delimiters[i].ind {
+ prev_char = delimiters[i - 1].char_type.to_char();
+ } else if delimiters[i].ind == 0 {
+ prev_char = ' ';
+ } else {
+ prev_char = result[delimiters[i].ind - 1]
+ .chars()
+ .last()
+ .expect("no 0 len spans");
+ }
+
+ if i != delimiters.len() - 1 && delimiters[i + 1].ind == delimiters[i].ind {
+ next_char = delimiters[i + 1].char_type.to_char();
+ } else if delimiters[i].ind == result.len() {
+ next_char = ' ';
+ } else {
+ next_char = result[delimiters[i].ind]
+ .chars()
+ .next()
+ .expect("no 0 len spans");
+ }
+
+ // if prev is whitespace cant end
+ // if next is whitespace cant start
+ // if neither whitespace but is underscore then cant either
+ match (prev_char.is_whitespace(), next_char.is_whitespace()) {
+ (false, false) => {
+ if delimiters[i].char_type == DelimiterType::UnderScore {
+ delimiters[i].could_open = false;
+ delimiters[i].could_close = false;
+ }
+ }
+ (true, false) => {
+ delimiters[i].could_close = false;
+ }
+ (false, true) => {
+ delimiters[i].could_open = false;
+ }
+ (true, true) => {
+ delimiters[i].could_open = false;
+ delimiters[i].could_close = false;
+ }
+ }
+
+ // if next is punctuation and prev is alphanumeric then cant start
+ // oposite for end
+ if next_char.is_ascii_punctuation()
+ && (!prev_char.is_whitespace() || !prev_char.is_ascii_punctuation())
+ {
+ delimiters[i].could_open = false;
+ }
+ if prev_char.is_ascii_punctuation()
+ && (!next_char.is_whitespace() || !next_char.is_ascii_punctuation())
+ {
+ delimiters[i].could_close = false;
+ }
+ }
+}
+
+fn md_parse_eval_spans(delimiters: &mut [DelimiterData]) -> Vec {
+ let mut spans = vec![];
+ let stack_bottom = 0; // because I have a null element in it
+ let mut current_position = stack_bottom + 1;
+ let mut opener_a = [stack_bottom; 3];
+ let mut opener_d = [stack_bottom; 3];
+
+ loop {
+ // find something which can close
+ while current_position != delimiters.len()
+ && !delimiters[current_position].could_close
+ && delimiters[current_position].is_active
+ {
+ current_position += 1;
+ }
+ if current_position == delimiters.len() {
+ break;
+ }
+ // check which type it is
+ let opener_min = match delimiters[current_position].char_type {
+ DelimiterType::Asterisk => &mut opener_a,
+ DelimiterType::UnderScore => &mut opener_d,
+ _ => panic!("this cant happen as current_position starts at 0 and all remaining delimiters are of above types"),
+ };
+
+ // min is the value upto which has already been checked for this type
+ let min = opener_min[delimiters[current_position].num_delimiters % 3].max(stack_bottom);
+
+ // go down from the previous until at min
+ let mut opener_ind = current_position - 1;
+ while opener_ind > min {
+ // found opener
+ if delimiters[opener_ind].is_active
+ && delimiters[opener_ind].could_open
+ && delimiters[opener_ind].char_type == delimiters[current_position].char_type
+ // see spec
+ // if one of them could open and close then sum cant be multiple of 3 unless both are
+ && !((delimiters[opener_ind].could_close
+ || delimiters[current_position].could_open)
+ && delimiters[opener_ind].num_delimiters % 3
+ != delimiters[current_position].num_delimiters % 3)
+ {
+ // found valid opener
+ break;
+ }
+ opener_ind -= 1;
+ }
+
+ // if hit min then there was no valid one
+ if opener_ind == min {
+ // update checked upto point
+ opener_min[delimiters[current_position].num_delimiters % 3] = current_position - 1;
+ current_position += 1;
+ } else {
+ // a delimiter cant both open and close
+ delimiters[current_position].could_open = false;
+ delimiters[opener_ind].could_close = false;
+
+ // it is strong emphasis if both have more than 2 delimiters
+ let strong = delimiters[opener_ind].num_delimiters >= 2
+ && delimiters[current_position].num_delimiters >= 2;
+ // create style data for this
+ spans.push(SpanData {
+ start_idx: delimiters[opener_ind].ind,
+ end_idx: delimiters[current_position].ind,
+ code_bold_italic: match strong {
+ true => SpanStyleEnum::Bold,
+ false => SpanStyleEnum::Italic,
+ },
+ });
+
+ // decrease remaining delimiters
+ delimiters[opener_ind].num_delimiters -= 1 + (strong as usize);
+ delimiters[current_position].num_delimiters -= 1 + (strong as usize);
+
+ // if go to 0 deactivate
+ if delimiters[opener_ind].num_delimiters == 0 {
+ delimiters[opener_ind].is_active = false;
+ }
+ if delimiters[current_position].num_delimiters == 0 {
+ delimiters[current_position].is_active = false;
+ current_position += 1;
+ }
+
+ // deactiveate all delimiters inside the new style span
+ for d in &mut delimiters[(opener_ind + 1)..current_position] {
+ d.is_active = false;
+ }
+ }
+ }
+ spans
+}
+
+fn md_parse(text_value: &str) -> (Vec, Vec) {
+ // parse the string into a vec of strings which are seperated by delimiters
+ // delimiters starts with a null delimiter and is always sorted by ind
+ // there are no 0 length delimiters except for the first null one
+ let (result, mut delimiters) = md_parse_delimiters(text_value);
+
+ // parse code blocks and any escapes directly effecting it
+ // code blocks have the highest precedence
+ let (result, mut span_data) = md_parse_code_blocks(result, &mut delimiters);
+
+ // other escapes are parsed and remaining ticks are reinserted
+ // after this point there should be no ticks or escapes in delimiters
+ let (mut result, mut delimiters) = md_parse_escapes(result, &mut delimiters);
+
+ // for each remaining delimiter it is checked whether
+ // it could start or end the style
+ md_parse_set_delimiter_open_close(&result, &mut delimiters);
+
+ // the delimiters are parsed and evaluated which ones form
+ // pairs and create style regions
+ // this does not remove delimiters which are fully used
+ // so after this 0 length delimiters can exist
+ span_data.append(&mut md_parse_eval_spans(&mut delimiters));
+
+ let mut final_result = vec![];
+
+ // all remaining delimiters are reinserted into the string as seperate spans
+ for d in delimiters.into_iter().rev() {
+ while d.ind < result.len() {
+ if let Some(thing) = result.pop() {
+ final_result.push(thing);
+ }
+ }
+
+ // update style regions
+ if d.char_type != DelimiterType::Null && d.num_delimiters != 0 {
+ for s in span_data.iter_mut() {
+ // if start needs to be after or equal
+ if s.start_idx >= d.ind {
+ s.start_idx += 1;
+ }
+ if s.end_idx > d.ind {
+ // if end needs to be after
+ s.end_idx += 1;
+ }
+ }
+
+ final_result.push(d.char_type.to_char().to_string().repeat(d.num_delimiters));
+ }
+ }
+
+ (final_result.into_iter().rev().collect(), span_data)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_md_parse() {
+ // the basic examples no actual md
+
+ let text = r"Hello, \nworld!";
+ assert_eq!(md_parse(text).0, ["Hello, ", "\nworld!"]);
+ assert_eq!(md_parse(text).1, []);
+
+ // when not part of a '\n', '\' is not special
+ let text = r"Hello, world! \1";
+ assert_eq!(md_parse(text).0, ["Hello, world! ", "\\1"]);
+ assert_eq!(md_parse(text).1, []);
+
+ // when precedes '\n', '\' escapes it.
+ let text = r"Hello, \\nworld!";
+ assert_eq!(md_parse(text).0, ["Hello, ", "\\", "nworld!"]);
+ assert_eq!(md_parse(text).1, []);
+
+ fn sd(s: usize, e: usize, i: u8) -> SpanData {
+ SpanData {
+ start_idx: s,
+ end_idx: e,
+ code_bold_italic: match i {
+ 0 => SpanStyleEnum::Code,
+ 1 => SpanStyleEnum::Bold,
+ 2 => SpanStyleEnum::Italic,
+ _ => unreachable!("set by tests to only be 0 1 or 2"),
+ },
+ }
+ }
+
+ // using the md
+ let text = r"He*ll*o, \nworld!";
+ assert_eq!(md_parse(text).0, ["He", "ll", "o, ", "\nworld!"]);
+ assert_eq!(md_parse(text).1, [sd(1, 2, 2)]);
+
+ // mismatched
+ let text = r"*Hello** , \nworld!";
+ assert_eq!(md_parse(text).0, ["Hello", "*", " , ", "\nworld!"]);
+ assert_eq!(md_parse(text).1, [sd(0, 1, 2)]);
+
+ // diff type
+ let text = r"He*llo_, \nworld!";
+ assert_eq!(md_parse(text).0, ["He", "*", "llo", "_", ", ", "\nworld!"]);
+ assert_eq!(md_parse(text).1, []);
+
+ // multiple diff type
+ let text = r"_hello*";
+ assert_eq!(md_parse(text).0, ["_", "hello", "*"]);
+ assert_eq!(md_parse(text).1, []);
+
+ // multiple same type
+ let text = r"He*ll*o, \nw*or*ld!";
+ assert_eq!(md_parse(text).0, ["He", "ll", "o, ", "\nw", "or", "ld!"]);
+ assert_eq!(md_parse(text).1, [sd(1, 2, 2), sd(4, 5, 2)]);
+
+ // space before
+ let text = r"**foo bar **";
+ assert_eq!(md_parse(text).0, ["**", "foo bar ", "**"]);
+ assert_eq!(md_parse(text).1, []);
+
+ // punctuation before alphnum after
+ let text = r"**(**foo)";
+ assert_eq!(md_parse(text).0, ["**", "(", "**", "foo)"]);
+ assert_eq!(md_parse(text).1, []);
+ }
+
+ #[test]
+ fn test_get_md_value() {
+ fn tc(i: u32, t: &str) -> MdSpan {
+ MdSpan {
+ code: i & (1 << 0) != 0,
+ bold: i & (1 << 1) != 0,
+ italic: i & (1 << 2) != 0,
+ text: t.to_string(),
+ }
+ }
+
+ let mut el = SvgElement::new("text", &[]);
+ let text = r"foo";
+ el.set_attr("md", text);
+ assert_eq!(get_md_value(&mut el), [tc(0, "foo")]);
+
+ let text = r"**(**foo)";
+ el.set_attr("md", text);
+ assert_eq!(get_md_value(&mut el), [tc(0, "**(**foo)")]);
+
+ let text = r"*foo *bar**";
+ el.set_attr("md", text);
+ assert_eq!(get_md_value(&mut el), [tc(4, "foo bar")]);
+
+ let text = r"*foo**bar**baz*";
+ el.set_attr("md", text);
+ assert_eq!(
+ get_md_value(&mut el),
+ [tc(4, "foo"), tc(6, "bar"), tc(4, "baz")]
+ );
+
+ let text = r"`foo*`";
+ el.set_attr("md", text);
+ assert_eq!(get_md_value(&mut el), [tc(1, "foo*")]);
+
+ // if first and last chars in code block are space remove them unless all empty
+ let text = r"` `` `";
+ el.set_attr("md", text);
+ assert_eq!(get_md_value(&mut el), [tc(1, "``")]);
+
+ let text = r"` `";
+ el.set_attr("md", text);
+ assert_eq!(get_md_value(&mut el), [tc(1, " ")]);
+ }
+}
diff --git a/src/elements/mod.rs b/src/elements/mod.rs
index 5dbe0cf..509a471 100644
--- a/src/elements/mod.rs
+++ b/src/elements/mod.rs
@@ -4,6 +4,7 @@ mod containers;
mod element;
mod layout;
mod loops;
+mod markdown;
mod path;
mod reuse;
mod special;
diff --git a/src/elements/text.rs b/src/elements/text.rs
index ef8c4da..48a3ed5 100644
--- a/src/elements/text.rs
+++ b/src/elements/text.rs
@@ -1,7 +1,10 @@
+use itertools::Itertools;
+
use super::SvgElement;
use crate::geometry::LocSpec;
use crate::types::{attr_split_cycle, fstr, strp};
+use crate::elements::markdown::{get_md_value, MdSpan};
use crate::errors::{Result, SvgdxError};
fn get_text_value(element: &mut SvgElement) -> String {
@@ -162,22 +165,97 @@ pub fn process_text_attr(element: &SvgElement) -> Result<(SvgElement, Vec = text_value.lines().collect();
+ // lines is a vec of (line)s
+ // a line is a vec of spans
+ // it starts with a single empty line
+ let mut lines = vec![vec![]];
+ for span in spans.iter() {
+ let mut segments = span.text.lines();
+ if let Some(first) = segments.next() {
+ if !first.is_empty() {
+ lines
+ .last_mut()
+ .expect("added item not removed")
+ .push(MdSpan {
+ code: span.code,
+ bold: span.bold,
+ italic: span.italic,
+ text: first.to_string(),
+ });
+ }
+ }
+
+ for s in segments {
+ lines.push(vec![MdSpan {
+ code: span.code,
+ bold: span.bold,
+ italic: span.italic,
+ text: s.to_string(),
+ }]);
+ }
+
+ if let Some(last_char) = span.text.chars().last() {
+ if last_char == '\n' {
+ lines.push(vec![]);
+ }
+ }
+ }
+ // if last char is newline dont do the new line
+ if let Some(last_span) = spans.last() {
+ if let Some(last_char) = last_span.text.chars().last() {
+ if last_char == '\n' {
+ lines.pop();
+ }
+ }
+ }
+
+ // fill empty lines with empty strings
+ for l in &mut lines {
+ if l.is_empty() {
+ l.push(MdSpan {
+ code: false,
+ bold: false,
+ italic: false,
+ text: String::new(),
+ });
+ }
+ }
let line_count = lines.len();
- let multiline = line_count > 1;
+ let multielement = line_count > 1 || spans.len() > 1;
let vertical = orig_elem.has_class("d-text-vertical");
// Whether text is pre-formatted (i.e. spaces are not collapsed)
let text_pre = orig_elem.has_class("d-text-pre");
- // There will always be a text element; if not multiline this is the only element.
+ // There will always be a text element; if not multielement this is the only element.
let mut text_elem = if orig_elem.name() == "text" {
orig_elem.clone()
} else {
@@ -244,6 +322,18 @@ pub fn process_text_attr(element: &SvgElement) -> Result<(SvgElement, Vec Result<(SvgElement, Vec Result<(SvgElement, Vec
+"#;
+ let expected = r#"
+
+
+multiline
+
+"#;
+ assert_eq!(
+ transform_str_default(input).unwrap().trim(),
+ expected.trim()
+ );
+
+ let input = r#"
+
+"#;
+ let expected = r#"
+
+hellomark down
+
+"#;
+
+ assert_eq!(
+ transform_str_default(input).unwrap().trim(),
+ expected.trim()
+ );
+
+ let input = r#"
+
+"#;
+ let expected = r#"
+
+
+ark downmhello
+
+"#;
+
+ assert_eq!(
+ transform_str_default(input).unwrap().trim(),
+ expected.trim()
+ );
+
+ let input = r#"
+
+"#;
+ let expected = r#"
+
+
+ark* down*mhello
+
+"#;
+
+ assert_eq!(
+ transform_str_default(input).unwrap().trim(),
+ expected.trim()
+ );
+}