use std::sync::LazyLock;
use memchr::memmem::Finder;
use ruff_source_file::UniversalNewlineIterator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ScriptTag {
metadata: String,
range: TextRange,
source_map: ScriptSourceMap,
}
impl ScriptTag {
pub fn metadata(&self) -> &str {
&self.metadata
}
pub fn source_map(&self) -> &ScriptSourceMap {
&self.source_map
}
pub fn parse(contents: &[u8]) -> Option<Self> {
FINDER
.find_iter(contents)
.find_map(|index| Self::parse_at(contents, index))
}
fn parse_at(contents: &[u8], index: usize) -> Option<Self> {
if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) {
return None;
}
let contents = std::str::from_utf8(contents).ok()?;
let contents = &contents[index..];
let start = TextSize::try_from(index).ok()?;
let mut lines = UniversalNewlineIterator::with_offset(contents, start);
if lines.next().is_none_or(|line| line != "# /// script") {
return None;
}
let mut metadata = String::new();
let mut source_map = ScriptSourceMap::default();
let mut closing = None;
for line in lines {
let Some(comment) = line.strip_prefix('#') else {
break;
};
let (content, indent_len) = if comment.is_empty() {
("", TextSize::ZERO)
} else if let Some(content) = comment.strip_prefix(' ') {
(content, ' '.text_len())
} else {
break;
};
if content == "///" {
closing = Some((metadata.len(), source_map.markers.len(), line.range()));
}
let prefix_length = '#'.text_len() + indent_len;
source_map.push_marker(metadata.text_len(), line.start() + prefix_length);
metadata.push_str(content);
metadata.push('\n');
}
let (metadata_end, marker_count, closing_range) = closing?;
metadata.truncate(metadata_end);
source_map.truncate(marker_count);
if metadata.is_empty() {
metadata.push('\n');
} else {
source_map.push_marker(metadata.text_len(), closing_range.start());
}
Some(Self {
metadata,
range: TextRange::new(start, closing_range.end()),
source_map,
})
}
}
impl Ranged for ScriptTag {
fn range(&self) -> TextRange {
self.range
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ScriptSourceMap {
markers: Vec<ScriptSourceMarker>,
}
impl ScriptSourceMap {
pub fn map_offset(&self, offset: TextSize) -> TextSize {
let Some(index) = self
.markers
.partition_point(|marker| marker.metadata_offset <= offset)
.checked_sub(1)
else {
return offset;
};
let marker = &self.markers[index];
marker.source_offset + (offset - marker.metadata_offset)
}
pub fn map_range(&self, range: TextRange) -> TextRange {
TextRange::new(self.map_offset(range.start()), self.map_offset(range.end()))
}
fn push_marker(&mut self, metadata_offset: TextSize, source_offset: TextSize) {
self.markers.push(ScriptSourceMarker {
metadata_offset,
source_offset,
});
}
fn truncate(&mut self, len: usize) {
self.markers.truncate(len);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ScriptSourceMarker {
metadata_offset: TextSize,
source_offset: TextSize,
}
#[cfg(test)]
mod tests {
use ruff_text_size::{Ranged, TextLen, TextRange};
use super::ScriptTag;
#[test]
fn carriage_return_line_endings() -> Result<(), &'static str> {
let tag = ScriptTag::parse(b"# /// script\r# value = true\r# ///\r")
.ok_or("Expected script metadata with carriage-return line endings")?;
assert_eq!(tag.metadata(), "value = true\n");
Ok(())
}
#[test]
fn metadata_block_range_includes_both_delimiters() -> Result<(), &'static str> {
let prefix = "#!/usr/bin/env python3\n\n";
let metadata = "# /// script\n# dependencies = []\n# ///";
let source = format!("{prefix}{metadata}\n\nprint('hello')\n");
let tag = ScriptTag::parse(source.as_bytes()).ok_or("Expected valid script metadata")?;
assert_eq!(
tag.range(),
TextRange::at(prefix.text_len(), metadata.text_len())
);
Ok(())
}
#[test]
fn metadata_range_accounts_for_unicode_crlf_and_multiline_values() -> Result<(), &'static str> {
let metadata_value = r#""""
first
last
""""#;
let source_value = r#""""
# first
#
# last
# """"#
.replace('\n', "\r\n");
let source = format!("π\r\n# /// script\r\n# value = {source_value}\r\n# ///\r\n");
let tag = ScriptTag::parse(source.as_bytes()).ok_or("Expected valid script metadata")?;
assert_eq!(tag.metadata(), format!("value = {metadata_value}\n"));
let metadata_range = TextRange::at("value = ".text_len(), metadata_value.text_len());
let source_range = TextRange::at(
"π\r\n# /// script\r\n# value = ".text_len(),
source_value.text_len(),
);
assert_eq!(tag.source_map().map_range(metadata_range), source_range);
Ok(())
}
#[test]
fn last_closing_delimiter_discards_following_comments() -> Result<(), &'static str> {
let source = r"# /// script
# first = true
# ///
# last = true
# ///
# ignored = true
";
let tag = ScriptTag::parse(source.as_bytes()).ok_or("Expected valid script metadata")?;
assert_eq!(
tag.metadata(),
r"first = true
///
last = true
"
);
let closing_start = source
.rfind("# ///")
.map(|offset| source[..offset].text_len())
.ok_or("Expected the final closing delimiter")?;
assert_eq!(
tag.source_map().map_offset(tag.metadata().text_len()),
closing_start,
);
assert_eq!(tag.end(), closing_start + "# ///".text_len());
Ok(())
}
}