#![forbid(unsafe_code)]
mod block;
mod inline;
mod sink;
mod vocab;
pub mod width;
use block::Engine;
use sink::{PartsSink, StringSink};
use vocab::Vocab;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Channel {
TelegramHtml,
SlackMarkdown,
Plain,
}
impl Channel {
pub fn name(self) -> &'static str {
match self {
Channel::TelegramHtml => "telegram-html",
Channel::SlackMarkdown => "slack-markdown",
Channel::Plain => "plain",
}
}
pub fn all() -> [Channel; 3] {
[Channel::TelegramHtml, Channel::SlackMarkdown, Channel::Plain]
}
pub fn parse(name: &str) -> Option<Channel> {
Self::all().into_iter().find(|c| c.name() == name)
}
pub fn limit(self) -> usize {
match self {
Channel::TelegramHtml => 4096,
Channel::SlackMarkdown | Channel::Plain => 12_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Dialect {
#[default]
Markdown,
SlackMrkdwn,
}
impl Dialect {
pub fn name(self) -> &'static str {
match self {
Dialect::Markdown => "markdown",
Dialect::SlackMrkdwn => "slack-mrkdwn",
}
}
pub fn parse(name: &str) -> Option<Dialect> {
[Dialect::Markdown, Dialect::SlackMrkdwn].into_iter().find(|d| d.name() == name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Options {
pub from: Dialect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Repairs {
pub closed_emphasis: usize,
pub closed_fence: usize,
pub reverted_code_span: usize,
pub dropped_marker: usize,
}
impl Repairs {
pub(crate) fn add(&mut self, other: Repairs) {
self.closed_emphasis += other.closed_emphasis;
self.closed_fence += other.closed_fence;
self.reverted_code_span += other.reverted_code_span;
self.dropped_marker += other.dropped_marker;
}
pub fn any(&self) -> bool {
*self != Repairs::default()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rendered {
pub parts: Vec<String>,
pub repairs: Repairs,
}
pub struct Streamer {
engine: Engine,
buf: String,
started: bool,
}
impl Streamer {
pub fn new(channel: Channel) -> Self {
Self::with_options(channel, Options::default())
}
pub fn with_options(channel: Channel, options: Options) -> Self {
Self { engine: Engine::new(channel, options), buf: String::new(), started: false }
}
pub fn repairs(&self) -> Repairs {
self.engine.repairs()
}
fn trim_leading(&mut self, out: &mut String, from: usize) {
if self.started {
return;
}
let fresh = &out[from..];
let keep = fresh.len() - fresh.trim_start_matches('\n').len();
if keep > 0 {
out.drain(from..from + keep);
}
if out.len() > from {
self.started = true;
}
}
pub fn push(&mut self, chunk: &str) -> &str {
let mut buf = std::mem::take(&mut self.buf);
buf.clear();
self.push_into(chunk, &mut buf);
self.buf = buf;
&self.buf
}
pub fn push_into(&mut self, chunk: &str, out: &mut String) {
let from = out.len();
let mut sink = StringSink(out);
self.engine.feed(chunk, &mut sink);
self.trim_leading(out, from);
}
pub fn finish(&mut self) -> &str {
let mut buf = std::mem::take(&mut self.buf);
buf.clear();
self.finish_into(&mut buf);
self.buf = buf;
&self.buf
}
pub fn finish_into(&mut self, out: &mut String) {
let from = out.len();
let mut sink = StringSink(out);
self.engine.finish(&mut sink);
self.trim_leading(out, from);
}
pub fn close_open(&self, out: &mut String) {
self.engine.close_open(out);
}
}
pub fn render(input: &str, channel: Channel) -> Vec<String> {
render_with(input, channel, Options::default()).parts
}
pub fn render_with(input: &str, channel: Channel, options: Options) -> Rendered {
let mut engine = Engine::new(channel, options);
let mut sink = PartsSink::new(Vocab::new(channel));
engine.feed(input, &mut sink);
engine.finish(&mut sink);
Rendered { repairs: engine.repairs(), parts: sink.into_parts() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_limits_are_channel_specific() {
assert_eq!(Channel::TelegramHtml.limit(), 4096);
assert_eq!(Channel::SlackMarkdown.limit(), 12_000);
}
#[test]
fn parts_never_end_inside_a_tag() {
let mut input = String::new();
for i in 0..40 {
input.push_str(&format!(
"- `method{i}(options?: SomeLongOptionsType{i}): string` — 주소의 뒤집힌 꼴을 \
돌려준다 [src](https://example.com/owner/repo/blob/master/src/mod{i}.ts#L{i}28)\n"
));
}
let parts = render(&input, Channel::TelegramHtml);
assert!(parts.len() > 1, "한도를 넘겨서 나뉘어야 하는 입력이다");
for (i, p) in parts.iter().enumerate() {
assert_eq!(
p.matches('<').count(),
p.matches('>').count(),
"조각 {i} 가 태그 한가운데서 끊겼다: …{}",
&p[p.len().saturating_sub(40)..]
);
assert!(p.chars().count() <= Channel::TelegramHtml.limit(), "조각 {i} 가 한도를 넘었다");
}
}
}