use super::text_style::{Color, TextStyle, UndefinedPaletteColorError};
use crate::theme::{ColorPalette, raw::RawColor};
use comrak::nodes::AlertType;
use std::{fmt, iter, path::PathBuf, str::FromStr};
use unicode_width::UnicodeWidthStr;
#[derive(Clone, Debug)]
pub(crate) enum MarkdownElement {
FrontMatter(String),
SetexHeading { text: Vec<Line<RawColor>> },
Heading { level: u8, text: Line<RawColor> },
Paragraph(Vec<Line<RawColor>>),
Image { path: PathBuf, title: String, source_position: SourcePosition },
List(Vec<ListItem>),
Snippet {
info: String,
code: String,
source_position: SourcePosition,
},
Table(Table),
ThematicBreak,
Comment { comment: String, source_position: SourcePosition },
BlockQuote(Vec<Line<RawColor>>),
Alert {
alert_type: AlertType,
title: Option<String>,
lines: Vec<Line<RawColor>>,
},
Footnote(Line<RawColor>),
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SourcePosition {
pub(crate) start: LineColumn,
}
impl fmt::Display for SourcePosition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.start.line, self.start.column)
}
}
impl From<comrak::nodes::Sourcepos> for SourcePosition {
fn from(position: comrak::nodes::Sourcepos) -> Self {
Self { start: position.start.into() }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct LineColumn {
pub(crate) line: usize,
pub(crate) column: usize,
}
impl From<comrak::nodes::LineColumn> for LineColumn {
fn from(position: comrak::nodes::LineColumn) -> Self {
Self { line: position.line, column: position.column }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Line<C = Color>(pub(crate) Vec<Text<C>>);
impl<C> Default for Line<C> {
fn default() -> Self {
Self(vec![])
}
}
impl<C> Line<C> {
pub(crate) fn width(&self) -> usize {
self.0.iter().map(|text| text.content.width()).sum()
}
}
impl Line<Color> {
pub(crate) fn apply_style(&mut self, style: &TextStyle) {
for text in &mut self.0 {
text.style.merge(style);
}
}
}
impl Line<RawColor> {
pub(crate) fn resolve(self, palette: &ColorPalette) -> Result<Line<Color>, UndefinedPaletteColorError> {
let mut output = Vec::with_capacity(self.0.len());
for text in self.0 {
let style = text.style.resolve(palette)?;
output.push(Text::new(text.content, style));
}
Ok(Line(output))
}
}
impl<C, T: Into<Text<C>>> From<T> for Line<C> {
fn from(text: T) -> Self {
Self(vec![text.into()])
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Text<C = Color> {
pub(crate) content: String,
pub(crate) style: TextStyle<C>,
}
impl<C> Default for Text<C> {
fn default() -> Self {
Self { content: Default::default(), style: TextStyle::default() }
}
}
impl<C> Text<C> {
pub(crate) fn new<S: Into<String>>(content: S, style: TextStyle<C>) -> Self {
Self { content: content.into(), style }
}
pub(crate) fn width(&self) -> usize {
self.content.width()
}
}
impl<C> From<String> for Text<C> {
fn from(text: String) -> Self {
Self { content: text, style: TextStyle::default() }
}
}
impl<C> From<&str> for Text<C> {
fn from(text: &str) -> Self {
Self { content: text.into(), style: TextStyle::default() }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ListItem {
pub(crate) depth: u8,
pub(crate) contents: Line<RawColor>,
pub(crate) item_type: ListItemType,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ListItemType {
Unordered,
OrderedParens(usize),
OrderedPeriod(usize),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Table {
pub(crate) header: TableRow,
pub(crate) rows: Vec<TableRow>,
}
impl Table {
pub(crate) fn columns(&self) -> usize {
self.header.0.len()
}
pub(crate) fn iter_column(&self, column: usize) -> impl Iterator<Item = &Line<RawColor>> {
let header_element = &self.header.0[column];
let row_elements = self.rows.iter().map(move |row| &row.0[column]);
iter::once(header_element).chain(row_elements)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TableRow(pub(crate) Vec<Line<RawColor>>);
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Percent(pub(crate) u8);
impl Percent {
pub(crate) fn as_ratio(&self) -> f64 {
self.0 as f64 / 100.0
}
}
impl FromStr for Percent {
type Err = PercentParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let (prefix, suffix) = input.split_once('%').ok_or(PercentParseError::Unit)?;
let value: u8 = prefix.parse().map_err(|_| PercentParseError::Value)?;
if !(1..=100).contains(&value) {
return Err(PercentParseError::Value);
}
if !suffix.is_empty() {
return Err(PercentParseError::Trailer(suffix.into()));
}
Ok(Percent(value))
}
}
#[derive(thiserror::Error, Debug)]
pub enum PercentParseError {
#[error("value must be a number between 1-100")]
Value,
#[error("no unit provided")]
Unit,
#[error("unexpected: '{0}'")]
Trailer(String),
}