1use std::{collections::HashSet, path::Path, rc::Rc};
8
9use gpui::{App, Global, Window};
10
11use crate::{BlockKind, Doc, render::OnJump};
12
13pub type LinkHandler = fn(url: &str, base: Option<&Path>, &mut Window, &mut App);
16
17struct Installed(LinkHandler);
18
19impl Global for Installed {}
20
21pub(crate) fn set_link_handler(cx: &mut App, handler: LinkHandler) {
23 cx.set_global(Installed(handler));
24}
25
26pub(crate) fn open(url: &str, base: Option<&Path>, window: &mut Window, cx: &mut App) {
27 match cx.try_global::<Installed>().map(|installed| installed.0) {
28 Some(handler) => handler(url, base, window, cx),
29 None => cx.open_url(url),
30 }
31}
32
33pub fn slug(text: &str) -> String {
36 text.chars()
37 .flat_map(char::to_lowercase)
38 .filter_map(|c| match c {
39 ' ' => Some('-'),
40 '-' | '_' => Some(c),
41 c if c.is_alphanumeric() => Some(c),
42 _ => None,
43 })
44 .collect()
45}
46
47pub fn heading(doc: &Doc, fragment: &str) -> Option<usize> {
51 anchors(doc)
52 .into_iter()
53 .find_map(|(anchor, block)| (anchor == fragment).then_some(block))
54}
55
56fn anchors(doc: &Doc) -> Vec<(String, usize)> {
57 let mut taken = HashSet::new();
58 doc.blocks
59 .iter()
60 .enumerate()
61 .filter_map(|(ix, block)| match &block.kind {
62 BlockKind::Heading { text, .. } => Some((slug(&text.text), ix)),
63 _ => None,
64 })
65 .map(|(base, ix)| {
66 let anchor = std::iter::once(base.clone())
67 .chain((1..).map(|n| format!("{base}-{n}")))
68 .find(|anchor| !taken.contains(anchor))
69 .expect("an unbounded run of suffixes");
70 taken.insert(anchor.clone());
71 (anchor, ix)
72 })
73 .collect()
74}
75
76#[derive(Clone)]
79pub(crate) struct Jump {
80 to: OnJump,
81 anchors: Rc<[(String, usize)]>,
82}
83
84impl Jump {
85 pub(crate) fn new(doc: &Doc, to: OnJump) -> Self {
86 Self {
87 to,
88 anchors: anchors(doc).into(),
89 }
90 }
91}
92
93pub(crate) fn follow(
96 url: &str,
97 base: Option<&Path>,
98 jump: Option<&Jump>,
99 window: &mut Window,
100 cx: &mut App,
101) {
102 let Some(fragment) = url.strip_prefix('#') else {
103 open(url, base, window, cx);
104 return;
105 };
106 if let Some(jump) = jump
107 && let Some(&(_, block)) = jump.anchors.iter().find(|(anchor, _)| anchor == fragment)
108 {
109 (jump.to)(block, window, cx);
110 }
111}