use crate::inline::Inline;
use crate::sink::Sink;
use crate::vocab::{Emph, Vocab};
use crate::width::str_width;
use crate::{Channel, Dialect, Options, Repairs};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
None,
Para,
Heading,
List,
Quote,
Fence,
Table,
}
pub(crate) struct Engine {
pub v: Vocab,
inline: Inline,
pending: Vec<char>,
line_open: bool,
kind: LineKind,
out: String,
state: State,
heading: usize,
wrote: bool,
blank: bool,
held: Option<String>,
scratch: String,
table: Table,
fence: FenceState,
cr: bool,
closed_fence: usize,
dialect: Dialect,
}
#[derive(Default)]
struct FenceState {
ch: char,
len: usize,
info: String,
body: bool,
}
impl Engine {
pub fn new(channel: Channel, options: Options) -> Self {
Self {
v: Vocab::new(channel),
inline: Inline::new(options.from),
pending: Vec::new(),
line_open: false,
kind: LineKind::Para,
out: String::new(),
state: State::None,
heading: 0,
wrote: false,
blank: false,
held: None,
scratch: String::new(),
table: Table::default(),
fence: FenceState::default(),
cr: false,
closed_fence: 0,
dialect: options.from,
}
}
pub fn repairs(&self) -> Repairs {
let mut r = self.inline.repairs;
r.closed_fence += self.closed_fence;
r
}
pub fn feed<S: Sink>(&mut self, chunk: &str, sink: &mut S) {
if chunk.is_empty() {
return;
}
if std::mem::take(&mut self.cr) && !chunk.starts_with('\n') {
self.pending.push('\r');
}
for seg in chunk.split_inclusive('\n') {
match seg.strip_suffix('\n') {
Some(rest) => {
let rest = rest.strip_suffix('\r').unwrap_or(rest);
self.pending.extend(rest.chars());
self.progress(true, sink);
}
None => {
let seg = match seg.strip_suffix('\r') {
Some(s) => {
self.cr = true;
s
}
None => seg,
};
self.pending.extend(seg.chars());
self.progress(false, sink);
}
}
}
}
pub fn finish<S: Sink>(&mut self, sink: &mut S) {
if std::mem::take(&mut self.cr) {
self.pending.push('\r');
}
if !self.pending.is_empty() || self.line_open {
self.progress(true, sink);
}
if let Some(held) = self.held.take() {
self.whole_para(&held, sink);
}
if self.state == State::Fence {
self.closed_fence += 1;
}
self.close_block(sink);
self.flush_all(sink);
sink.boundary();
}
fn progress<S: Sink>(&mut self, eol: bool, sink: &mut S) {
if self.state == State::Fence || self.state == State::Table || self.held.is_some() {
if !eol {
return;
}
if self.consume_whole_line(sink) {
return;
}
}
if !self.line_open {
match classify(&self.pending, eol, true) {
Decision::NeedMore => return,
Decision::Whole(kind) => {
self.whole(kind, sink);
self.pending.clear();
return;
}
Decision::Prefix(kind, len) => {
self.open_line(kind, len, sink);
}
}
}
let cut = if eol { trim_end(&self.pending) } else { safe_cut(&self.pending) };
if cut > 0 {
self.inline.render(&self.pending[..cut], &mut self.out, &self.v);
self.pending.drain(..cut);
}
if eol {
self.pending.clear();
self.end_line(sink);
}
self.flush_safe(sink);
}
fn consume_whole_line<S: Sink>(&mut self, sink: &mut S) -> bool {
if self.state == State::Fence {
let line = self.take_line();
let closing = fence_marker(line.trim_start())
.is_some_and(|(ch, len, _)| ch == self.fence.ch && len >= self.fence.len);
if closing {
self.close_block(sink);
} else {
self.fence_body(&line);
self.flush_all(sink);
}
self.scratch = line;
return true;
}
if let Some(held) = self.held.take() {
let line = self.take_line();
if is_delimiter_row(&line) && self.table.begin(&held, &line) {
self.pending.clear();
self.scratch = line;
self.close_block(sink);
self.state = State::Table;
return true;
}
self.pending.clear();
self.pending.extend(line.chars());
self.scratch = line;
self.whole_para(&held, sink);
return false;
}
if self.state == State::Table {
let line = self.take_line();
let row = !line.trim().is_empty() && line.contains('|');
if row {
self.pending.clear();
self.table.push(&line);
} else {
self.pending.clear();
self.pending.extend(line.chars());
self.close_block(sink);
}
self.scratch = line;
return row;
}
false
}
fn take_line(&mut self) -> String {
let mut line = std::mem::take(&mut self.scratch);
line.clear();
line.extend(self.pending.iter());
self.pending.clear();
line
}
fn whole<S: Sink>(&mut self, kind: WholeKind, sink: &mut S) {
match kind {
WholeKind::Blank => {
self.close_block(sink);
self.blank = self.wrote;
}
WholeKind::Rule => {
self.close_block(sink);
self.start_line();
self.out.push_str(self.v.rule());
self.close_block(sink);
}
WholeKind::Fence => {
let line = self.take_line();
let t = line.trim_start();
let (ch, len, info) = fence_marker(t).expect("분류가 펜스라고 했다");
self.close_block(sink);
self.start_line();
self.fence = FenceState { ch, len, info: info.to_string(), body: false };
self.state = State::Fence;
let info = std::mem::take(&mut self.fence.info);
self.v.verbatim_open(&info, &mut self.out);
self.fence.info = info;
self.flush_all(sink);
self.scratch = line;
}
WholeKind::TableCandidate => {
self.held = Some(self.pending.iter().collect());
}
}
}
fn open_line<S: Sink>(&mut self, kind: LineKind, prefix: usize, sink: &mut S) {
match kind {
LineKind::Para if self.state == State::List => {
self.out.push('\n');
self.inline.note_raw("\n");
self.inline.end_line();
for _ in 0..prefix.min(8) {
self.out.push(' ');
self.inline.note_raw(" ");
}
}
LineKind::Para => {
if self.state != State::Para {
self.close_block(sink);
self.state = State::Para;
self.start_line();
} else {
self.out.push('\n');
self.inline.note_raw("\n");
self.inline.end_line();
}
}
LineKind::Heading(level) => {
self.close_block(sink);
self.state = State::Heading;
self.start_line();
self.open_heading(level);
}
LineKind::Quote => {
if self.state != State::Quote {
self.close_block(sink);
self.state = State::Quote;
self.start_line();
self.out.push_str(self.v.quote_open());
} else {
self.out.push('\n');
self.inline.note_raw("\n");
self.inline.end_line();
}
self.out.push_str(self.v.quote_prefix());
self.inline.note_raw(self.v.quote_prefix());
self.inline.set_prev(None);
}
LineKind::Bullet(indent) | LineKind::Ordered(indent, _) => {
if self.state != State::List {
self.close_block(sink);
self.state = State::List;
} else {
self.inline.finish_block(&mut self.out, &self.v);
}
self.start_line();
for _ in 0..indent.min(8) {
self.out.push(' ');
}
match kind {
LineKind::Ordered(_, n) => {
push_usize(&mut self.out, n);
self.out.push_str(". ");
}
_ => self.out.push_str(self.v.bullet()),
}
self.inline.set_prev(None);
}
}
self.pending.drain(..prefix);
self.kind = kind;
self.line_open = true;
}
fn end_line<S: Sink>(&mut self, sink: &mut S) {
match self.kind {
LineKind::Heading(_) => self.close_block(sink),
LineKind::Bullet(_) | LineKind::Ordered(..) | LineKind::Para | LineKind::Quote => {
self.inline.end_line()
}
}
self.line_open = false;
}
fn whole_para<S: Sink>(&mut self, line: &str, sink: &mut S) {
let saved = std::mem::take(&mut self.pending);
self.pending.extend(line.chars());
self.line_open = false;
match classify(&self.pending, true, false) {
Decision::Prefix(kind, len) => {
self.open_line(kind, len, sink);
let cut = trim_end(&self.pending);
if cut > 0 {
self.inline.render(&self.pending[..cut], &mut self.out, &self.v);
}
self.end_line(sink);
}
_ => self.whole(WholeKind::Blank, sink),
}
self.pending = saved;
self.flush_safe(sink);
}
fn open_heading(&mut self, level: usize) {
let level = if self.heading == 0 { level } else { level.min(self.heading + 1) };
self.heading = level;
let max = self.v.max_heading();
if max > 0 {
for _ in 0..level.min(max) {
self.out.push('#');
}
self.out.push(' ');
} else if !self.v.is_plain() {
self.out.push_str(self.v.open(Emph::Bold));
}
self.inline.set_prev(None);
}
fn fence_body(&mut self, line: &str) {
if self.fence.body || self.v.verbatim_body_newline() {
self.out.push('\n');
}
self.fence.body = true;
self.v.escape(line, &mut self.out);
}
fn start_line(&mut self) {
if self.wrote {
self.out.push('\n');
if self.blank {
self.out.push('\n');
}
}
self.blank = false;
self.wrote = true;
}
pub fn close_open(&self, out: &mut String) {
self.block_close_markup(out);
}
fn block_close_markup(&self, out: &mut String) {
match self.state {
State::Heading => {
if self.v.max_heading() == 0 && !self.v.is_plain() {
out.push_str(self.v.close(Emph::Bold));
}
}
State::Quote => out.push_str(self.v.quote_close()),
State::Fence => self.v.verbatim_close(&self.fence.info, out),
State::None | State::Para | State::List | State::Table => {}
}
}
fn close_block<S: Sink>(&mut self, sink: &mut S) {
match self.state {
State::None => {}
State::Para | State::List | State::Heading | State::Quote => {
self.inline.finish_block(&mut self.out, &self.v);
let mut out = std::mem::take(&mut self.out);
self.block_close_markup(&mut out);
self.out = out;
}
State::Fence => {
let mut out = std::mem::take(&mut self.out);
self.block_close_markup(&mut out);
self.out = out;
}
State::Table => {
let mut table = std::mem::take(&mut self.table);
self.start_line();
table.render(&self.v, self.dialect, &mut self.inline.repairs, &mut self.out);
table.clear();
self.table = table;
}
}
self.state = State::None;
self.inline.reset();
self.flush_all(sink);
sink.boundary();
}
fn flush_safe<S: Sink>(&mut self, sink: &mut S) {
let safe = self.inline.safe_len(self.out.len());
if safe == 0 {
return;
}
sink.text(&self.out[..safe]);
self.out.drain(..safe);
self.inline.shift(safe);
}
fn flush_all<S: Sink>(&mut self, sink: &mut S) {
debug_assert!(!self.inline.is_open(), "열린 마크업이 있는 채로 전부 내보낼 수 없다");
if !self.out.is_empty() {
sink.text(&self.out);
self.out.clear();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LineKind {
Para,
Heading(usize),
Quote,
Bullet(usize),
Ordered(usize, usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WholeKind {
Blank,
Rule,
Fence,
TableCandidate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Decision {
NeedMore,
Prefix(LineKind, usize),
Whole(WholeKind),
}
fn marker_space(c: char) -> bool {
c == ' ' || c == '\t'
}
fn classify(p: &[char], eol: bool, can_table: bool) -> Decision {
let indent = p.iter().take_while(|&&c| c == ' ' || c == '\t').count();
if indent == p.len() {
return if eol { Decision::Whole(WholeKind::Blank) } else { Decision::NeedMore };
}
let t = &p[indent..];
let para = Decision::Prefix(LineKind::Para, indent);
let at_end = |d: Decision| if eol { para } else { d };
match t[0] {
'#' => {
let n = run(t, '#');
if n > 6 {
return para;
}
match t.get(n) {
Some(c) if marker_space(*c) => {
Decision::Prefix(LineKind::Heading(n), indent + n + 1)
}
Some(_) => para,
None => at_end(Decision::NeedMore),
}
}
'>' => match t.get(1) {
Some(' ') => Decision::Prefix(LineKind::Quote, indent + 2),
Some(_) => Decision::Prefix(LineKind::Quote, indent + 1),
None => {
if eol {
Decision::Prefix(LineKind::Quote, indent + 1)
} else {
Decision::NeedMore
}
}
},
c @ ('`' | '~') => {
let n = run(t, c);
if n >= 3 {
return if eol { Decision::Whole(WholeKind::Fence) } else { Decision::NeedMore };
}
if t.len() > n {
para
} else {
at_end(Decision::NeedMore)
}
}
'|' if can_table => {
if eol {
Decision::Whole(WholeKind::TableCandidate)
} else {
Decision::NeedMore
}
}
'+' | '•' => match t.get(1) {
Some(c) if marker_space(*c) => Decision::Prefix(LineKind::Bullet(indent), indent + 2),
Some(_) => para,
None => at_end(Decision::NeedMore),
},
c @ ('-' | '*' | '_') => {
let n = run(t, c);
if n == 1 && c != '_' {
return match t.get(1) {
Some(c) if marker_space(*c) => {
Decision::Prefix(LineKind::Bullet(indent), indent + 2)
}
Some(_) => para,
None => at_end(Decision::NeedMore),
};
}
if t[n..].iter().any(|&x| x != c && !marker_space(x)) {
return para;
}
if eol {
if t.iter().filter(|&&x| x == c).count() >= 3 {
Decision::Whole(WholeKind::Rule)
} else {
para
}
} else {
Decision::NeedMore
}
}
'0'..='9' => {
let d = t.iter().take_while(|c| c.is_ascii_digit()).count();
if d > 9 {
return para;
}
match (t.get(d), t.get(d + 1)) {
(Some('.' | ')'), Some(c)) if marker_space(*c) => {
let mut n = 0usize;
for c in &t[..d] {
n = n * 10 + (*c as usize - '0' as usize);
}
Decision::Prefix(LineKind::Ordered(indent, n), indent + d + 2)
}
(Some('.' | ')'), None) | (None, _) => at_end(Decision::NeedMore),
(Some(_), _) => para,
}
}
_ => para,
}
}
fn safe_cut(p: &[char]) -> usize {
let mut k = p.len();
let mut i = 0;
while i < k {
if p[i] != '[' {
i += 1;
continue;
}
match link_end(&p[i..k]) {
Some(end) => i += end,
None => {
k = i;
break;
}
}
}
let mut i = 0;
while i + 4 <= k {
if p[i..].starts_with(&['<', '!', '-', '-']) {
match p[i + 4..k].windows(3).position(|w| w == ['-', '-', '>']) {
Some(end) => i += 4 + end + 3,
None => {
k = i;
break;
}
}
} else {
i += 1;
}
}
if let Some(at) = p[..k].iter().rposition(|&c| c == '<') {
let body = &p[at + 1..k];
if !p[at..k].contains(&'>') {
let autolink = starts_with_chars(body, "http://") || starts_with_chars(body, "https://");
if autolink {
let ws = body.iter().position(|c| c.is_whitespace());
let bar = body.iter().position(|&c| c == '|');
if ws.is_none_or(|w| bar.is_some_and(|b| b < w)) {
k = k.min(at);
}
} else {
let tagish = p.get(at + 1).is_none_or(|&c| c.is_ascii_alphabetic() || c == '/' || c == '!');
if tagish && k - at < 80 {
k = k.min(at);
}
}
}
}
while k > 0 && matches!(p[k - 1], '*' | '_' | '~' | '`' | '\\' | ' ' | '\t') {
k -= 1;
}
while k > 0 && p[k - 1] == '\\' {
k -= 1;
}
k
}
fn link_end(p: &[char]) -> Option<usize> {
let mut j = 1;
let mut depth = 0usize;
while j < p.len() {
match p[j] {
'[' => depth += 1,
']' if depth == 0 => break,
']' => depth -= 1,
_ => {}
}
j += 1;
}
if j >= p.len() {
return None;
}
match p.get(j + 1) {
Some('(') => p[j + 2..].iter().position(|&c| c == ')').map(|c| j + 2 + c + 1),
Some(_) => Some(j + 1),
None => None,
}
}
fn starts_with_chars(chars: &[char], s: &str) -> bool {
let mut it = chars.iter();
for c in s.chars() {
match it.next() {
Some(&x) if x == c => {}
Some(_) => return false,
None => return true,
}
}
true
}
fn trim_end(p: &[char]) -> usize {
let mut k = p.len();
while k > 0 && (p[k - 1] == ' ' || p[k - 1] == '\t') {
k -= 1;
}
k
}
fn run(t: &[char], c: char) -> usize {
t.iter().take_while(|&&x| x == c).count()
}
fn push_usize(out: &mut String, mut n: usize) {
let mut buf = [0u8; 20];
let mut i = buf.len();
loop {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
if n == 0 {
break;
}
}
out.push_str(std::str::from_utf8(&buf[i..]).expect("숫자는 ASCII 다"));
}
fn fence_marker(t: &str) -> Option<(char, usize, &str)> {
for ch in ['`', '~'] {
let n = t.chars().take_while(|&c| c == ch).count();
if n >= 3 {
return Some((ch, n, t[n..].trim()));
}
}
None
}
fn is_delimiter_row(line: &str) -> bool {
let t = line.trim();
t.contains('-')
&& t.contains('|')
&& t.chars().all(|c| matches!(c, '-' | ':' | '|' | ' ' | '\t'))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum Align {
#[default]
Left,
Right,
Center,
}
#[derive(Default)]
pub(crate) struct Table {
rows: Vec<Vec<String>>,
align: Vec<Align>,
cell: String,
}
impl Table {
fn begin(&mut self, header: &str, delim: &str) -> bool {
let head = split_cells(header);
let delim = split_cells(delim);
if head.is_empty() || head.len() != delim.len() {
return false;
}
self.align = delim
.iter()
.map(|d| {
let d = d.trim();
match (d.starts_with(':'), d.ends_with(':')) {
(true, true) => Align::Center,
(false, true) => Align::Right,
_ => Align::Left,
}
})
.collect();
self.rows.clear();
self.rows.push(head);
true
}
fn push(&mut self, line: &str) {
self.rows.push(split_cells(line));
}
fn clear(&mut self) {
self.rows.clear();
self.align.clear();
}
fn render(&mut self, v: &Vocab, dialect: Dialect, repairs: &mut Repairs, out: &mut String) {
let cols = self.align.len();
let plain = Vocab::new(Channel::Plain);
let cell_vocab = if v.tables_native() { v } else { &plain };
let rows = std::mem::take(&mut self.rows);
let mut cells: Vec<Vec<String>> = Vec::with_capacity(rows.len());
let mut inline = Inline::new(dialect);
let mut chars: Vec<char> = Vec::new();
for row in &rows {
let mut line = Vec::with_capacity(cols);
for c in 0..cols {
self.cell.clear();
chars.clear();
if c + 1 == cols && row.len() > cols {
for (k, cell) in row[c..].iter().enumerate() {
if k > 0 {
chars.extend(" | ".chars());
}
chars.extend(cell.chars());
}
} else {
chars.extend(row.get(c).map(String::as_str).unwrap_or("").chars());
}
inline.render(&chars, &mut self.cell, cell_vocab);
inline.finish_block(&mut self.cell, cell_vocab);
inline.reset();
line.push(std::mem::take(&mut self.cell));
}
cells.push(line);
}
self.rows = rows;
repairs.add(inline.repairs);
if v.tables_native() {
write_gfm_table(out, &cells, &self.align);
return;
}
let mut widths = vec![0usize; cols];
for row in &cells {
for (c, cell) in row.iter().enumerate() {
widths[c] = widths[c].max(str_width(cell));
}
}
let mut body = String::new();
for (r, row) in cells.iter().enumerate() {
if r > 0 {
body.push('\n');
}
write_row(&mut body, row, &widths, &self.align);
if r == 0 {
body.push('\n');
for (c, w) in widths.iter().enumerate() {
if c > 0 {
body.push_str(" | ");
}
for _ in 0..*w {
body.push('-');
}
}
}
}
v.verbatim_open("", out);
if v.verbatim_body_newline() {
out.push('\n');
}
v.escape(&body, out);
v.verbatim_close("", out);
}
}
fn write_gfm_table(out: &mut String, cells: &[Vec<String>], align: &[Align]) {
let write_cells = |out: &mut String, row: &[String]| {
out.push('|');
for cell in row {
out.push(' ');
for c in cell.chars() {
if c == '|' {
out.push('\\');
}
out.push(c);
}
out.push_str(" |");
}
};
for (r, row) in cells.iter().enumerate() {
if r > 0 {
out.push('\n');
}
write_cells(out, row);
if r == 0 {
out.push_str("\n|");
for i in 0..row.len() {
out.push_str(match align.get(i).unwrap_or(&Align::Left) {
Align::Left => " --- |",
Align::Right => " ---: |",
Align::Center => " :---: |",
});
}
}
}
}
fn write_row(out: &mut String, row: &[String], widths: &[usize], align: &[Align]) {
let last = row.len().saturating_sub(1);
for (c, cell) in row.iter().enumerate() {
if c > 0 {
out.push_str(" | ");
}
let pad = widths[c].saturating_sub(str_width(cell));
let (before, after) = match align.get(c).copied().unwrap_or_default() {
Align::Left => (0, pad),
Align::Right => (pad, 0),
Align::Center => (pad / 2, pad - pad / 2),
};
for _ in 0..before {
out.push(' ');
}
out.push_str(cell);
if c != last {
for _ in 0..after {
out.push(' ');
}
}
}
}
fn split_cells(line: &str) -> Vec<String> {
let t = line.trim();
let t = t.strip_prefix('|').unwrap_or(t);
let t = t.strip_suffix('|').unwrap_or(t);
let mut cells = Vec::new();
let mut cur = String::new();
let mut escaped = false;
for c in t.chars() {
match c {
'\\' if !escaped => escaped = true,
'|' if !escaped => {
cells.push(cur.trim().to_string());
cur = String::new();
}
_ => {
if escaped && c != '|' {
cur.push('\\');
}
escaped = false;
cur.push(c);
}
}
}
cells.push(cur.trim().to_string());
cells
}