#![doc = env!("CARGO_PKG_DESCRIPTION")]
#![doc = ""]
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct Readme;
use std::{
borrow::Cow,
io::{BufRead, Cursor, Write},
ops::Deref,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Profile<'a> {
pub narrative: Cow<'a, str>,
pub ignore_line_postfix: Cow<'a, str>,
pub ignored_block_start: Cow<'a, str>,
pub ignored_block_end: Cow<'a, str>,
pub indent: Cow<'a, str>,
}
impl Default for Profile<'_> {
fn default() -> Self {
SLASH.clone()
}
}
impl<'a> Profile<'a> {
pub fn from_comment(comment: &str) -> Self {
Self {
narrative: Cow::Owned(format!("{comment}:")),
ignore_line_postfix: Cow::Owned(format!("{comment}")),
ignored_block_start: Cow::Owned(format!("{comment}{{")),
ignored_block_end: Cow::Owned(format!("{comment}}}")),
indent: Cow::Owned(format!("{comment}>")),
}
}
pub fn with_narrative(mut self, s: impl Into<Cow<'a, str>>) -> Self {
self.narrative = s.into();
self
}
pub fn with_ignore_line_postfix(mut self, s: impl Into<Cow<'a, str>>) -> Self {
self.ignore_line_postfix = s.into();
self
}
pub fn with_ignored_block_start(mut self, s: impl Into<Cow<'a, str>>) -> Self {
self.ignored_block_start = s.into();
self
}
pub fn with_ignored_block_end(mut self, s: impl Into<Cow<'a, str>>) -> Self {
self.ignored_block_end = s.into();
self
}
pub fn with_indent(mut self, s: impl Into<Cow<'a, str>>) -> Self {
self.indent = s.into();
self
}
}
pub const SLASH: Profile = Profile {
narrative: Cow::Borrowed("//:"),
ignore_line_postfix: Cow::Borrowed("//"),
ignored_block_start: Cow::Borrowed("//{"),
ignored_block_end: Cow::Borrowed("//}"),
indent: Cow::Borrowed("//>"),
};
pub const HASH: Profile = Profile {
narrative: Cow::Borrowed("#:"),
ignore_line_postfix: Cow::Borrowed("#"),
ignored_block_start: Cow::Borrowed("#{"),
ignored_block_end: Cow::Borrowed("#}"),
indent: Cow::Borrowed("#>"),
};
pub const DASH: Profile = Profile {
narrative: Cow::Borrowed("--:"),
ignore_line_postfix: Cow::Borrowed("--"),
ignored_block_start: Cow::Borrowed("--{"),
ignored_block_end: Cow::Borrowed("--}"),
indent: Cow::Borrowed("-->"),
};
pub const SEMICOLON: Profile = Profile {
narrative: Cow::Borrowed(";:"),
ignore_line_postfix: Cow::Borrowed(";"),
ignored_block_start: Cow::Borrowed(";{"),
ignored_block_end: Cow::Borrowed(";}"),
indent: Cow::Borrowed(";>"),
};
#[derive(Debug)]
pub struct ConvertError(String);
impl std::fmt::Display for ConvertError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rsticle Error: {}", self.0)
}
}
impl std::error::Error for ConvertError {}
impl From<std::io::Error> for ConvertError {
fn from(value: std::io::Error) -> Self {
Self(value.to_string())
}
}
impl From<std::fmt::Error> for ConvertError {
fn from(value: std::fmt::Error) -> Self {
Self(value.to_string())
}
}
type Indent = usize;
#[derive(PartialEq, Eq)]
enum State {
IgnoringBlock,
Code(Option<Indent>),
Narrative,
}
pub fn convert(
profile: &Profile<'_>,
input: impl BufRead,
output: &mut impl Write,
) -> Result<(), ConvertError> {
let mut state = State::Code(None);
let mut add_indent = 0;
let mut line_buf = Vec::<String>::with_capacity(128);
let narrative_prefix = profile.narrative.len();
let narrative = profile.narrative.deref();
let ignore_line_postfix = profile.ignore_line_postfix.deref();
let ignored_block_start = profile.ignored_block_start.deref();
let ignored_block_end = profile.ignored_block_end.deref();
let indent = profile.indent.deref();
for (line_no, line) in input.lines().enumerate() {
let line = line?;
let trimmed = line.trim();
match &mut state {
State::IgnoringBlock => {
if trimmed.starts_with(ignored_block_end) {
state = State::Code(None);
}
}
State::Code(block_indent) => {
if trimmed.starts_with(narrative) {
flush_dedented(&mut line_buf, output, *block_indent, add_indent)?;
output_line(output, trimmed[narrative_prefix..].trim())?;
state = State::Narrative;
} else if trimmed.starts_with(ignored_block_start) {
flush_dedented(&mut line_buf, output, *block_indent, add_indent)?;
state = State::IgnoringBlock
} else if trimmed.starts_with(indent) {
flush_dedented(&mut line_buf, output, *block_indent, add_indent)?;
add_indent = trimmed[indent.len()..].trim().chars().count();
} else if !trimmed.ends_with(ignore_line_postfix) {
if let Some(line_indent) = buffer_line(&mut line_buf, line) {
let min_indent = block_indent
.map(|it| it.min(line_indent))
.unwrap_or(line_indent);
block_indent.replace(min_indent);
}
}
}
State::Narrative => {
if trimmed.starts_with(ignored_block_start) {
state = State::IgnoringBlock;
} else if trimmed.starts_with(narrative) {
let trimmed_without_prefix = &trimmed[narrative_prefix..];
if trimmed_without_prefix.is_empty() {
output_line(output, "")?;
} else if trimmed_without_prefix.starts_with(" ") {
output_line(output, &trimmed_without_prefix[1..])?;
} else {
let (line_example, dots) = trimmed
.get(0..narrative_prefix + 7)
.map(|shortened| (shortened, "..."))
.unwrap_or_else(|| (trimmed, ""));
return Err(ConvertError(format!(
"Line {} ({line_example}{dots}): Space required after {narrative}",
line_no + 1,
)));
}
} else if trimmed.starts_with(indent) {
add_indent = trimmed[indent.len()..].trim().chars().count();
} else {
let indent = if trimmed.ends_with(ignore_line_postfix) {
None
} else {
buffer_line(&mut line_buf, line)
};
state = State::Code(indent);
}
}
}
}
if let State::Code(block_indent) = state {
flush_dedented(&mut line_buf, output, block_indent, add_indent)?;
}
Ok(())
}
pub fn convert_str(profile: &Profile, input: &str) -> Result<String, ConvertError> {
let input = Cursor::new(input);
let mut output = Vec::new();
convert(&profile, input, &mut output)?;
String::from_utf8(output).map_err(|e| ConvertError(format!("Invalid UTF-8: {e}")))
}
fn buffer_line(buf: &mut Vec<String>, line: String) -> Option<Indent> {
let indent = line.len() - line.trim_start().len();
let not_just_whitespace = line.trim().len() > 0;
buf.push(line);
not_just_whitespace.then_some(indent)
}
fn output_line(output: &mut impl Write, line: &str) -> Result<(), ConvertError> {
output.write_all(line.as_bytes())?;
output.write_all(b"\n")?;
Ok(())
}
fn flush_dedented(
buf: &mut Vec<String>,
output: &mut impl std::io::Write,
dedent: Option<Indent>,
add: Indent,
) -> Result<(), ConvertError> {
let dedent = dedent.unwrap_or(0);
let added_spaces = vec![b' '; add];
for line in buf.drain(..) {
if line.len() > dedent && line.trim().len() != 0 {
let content = &line[dedent..];
output.write_all(&added_spaces)?;
output.write_all(content.as_bytes())?;
}
output.write_all(b"\n")?;
}
Ok(())
}
#[cfg(test)]
mod tests {}