1use crate::{
2 lsp_types::{Location, Position, Range},
3 text::{LineIndex, PositionEncoding},
4 Label,
5};
6
7const ENCODING: PositionEncoding = PositionEncoding::Utf16;
13
14pub mod fs;
15pub mod ns;
17pub mod token;
18pub mod triple;
19
20pub use rdf_parsers::{spanned, Spanned};
21
22pub fn range_to_range(range: &std::ops::Range<usize>, rope: &LineIndex) -> Option<Range> {
35 let start = offset_to_position(range.start, rope)?;
36 let end = offset_to_position(range.end, rope)?;
37 Range::new(start, end).into()
38}
39
40pub fn lsp_range_to_range(
41 range: &crate::lsp_types::Range,
42 rope: &LineIndex,
43) -> Option<std::ops::Range<usize>> {
44 let start = position_to_offset(range.start, rope)?;
45 let end = position_to_offset(range.end, rope)?;
46 Some(start..end)
47}
48
49pub fn offset_to_position(offset: usize, rope: &LineIndex) -> Option<Position> {
50 rope.byte_to_position(offset, ENCODING)
51}
52pub fn position_to_offset(position: Position, rope: &LineIndex) -> Option<usize> {
53 rope.position_to_byte(position, ENCODING)
54}
55pub fn offsets_to_range(start: usize, end: usize, rope: &LineIndex) -> Option<Range> {
56 let start = offset_to_position(start, rope)?;
57 let end = offset_to_position(end, rope)?;
58 Some(Range { start, end })
59}
60
61pub fn token_to_location(
62 token: &std::ops::Range<usize>,
63 label: &Label,
64 rope: &LineIndex,
65) -> Option<Location> {
66 let range = range_to_range(token, rope)?;
67 Some(Location {
68 range,
69 uri: label.0.clone(),
70 })
71}
72
73pub fn resolve_iri(base: &str, relative: &str) -> String {
80 if looks_absolute(relative) {
82 return relative.to_string();
83 }
84 if relative.starts_with("//") {
86 let scheme = base.split("://").next().unwrap_or("http");
87 return format!("{}:{}", scheme, relative);
88 }
89 if relative.starts_with('/') {
91 let authority_end = base
92 .find("://")
93 .and_then(|i| base[i + 3..].find('/').map(|j| i + 3 + j))
94 .unwrap_or(base.len());
95 return format!("{}{}", &base[..authority_end], relative);
96 }
97 if relative.is_empty() || relative.starts_with('#') {
99 let base_no_frag = base.split('#').next().unwrap_or(base);
100 return format!("{}{}", base_no_frag, relative);
101 }
102 let base_dir = match base.rfind('/') {
104 Some(i) => &base[..=i],
105 None => "",
106 };
107 let merged = format!("{}{}", base_dir, relative);
108 normalize_path_segments(&merged)
109}
110
111fn looks_absolute(s: &str) -> bool {
114 let bytes = s.as_bytes();
115 if bytes.is_empty() || !bytes[0].is_ascii_alphabetic() {
116 return false;
117 }
118 for (i, &b) in bytes.iter().enumerate() {
119 if b == b':' {
120 return i > 0;
121 }
122 if !matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'-' | b'.') {
123 return false;
124 }
125 }
126 false
127}
128
129fn normalize_path_segments(path: &str) -> String {
132 let (prefix, rest) = if let Some(i) = path.find("://") {
134 let after = &path[i + 3..];
135 let slash = after.find('/').map(|j| i + 3 + j).unwrap_or(path.len());
136 (&path[..slash], &path[slash..])
137 } else {
138 ("", path)
139 };
140
141 let mut stack: Vec<&str> = Vec::new();
142 for seg in rest.split('/') {
143 match seg {
144 "." => {}
145 ".." => {
146 stack.pop();
147 }
148 s => stack.push(s),
149 }
150 }
151 let trailing = if rest.ends_with("/..") || rest.ends_with("/.") {
153 "/"
154 } else {
155 ""
156 };
157 format!("{}/{}{}", prefix, stack.join("/"), trailing)
158}