use pulldown_cmark::LinkType;
use std::borrow::Cow;
#[derive(Debug, Clone)]
pub struct LineInfo {
pub byte_offset: usize,
pub byte_len: usize,
pub indent: usize,
pub visual_indent: usize,
pub is_blank: bool,
pub in_code_block: bool,
pub in_front_matter: bool,
pub in_html_block: bool,
pub in_list_block: bool,
pub in_table_block: bool,
pub in_html_comment: bool,
pub list_item: Option<Box<ListItemInfo>>,
pub heading: Option<Box<HeadingInfo>>,
pub blockquote: Option<Box<BlockquoteInfo>>,
pub in_mkdocstrings: bool,
pub in_esm_block: bool,
pub in_code_span_continuation: bool,
pub is_horizontal_rule: bool,
pub in_math_block: bool,
pub in_pandoc_div: bool,
pub is_div_marker: bool,
pub in_jsx_expression: bool,
pub in_mdx_comment: bool,
pub in_admonition: bool,
pub in_content_tab: bool,
pub in_mkdocs_html_markdown: bool,
pub in_definition_list: bool,
pub in_obsidian_comment: bool,
pub in_pymdown_block: bool,
pub in_kramdown_extension_block: bool,
pub is_kramdown_block_ial: bool,
pub in_jsx_block: bool,
pub in_footnote_definition: bool,
pub in_myst_directive: bool,
pub is_myst_comment: bool,
}
impl LineInfo {
pub fn content<'a>(&self, source: &'a str) -> &'a str {
&source[self.byte_offset..self.byte_offset + self.byte_len]
}
#[inline]
pub fn in_mkdocs_container(&self) -> bool {
self.in_admonition || self.in_content_tab || self.in_mkdocs_html_markdown
}
#[inline]
pub fn is_valid_heading(&self) -> bool {
self.heading.as_ref().is_some_and(|h| h.is_valid)
}
#[inline]
pub fn is_paragraph_context(&self) -> bool {
!self.in_code_block
&& !self.in_front_matter
&& !self.in_html_block
&& !self.in_html_comment
&& !self.in_math_block
&& !self.is_horizontal_rule
&& !self.is_div_marker
&& !self.in_pymdown_block
&& !self.in_kramdown_extension_block
&& !self.is_kramdown_block_ial
&& !self.is_myst_comment
&& self.heading.is_none()
}
}
#[derive(Debug, Clone)]
pub struct ListItemInfo {
pub marker: String,
pub is_ordered: bool,
pub number: Option<usize>,
pub marker_column: usize,
pub content_column: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HeadingStyle {
ATX,
Setext1,
Setext2,
}
#[derive(Debug, Clone)]
pub struct ParsedLink<'a> {
pub line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub byte_offset: usize,
pub byte_end: usize,
pub text: Cow<'a, str>,
pub url: Cow<'a, str>,
pub title: Option<Cow<'a, str>>,
pub is_reference: bool,
pub reference_id: Option<Cow<'a, str>>,
pub link_type: LinkType,
}
#[derive(Debug, Clone)]
pub struct BrokenLinkInfo {
pub reference: String,
pub span: std::ops::Range<usize>,
pub link_type: LinkType,
}
#[derive(Debug, Clone)]
pub struct FootnoteRef {
pub id: String,
pub line: usize,
pub byte_offset: usize,
}
#[derive(Debug, Clone)]
pub struct ParsedImage<'a> {
pub line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub byte_offset: usize,
pub byte_end: usize,
pub alt_text: Cow<'a, str>,
pub url: Cow<'a, str>,
pub title: Option<Cow<'a, str>>,
pub is_reference: bool,
pub reference_id: Option<Cow<'a, str>>,
pub link_type: LinkType,
}
#[derive(Debug, Clone)]
pub struct ReferenceDef {
pub line: usize,
pub id: String,
pub url: String,
pub title: Option<String>,
pub byte_offset: usize,
pub byte_end: usize,
pub title_byte_start: Option<usize>,
pub title_byte_end: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct CodeSpan {
pub line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub byte_offset: usize,
pub byte_end: usize,
pub backtick_count: usize,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct MathSpan {
pub line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub byte_offset: usize,
pub byte_end: usize,
pub is_display: bool,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct HeadingInfo {
pub level: u8,
pub style: HeadingStyle,
pub marker: String,
pub marker_column: usize,
pub content_column: usize,
pub text: String,
pub custom_id: Option<String>,
pub raw_text: String,
pub has_closing_sequence: bool,
pub closing_sequence: String,
pub is_valid: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct ParsedHeading<'a> {
pub line_num: usize,
pub heading: &'a HeadingInfo,
pub line_info: &'a LineInfo,
pub blockquote_depth: usize,
}
impl ParsedHeading<'_> {
#[inline]
pub fn is_blockquote(&self) -> bool {
self.blockquote_depth > 0
}
#[inline]
pub fn is_setext(&self) -> bool {
matches!(self.heading.style, HeadingStyle::Setext1 | HeadingStyle::Setext2)
}
#[must_use]
pub fn text_byte_range(&self, source: &str) -> (usize, usize) {
let line = self.line_info.content(source);
let content_start = self.heading.content_column.min(line.len());
let relative_start = line[content_start..].find(&self.heading.text).unwrap_or(0);
let start = content_start + relative_start;
(start, (start + self.heading.text.len()).min(line.len()))
}
}
pub struct ParsedHeadingsIter<'a> {
lines: &'a [LineInfo],
blockquote_headings: &'a [Option<Box<HeadingInfo>>],
current_index: usize,
}
impl<'a> ParsedHeadingsIter<'a> {
pub(super) fn new(lines: &'a [LineInfo], blockquote_headings: &'a [Option<Box<HeadingInfo>>]) -> Self {
debug_assert_eq!(lines.len(), blockquote_headings.len());
Self {
lines,
blockquote_headings,
current_index: 0,
}
}
}
impl<'a> Iterator for ParsedHeadingsIter<'a> {
type Item = ParsedHeading<'a>;
fn next(&mut self) -> Option<Self::Item> {
while self.current_index < self.lines.len() {
let idx = self.current_index;
self.current_index += 1;
let line_info = &self.lines[idx];
let (heading, blockquote_depth) = if let Some(heading) = line_info.heading.as_deref() {
(heading, 0)
} else if let Some(heading) = self.blockquote_headings[idx].as_deref() {
(heading, line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level))
} else {
continue;
};
return Some(ParsedHeading {
line_num: idx + 1,
heading,
line_info,
blockquote_depth,
});
}
None
}
}
#[derive(Debug, Clone)]
pub struct ValidHeading<'a> {
pub line_num: usize,
pub heading: &'a HeadingInfo,
pub line_info: &'a LineInfo,
}
pub struct ValidHeadingsIter<'a> {
lines: &'a [LineInfo],
current_index: usize,
}
impl<'a> ValidHeadingsIter<'a> {
pub(super) fn new(lines: &'a [LineInfo]) -> Self {
Self {
lines,
current_index: 0,
}
}
}
impl<'a> Iterator for ValidHeadingsIter<'a> {
type Item = ValidHeading<'a>;
fn next(&mut self) -> Option<Self::Item> {
while self.current_index < self.lines.len() {
let idx = self.current_index;
self.current_index += 1;
let line_info = &self.lines[idx];
if let Some(heading) = line_info.heading.as_deref()
&& heading.is_valid
{
return Some(ValidHeading {
line_num: idx + 1, heading,
line_info,
});
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct BlockquoteInfo {
pub nesting_level: usize,
pub marker_column: usize,
pub prefix: String,
pub content: String,
pub has_multiple_spaces_after_marker: bool,
}
#[derive(Debug, Clone)]
pub struct ListBlock {
pub start_line: usize,
pub end_line: usize,
pub is_ordered: bool,
pub marker: Option<String>,
pub blockquote_prefix: String,
pub item_lines: Vec<usize>,
pub nesting_level: usize,
pub max_marker_width: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct ParsedListItem<'a> {
line_num: usize,
item: &'a ListItemInfo,
line_info: &'a LineInfo,
}
impl<'a> ParsedListItem<'a> {
pub(super) fn new(line_num: usize, item: &'a ListItemInfo, line_info: &'a LineInfo) -> Self {
Self {
line_num,
item,
line_info,
}
}
#[inline]
pub fn line_num(self) -> usize {
self.line_num
}
#[inline]
pub fn line_info(self) -> &'a LineInfo {
self.line_info
}
#[inline]
pub fn marker(self) -> &'a str {
&self.item.marker
}
#[inline]
pub fn marker_char(self) -> Option<char> {
self.item.marker.chars().next()
}
#[inline]
pub fn is_ordered(self) -> bool {
self.item.is_ordered
}
#[inline]
pub fn number(self) -> Option<usize> {
self.item.number
}
#[inline]
pub fn marker_column(self) -> usize {
self.item.marker_column
}
#[inline]
pub fn content_column(self) -> usize {
self.item.content_column
}
#[inline]
pub fn marker_byte_offset(self) -> usize {
self.line_info.byte_offset + self.item.marker_column
}
#[inline]
pub fn blockquote_depth(self) -> usize {
self.line_info.blockquote.as_ref().map_or(0, |bq| bq.nesting_level)
}
#[inline]
pub fn blockquote_prefix_len(self) -> usize {
self.line_info.blockquote.as_ref().map_or(0, |bq| bq.prefix.len())
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParsedListBlock<'a> {
block: &'a ListBlock,
lines: &'a [LineInfo],
}
impl<'a> ParsedListBlock<'a> {
pub(super) fn new(block: &'a ListBlock, lines: &'a [LineInfo]) -> Self {
Self { block, lines }
}
#[inline]
pub fn start_line(self) -> usize {
self.block.start_line
}
#[inline]
pub fn end_line(self) -> usize {
self.block.end_line
}
#[inline]
pub fn is_ordered(self) -> bool {
self.block.is_ordered
}
#[inline]
pub fn marker(self) -> Option<&'a str> {
self.block.marker.as_deref()
}
#[inline]
pub fn blockquote_prefix(self) -> &'a str {
&self.block.blockquote_prefix
}
#[inline]
pub fn nesting_level(self) -> usize {
self.block.nesting_level
}
#[inline]
pub fn max_marker_width(self) -> usize {
self.block.max_marker_width
}
pub fn items(self) -> ParsedListBlockItemsIter<'a> {
ParsedListBlockItemsIter {
item_lines: &self.block.item_lines,
lines: self.lines,
current_index: 0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParsedListBlocks<'a> {
blocks: &'a [ListBlock],
lines: &'a [LineInfo],
}
impl<'a> ParsedListBlocks<'a> {
pub(super) fn new(blocks: &'a [ListBlock], lines: &'a [LineInfo]) -> Self {
Self { blocks, lines }
}
#[inline]
pub fn is_empty(self) -> bool {
self.blocks.is_empty()
}
#[inline]
pub fn len(self) -> usize {
self.blocks.len()
}
pub fn get(self, index: usize) -> Option<ParsedListBlock<'a>> {
self.blocks
.get(index)
.map(|block| ParsedListBlock::new(block, self.lines))
}
pub fn iter(self) -> ParsedListBlocksIter<'a> {
ParsedListBlocksIter {
blocks: self.blocks.iter(),
lines: self.lines,
}
}
}
impl<'a> IntoIterator for ParsedListBlocks<'a> {
type Item = ParsedListBlock<'a>;
type IntoIter = ParsedListBlocksIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct ParsedListBlocksIter<'a> {
blocks: std::slice::Iter<'a, ListBlock>,
lines: &'a [LineInfo],
}
impl<'a> Iterator for ParsedListBlocksIter<'a> {
type Item = ParsedListBlock<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.blocks.next().map(|block| ParsedListBlock::new(block, self.lines))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.blocks.size_hint()
}
}
impl ExactSizeIterator for ParsedListBlocksIter<'_> {}
pub struct ParsedListBlockItemsIter<'a> {
item_lines: &'a [usize],
lines: &'a [LineInfo],
current_index: usize,
}
impl<'a> Iterator for ParsedListBlockItemsIter<'a> {
type Item = ParsedListItem<'a>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(&line_num) = self.item_lines.get(self.current_index) {
self.current_index += 1;
let Some(line_index) = line_num.checked_sub(1) else {
continue;
};
let Some(line_info) = self.lines.get(line_index) else {
continue;
};
if let Some(item) = line_info.list_item.as_deref() {
return Some(ParsedListItem::new(line_num, item, line_info));
}
}
None
}
}
pub struct ParsedListItemsIter<'a> {
lines: &'a [LineInfo],
current_index: usize,
}
impl<'a> ParsedListItemsIter<'a> {
pub(super) fn new(lines: &'a [LineInfo]) -> Self {
Self {
lines,
current_index: 0,
}
}
}
impl<'a> Iterator for ParsedListItemsIter<'a> {
type Item = ParsedListItem<'a>;
fn next(&mut self) -> Option<Self::Item> {
while self.current_index < self.lines.len() {
let idx = self.current_index;
self.current_index += 1;
let line_info = &self.lines[idx];
if let Some(item) = line_info.list_item.as_deref() {
return Some(ParsedListItem::new(idx + 1, item, line_info));
}
}
None
}
}
#[derive(Debug, Clone)]
pub(super) struct CommonMarkOrderedListInfo {
pub(super) start_value: u64,
pub(super) item_lines: Vec<usize>,
}
#[derive(Debug, Clone, Copy)]
pub struct CommonMarkOrderedList<'a> {
list: &'a CommonMarkOrderedListInfo,
lines: &'a [LineInfo],
}
impl<'a> CommonMarkOrderedList<'a> {
pub(super) fn new(list: &'a CommonMarkOrderedListInfo, lines: &'a [LineInfo]) -> Self {
Self { list, lines }
}
#[inline]
pub fn start_value(self) -> u64 {
self.list.start_value
}
pub fn items(self) -> CommonMarkOrderedListItemsIter<'a> {
CommonMarkOrderedListItemsIter {
item_lines: &self.list.item_lines,
lines: self.lines,
current_index: 0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CommonMarkOrderedLists<'a> {
lists: &'a [CommonMarkOrderedListInfo],
lines: &'a [LineInfo],
}
impl<'a> CommonMarkOrderedLists<'a> {
pub(super) fn new(lists: &'a [CommonMarkOrderedListInfo], lines: &'a [LineInfo]) -> Self {
Self { lists, lines }
}
#[inline]
pub fn is_empty(self) -> bool {
self.lists.is_empty()
}
#[inline]
pub fn len(self) -> usize {
self.lists.len()
}
pub fn get(self, index: usize) -> Option<CommonMarkOrderedList<'a>> {
self.lists
.get(index)
.map(|list| CommonMarkOrderedList::new(list, self.lines))
}
pub fn iter(self) -> CommonMarkOrderedListsIter<'a> {
CommonMarkOrderedListsIter {
lists: self.lists.iter(),
lines: self.lines,
}
}
}
impl<'a> IntoIterator for CommonMarkOrderedLists<'a> {
type Item = CommonMarkOrderedList<'a>;
type IntoIter = CommonMarkOrderedListsIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct CommonMarkOrderedListsIter<'a> {
lists: std::slice::Iter<'a, CommonMarkOrderedListInfo>,
lines: &'a [LineInfo],
}
impl<'a> Iterator for CommonMarkOrderedListsIter<'a> {
type Item = CommonMarkOrderedList<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.lists
.next()
.map(|list| CommonMarkOrderedList::new(list, self.lines))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.lists.size_hint()
}
}
impl ExactSizeIterator for CommonMarkOrderedListsIter<'_> {}
pub struct CommonMarkOrderedListItemsIter<'a> {
item_lines: &'a [usize],
lines: &'a [LineInfo],
current_index: usize,
}
impl<'a> Iterator for CommonMarkOrderedListItemsIter<'a> {
type Item = ParsedListItem<'a>;
fn next(&mut self) -> Option<Self::Item> {
while let Some(&line_num) = self.item_lines.get(self.current_index) {
self.current_index += 1;
let Some(line_index) = line_num.checked_sub(1) else {
continue;
};
let Some(line_info) = self.lines.get(line_index) else {
continue;
};
let Some(item) = line_info.list_item.as_deref() else {
continue;
};
if item.is_ordered {
return Some(ParsedListItem::new(line_num, item, line_info));
}
}
None
}
}
#[derive(Debug, Clone, Default)]
pub struct CharFrequency {
pub hash_count: usize,
pub asterisk_count: usize,
pub underscore_count: usize,
pub hyphen_count: usize,
pub plus_count: usize,
pub gt_count: usize,
pub pipe_count: usize,
pub bracket_count: usize,
pub backtick_count: usize,
pub lt_count: usize,
pub exclamation_count: usize,
pub newline_count: usize,
}
#[derive(Debug, Clone)]
pub struct HtmlTag {
pub line: usize,
pub start_col: usize,
pub end_col: usize,
pub byte_offset: usize,
pub byte_end: usize,
pub tag_name: String,
pub is_closing: bool,
pub is_self_closing: bool,
}
#[derive(Debug, Clone)]
pub struct EmphasisSpan {
pub line: usize,
pub start_col: usize,
pub end_col: usize,
pub byte_offset: usize,
pub byte_end: usize,
pub marker: char,
pub is_strong: bool,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct BareUrl {
pub line: usize,
pub start_col: usize,
pub end_col: usize,
pub byte_offset: usize,
pub byte_end: usize,
pub url: String,
}
#[derive(Debug, Clone)]
pub struct LazyContLine {
pub line_num: usize,
pub expected_indent: usize,
pub current_indent: usize,
pub blockquote_level: usize,
}
pub fn is_horizontal_rule_line(line: &str) -> bool {
let leading_spaces = line.len() - line.trim_start_matches(' ').len();
if leading_spaces > 3 || line.starts_with('\t') {
return false;
}
is_horizontal_rule_content(line.trim())
}
pub fn is_horizontal_rule_content(trimmed: &str) -> bool {
if trimmed.len() < 3 {
return false;
}
let mut chars = trimmed.chars();
let Some(first_char @ ('-' | '*' | '_')) = chars.next() else {
return false;
};
let mut count = 1; for ch in chars {
if ch == first_char {
count += 1;
} else if ch != ' ' && ch != '\t' {
return false;
}
}
count >= 3
}