use crate::editor::footnote_edit;
use crate::editor::link::LinkTarget;
use crate::editor::EditorState;
use super::coord::rendered_line_at_row;
const BACK_LINK_GLYPH: char = '↩';
pub fn footnote_at_offset(source: &str, byte: usize) -> Option<LinkTarget> {
let byte = byte.min(source.len());
let line_start = source[..byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_end = source[byte..]
.find('\n')
.map(|i| byte + i)
.unwrap_or(source.len());
let line = &source[line_start..line_end];
let col = byte - line_start;
footnote_edit::scan(line).into_iter().find_map(|s| {
let span_end = if s.is_definition { s.end } else { s.end - 1 };
if col >= s.start && col <= span_end {
Some(if s.is_definition {
LinkTarget::FootnoteBack(s.label)
} else {
LinkTarget::Footnote(s.label)
})
} else {
None
}
})
}
pub(super) fn back_link_glyph_at_click(
state: &EditorState,
col: u16,
row: u16,
) -> Option<LinkTarget> {
let (line, _) = rendered_line_at_row(state, row as usize)?;
let total: usize = line.spans.iter().map(|s| s.content.chars().count()).sum();
let glyph_col = total.checked_sub(1)?;
let col = col as usize;
if col != glyph_col && col != glyph_col.saturating_sub(1) {
return None;
}
if line.spans.iter().flat_map(|s| s.content.chars()).last() != Some(BACK_LINK_GLYPH) {
return None;
}
let (line_idx, _) = state.rendered_line_at_visual_row(
state.scroll.saturating_add(row as usize),
state.viewport_width,
);
let block_byte = state
.parsed
.source_map
.original_byte_for_rendered_line(line_idx)?;
let range = state
.parsed
.source_map
.original_range_for_byte(block_byte)?;
let source = state.buffer.contents();
let block_text = source.get(range.start..range.end.min(source.len()))?;
let label = footnote_edit::scan(block_text)
.into_iter()
.find(|s| s.is_definition)
.map(|s| s.label)?;
Some(LinkTarget::FootnoteBack(label))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reference_classifies_as_footnote() {
let src = "See note.[^1] end\n";
let byte = src.find("[^1]").unwrap() + 1; assert_eq!(
footnote_at_offset(src, byte),
Some(LinkTarget::Footnote("1".into()))
);
}
#[test]
fn definition_marker_classifies_as_back_link() {
let src = "[^1]: the note text\n";
assert_eq!(
footnote_at_offset(src, 0),
Some(LinkTarget::FootnoteBack("1".into()))
);
}
#[test]
fn named_label_supported() {
let src = "ref[^note] here\n";
let byte = src.find("[^note]").unwrap() + 2;
assert_eq!(
footnote_at_offset(src, byte),
Some(LinkTarget::Footnote("note".into()))
);
}
#[test]
fn reference_inside_definition_body_resolves_to_that_reference() {
let src = "[^1]: see [^2] also\n";
let byte = src.find("[^2]").unwrap() + 1;
assert_eq!(
footnote_at_offset(src, byte),
Some(LinkTarget::Footnote("2".into()))
);
}
#[test]
fn body_text_is_not_a_footnote() {
let src = "[^1]: the note text\n";
let byte = src.find("note").unwrap();
assert_eq!(footnote_at_offset(src, byte), None);
}
#[test]
fn plain_brackets_are_not_footnotes() {
let src = "an [array] index\n";
let byte = src.find("array").unwrap();
assert_eq!(footnote_at_offset(src, byte), None);
}
#[test]
fn escaped_reference_is_not_a_footnote() {
let src = r"an \[^1] escaped marker";
let byte = src.find("[^1]").unwrap() + 1;
assert_eq!(footnote_at_offset(src, byte), None);
}
}