use std::{iter::once, ops::Range};
use either::Either;
use itertools::Itertools;
use pulldown_cmark::{Event, Options, Parser, Tag};
use super::super::{
ChunkConfig, ChunkSizer,
splitter::{SemanticLevel, Splitter},
trim::Trim,
};
#[derive(Debug)]
pub struct MarkdownSplitter<Sizer>
where
Sizer: ChunkSizer,
{
chunk_config: ChunkConfig<Sizer>,
}
impl<Sizer> MarkdownSplitter<Sizer>
where
Sizer: ChunkSizer,
{
#[must_use]
pub fn new(chunk_config: impl Into<ChunkConfig<Sizer>>) -> Self {
Self {
chunk_config: chunk_config.into(),
}
}
pub fn chunks<'splitter, 'text: 'splitter>(
&'splitter self,
text: &'text str,
) -> impl Iterator<Item = &'text str> + 'splitter {
Splitter::<_>::chunks(self, text)
}
}
impl<Sizer> Splitter<Sizer> for MarkdownSplitter<Sizer>
where
Sizer: ChunkSizer,
{
type Level = Element;
const TRIM: Trim = Trim::PreserveIndentation;
fn chunk_config(&self) -> &ChunkConfig<Sizer> {
&self.chunk_config
}
fn parse(&self, text: &str) -> Vec<(Self::Level, Range<usize>)> {
Parser::new_ext(text, Options::all())
.into_offset_iter()
.filter_map(|(event, range)| match event {
Event::Start(
Tag::Emphasis
| Tag::Strong
| Tag::Strikethrough
| Tag::Link { .. }
| Tag::Image { .. }
| Tag::Subscript
| Tag::Superscript
| Tag::TableCell,
)
| Event::Text(_)
| Event::HardBreak
| Event::Code(_)
| Event::InlineHtml(_)
| Event::InlineMath(_)
| Event::FootnoteReference(_)
| Event::TaskListMarker(_) => Some((Element::Inline, range)),
Event::SoftBreak => Some((Element::SoftBreak, range)),
Event::Html(_)
| Event::DisplayMath(_)
| Event::Start(
Tag::Paragraph
| Tag::CodeBlock(_)
| Tag::FootnoteDefinition(_)
| Tag::MetadataBlock(_)
| Tag::TableHead
| Tag::BlockQuote(_)
| Tag::TableRow
| Tag::Item
| Tag::HtmlBlock
| Tag::List(_)
| Tag::Table(_)
| Tag::DefinitionList
| Tag::DefinitionListTitle
| Tag::DefinitionListDefinition,
) => Some((Element::Block, range)),
Event::Rule => Some((Element::Rule, range)),
Event::Start(Tag::Heading { level, .. }) => Some((Element::Heading(level.into()), range)),
Event::End(_) => None,
})
.collect()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum HeadingLevel {
H6,
H5,
H4,
H3,
H2,
H1,
}
impl From<pulldown_cmark::HeadingLevel> for HeadingLevel {
fn from(value: pulldown_cmark::HeadingLevel) -> Self {
match value {
pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
}
}
}
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum SemanticSplitPosition {
Own,
Next,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Element {
SoftBreak,
Inline,
Block,
Rule,
Heading(HeadingLevel),
}
impl Element {
fn split_position(self) -> SemanticSplitPosition {
match self {
Self::SoftBreak | Self::Block | Self::Rule | Self::Inline => SemanticSplitPosition::Own,
Self::Heading(_) => SemanticSplitPosition::Next,
}
}
fn treat_whitespace_as_previous(self) -> bool {
match self {
Self::SoftBreak | Self::Inline | Self::Rule | Self::Heading(_) => false,
Self::Block => true,
}
}
}
impl SemanticLevel for Element {
fn sections(
text: &str,
level_ranges: impl Iterator<Item = (Self, Range<usize>)>,
) -> impl Iterator<Item = (usize, &str)> {
let mut cursor = 0;
let mut final_match = false;
level_ranges
.batching(move |it| {
loop {
match it.next() {
None if final_match => return None,
None => {
final_match = true;
return text.get(cursor..).map(|t| Either::Left(once((cursor, t))));
}
Some((level, range)) => {
let offset = cursor;
match level.split_position() {
SemanticSplitPosition::Own => {
if range.start < cursor {
continue;
}
let prev_section =
text.get(cursor..range.start).expect("invalid character sequence");
if level.treat_whitespace_as_previous()
&& prev_section.chars().all(char::is_whitespace)
{
let section = text.get(cursor..range.end).expect("invalid character sequence");
cursor = range.end;
return Some(Either::Left(once((offset, section))));
}
let separator =
text.get(range.start..range.end).expect("invalid character sequence");
cursor = range.end;
return Some(Either::Right(
[(offset, prev_section), (range.start, separator)].into_iter(),
));
}
SemanticSplitPosition::Next => {
if range.start < cursor {
continue;
}
let prev_section =
text.get(cursor..range.start).expect("invalid character sequence");
cursor = range.start;
return Some(Either::Left(once((offset, prev_section))));
}
}
}
}
}
})
.flatten()
.filter(|(_, s)| !s.is_empty())
}
}