use crate::vocab::{Emph, Vocab};
use crate::{Dialect, Repairs};
struct Open {
emph: Emph,
at: usize,
run: usize,
ch: char,
guess: bool,
after_space: bool,
soft: bool,
}
pub(crate) struct Inline {
open: Vec<Open>,
prev: Option<char>,
scratch: String,
code_src: Vec<char>,
dialect: Dialect,
pub repairs: Repairs,
}
impl Inline {
pub fn new(dialect: Dialect) -> Self {
Self {
open: Vec::new(),
prev: None,
scratch: String::new(),
code_src: Vec::new(),
dialect,
repairs: Repairs::default(),
}
}
pub fn reset(&mut self) {
self.open.clear();
self.prev = None;
}
pub fn safe_len(&self, out_len: usize) -> usize {
self.open.first().map_or(out_len, |o| o.at)
}
pub fn shift(&mut self, n: usize) {
for o in &mut self.open {
o.at -= n;
}
}
pub fn end_line(&mut self) {
self.prev = Some('\n');
}
pub fn note_raw(&mut self, s: &str) {
if self.open.last().is_some_and(|o| o.emph == Emph::Code) {
self.code_src.extend(s.chars());
}
}
pub fn set_prev(&mut self, c: Option<char>) {
self.prev = c;
}
pub fn is_open(&self) -> bool {
!self.open.is_empty()
}
pub fn render(&mut self, line: &[char], out: &mut String, v: &Vocab) {
let mut i = 0;
while i < line.len() {
if let Some(top) = self.open.last() {
if top.emph == Emph::Code {
let run = if line[i] == '`' { run_len(line, i, '`') } else { 0 };
if run == top.run {
self.close_at(self.open.len() - 1, out, v);
i += run;
} else {
let step = run.max(1);
for k in 0..step {
v.escape_char(line[i + k], out);
self.code_src.push(line[i + k]);
}
i += step;
}
continue;
}
}
let c = line[i];
if c == '[' {
if let Some((text, url_from, url_to)) = find_link(line, i) {
self.render_link(&line[text.0..text.1], &line[url_from..url_to], out, v);
i = url_to + 1;
continue;
}
}
if c == '<' {
if let Some(step) = self.angle(line, i, out, v) {
i += step;
continue;
}
}
if c == '\\' {
if let Some(&next) = line.get(i + 1) {
if next.is_ascii_punctuation() {
v.literal(next, out);
self.prev = Some(next);
i += 2;
continue;
}
}
}
if c == '`' {
let run = run_len(line, i, '`');
let prev = self.prev_char(line, i);
self.code_src.clear();
self.open.push(Open {
emph: Emph::Code,
at: out.len(),
run,
ch: '`',
guess: false,
after_space: prev.is_none_or(char::is_whitespace),
soft: false,
});
i += run;
continue;
}
if !matches!(c, '*' | '_' | '~') {
v.escape_char(c, out);
i += 1;
continue;
}
let take = run_len(line, i, c);
let mrkdwn = self.dialect == Dialect::SlackMrkdwn;
let take = if c != '~' && take == 3 && !mrkdwn {
if self.open.iter().any(|o| o.emph == Emph::Italic) {
1
} else if self.open.iter().any(|o| o.emph == Emph::Bold) {
3
} else {
2
}
} else {
take
};
let emph = match (c, take) {
('~', _) => Emph::Strike,
('*', _) if mrkdwn => Emph::Bold,
(_, 1) => Emph::Italic,
_ => Emph::Bold,
};
if c == '~' && take < 2 && !mrkdwn {
v.escape_char(c, out);
i += 1;
continue;
}
let prev = self.prev_char(line, i);
let next = line.get(i + take).copied();
let after_space = prev.is_none_or(char::is_whitespace);
let same = self.open.iter().rposition(|o| o.emph == emph);
let intraword = c == '_' && prev.is_some_and(char::is_alphanumeric);
if intraword && same.is_none() {
for _ in 0..take {
v.escape_char(c, out);
}
i += take;
continue;
}
let soft = mrkdwn && c == '~' && take == 1;
let soft_blocked_open =
soft && (prev.is_some_and(is_word_char) || next.is_some_and(|n| n.is_ascii_digit()));
let soft_blocked_close = soft && next.is_some_and(|n| n.is_ascii_alphanumeric());
let left = can_open(prev, next) && !intraword && !soft_blocked_open;
match same {
Some(at) if self.open[at].guess && (!left || !after_space) => {
for _ in 0..take {
v.escape_char(c, out);
}
}
Some(at) if self.open[at].guess => self.reopen_at(at, out, v, (emph, take, c, after_space, soft)),
Some(_) if !after_space && soft_blocked_close => v.escape_char(c, out),
Some(at) if !after_space => self.close_at(at, out, v),
Some(at) if left && at + 1 == self.open.len() => {
self.reopen_at(at, out, v, (emph, take, c, after_space, soft))
}
Some(_) if left => {}
Some(at) => self.close_at(at, out, v),
None if left => self.open.push(Open {
emph,
at: out.len(),
run: take,
ch: c,
guess: false,
after_space,
soft,
}),
None => self.open.push(Open {
emph,
at: out.len(),
run: take,
ch: c,
guess: true,
after_space,
soft,
}),
}
i += take;
}
if let Some(&last) = line.last() {
self.prev = Some(last);
}
}
pub fn finish_block(&mut self, out: &mut String, v: &Vocab) {
while let Some(top) = self.open.last() {
if top.emph == Emph::Code {
self.revert_code_span(out, v);
continue;
}
self.finalize(out, v, false);
}
self.prev = None;
}
fn revert_code_span(&mut self, out: &mut String, v: &Vocab) {
let Some(open) = self.open.pop() else { return };
self.repairs.reverted_code_span += 1;
out.truncate(open.at);
for _ in 0..open.run {
out.push(open.ch);
}
let src = std::mem::take(&mut self.code_src);
self.prev = Some(open.ch);
self.render(&src, out, v);
if self.code_src.is_empty() {
self.code_src = src;
self.code_src.clear();
}
}
fn reopen_at(&mut self, at: usize, out: &mut String, v: &Vocab, fresh: (Emph, usize, char, bool, bool)) {
let (emph, take, c, after_space, soft) = fresh;
while self.open.len() > at + 1 {
self.finalize(out, v, false);
}
let old = self.open.pop().expect("at 은 유효한 인덱스다");
if old.run == 1 || (old.guess && old.after_space) {
for _ in 0..old.run {
out.insert(old.at, old.ch);
}
} else {
self.repairs.dropped_marker += 1;
}
self.open.push(Open { emph, at: out.len(), run: take, ch: c, guess: false, after_space, soft });
}
fn prev_char(&self, line: &[char], i: usize) -> Option<char> {
if i > 0 {
Some(line[i - 1])
} else {
self.prev
}
}
fn close_at(&mut self, at: usize, out: &mut String, v: &Vocab) {
while self.open.len() > at + 1 {
self.finalize(out, v, false);
}
self.finalize(out, v, true);
}
fn finalize(&mut self, out: &mut String, v: &Vocab, matched: bool) {
let Some(open) = self.open.pop() else { return };
let empty = out.len() == open.at;
if open.guess || empty || (open.soft && !matched) {
if open.after_space || open.run == 1 {
for _ in 0..open.run {
out.insert(open.at, open.ch);
}
} else {
self.repairs.dropped_marker += 1;
}
return;
}
if !matched {
self.repairs.closed_emphasis += 1;
}
if open.emph == Emph::Code && !v.is_plain() {
let body = &out[open.at..];
if body.len() >= 2
&& body.starts_with(' ')
&& body.ends_with(' ')
&& !body.chars().all(|c| c == ' ')
{
out.truncate(out.len() - 1);
out.remove(open.at);
}
}
if open.emph == Emph::Code && v.open(Emph::Code) == "`" {
let body = &out[open.at..];
let longest = longest_run(body, '`');
if longest > 0 {
let pad = body.starts_with('`') || body.ends_with('`');
let mut fence = String::with_capacity(longest + 2);
for _ in 0..longest + 1 {
fence.push('`');
}
if pad {
out.push(' ');
}
out.push_str(&fence);
if pad {
fence.push(' ');
}
out.insert_str(open.at, &fence);
return;
}
}
out.insert_str(open.at, v.open(open.emph));
out.push_str(v.close(open.emph));
}
fn angle(&mut self, line: &[char], i: usize, out: &mut String, v: &Vocab) -> Option<usize> {
let rest = &line[i..];
if starts_with(rest, "<!--") {
let end = find_seq(&rest[4..], "-->")? + 4;
return Some(end + 3);
}
let close = rest.iter().position(|&c| c == '>')?;
if starts_with(&rest[1..], "http://") || starts_with(&rest[1..], "https://") {
let body = &rest[1..close];
let (url, label) = match body.iter().position(|&c| c == '|') {
Some(bar) => (&body[..bar], &body[bar + 1..]),
None => (body, body),
};
if url.iter().any(|c| c.is_whitespace()) {
return None;
}
let mut href = String::with_capacity(url.len());
href.extend(url.iter());
let mut text = std::mem::take(&mut self.scratch);
text.clear();
for &c in label {
v.escape_char(c, &mut text);
}
v.link(&text, &href, out);
self.scratch = text;
self.prev = Some('>');
return Some(close + 1);
}
let closing = rest.get(1) == Some(&'/');
let name_at = if closing { 2 } else { 1 };
let mut j = name_at;
while j < close && rest[j].is_ascii_alphanumeric() {
j += 1;
}
let name = &rest[name_at..j];
if name.is_empty() || !(rest[j] == '>' || rest[j] == '/' || rest[j].is_whitespace()) {
return None;
}
if !is_known_tag(name) {
return None;
}
if !closing && eq_ignore_case(name, "br") {
out.push('\n');
self.prev = Some('\n');
}
Some(close + 1)
}
fn render_link(&mut self, text: &[char], url: &[char], out: &mut String, v: &Vocab) {
let mut scratch = std::mem::take(&mut self.scratch);
scratch.clear();
let mut nested = Inline::new(self.dialect);
nested.render(text, &mut scratch, v);
nested.finish_block(&mut scratch, v);
self.repairs.add(nested.repairs);
let mut href = String::with_capacity(url.len());
href.extend(url.iter());
v.link(&scratch, &href, out);
self.scratch = scratch;
}
}
fn find_link(line: &[char], at: usize) -> Option<((usize, usize), usize, usize)> {
let mut j = at + 1;
let mut depth = 0usize;
while j < line.len() {
match line[j] {
'[' => depth += 1,
']' if depth == 0 => break,
']' => depth -= 1,
_ => {}
}
j += 1;
}
if j >= line.len() || line.get(j + 1) != Some(&'(') {
return None;
}
let k = line[j + 2..].iter().position(|&c| c == ')')? + j + 2;
Some(((at + 1, j), j + 2, k))
}
fn starts_with(chars: &[char], s: &str) -> bool {
let mut it = chars.iter();
s.chars().all(|c| it.next() == Some(&c))
}
fn find_seq(chars: &[char], s: &str) -> Option<usize> {
(0..chars.len()).find(|&k| starts_with(&chars[k..], s))
}
fn eq_ignore_case(chars: &[char], s: &str) -> bool {
chars.len() == s.len() && chars.iter().zip(s.chars()).all(|(a, b)| a.eq_ignore_ascii_case(&b))
}
fn is_known_tag(name: &[char]) -> bool {
const KNOWN: [&str; 21] = [
"br", "sub", "sup", "b", "strong", "i", "em", "u", "s", "strike", "del", "code", "span",
"div", "p", "small", "mark", "kbd", "font", "center", "details",
];
KNOWN.iter().any(|t| eq_ignore_case(name, t)) || eq_ignore_case(name, "summary")
}
fn longest_run(s: &str, c: char) -> usize {
let mut best = 0;
let mut cur = 0;
for x in s.chars() {
if x == c {
cur += 1;
best = best.max(cur);
} else {
cur = 0;
}
}
best
}
fn run_len(line: &[char], at: usize, c: char) -> usize {
line[at..].iter().take_while(|&&x| x == c).count()
}
fn can_open(prev: Option<char>, next: Option<char>) -> bool {
next.is_some_and(|n| {
!n.is_whitespace() && (!is_punct(n) || prev.is_none_or(|p| !is_word_char(p)))
})
}
fn is_word_char(c: char) -> bool {
c.is_alphabetic() || c.is_ascii_digit()
}
fn is_punct(c: char) -> bool {
c.is_ascii_punctuation()
|| matches!(c,
',' | '。' | '、' | '!' | '?' | ';' | ':' | '·' | '…' | '—' | '~'
| '「' | '」' | '『' | '』' | '(' | ')' | '【' | '】' | '《' | '》'
| '“' | '”' | '‘' | '’')
}