#[derive(Debug, Clone)]
pub struct LanguageInfo {
pub id: String,
pub name: String,
pub extensions: Vec<String>,
pub mime_types: Vec<String>,
pub comment_tokens: CommentTokens,
}
impl LanguageInfo {
#[must_use]
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
extensions: Vec::new(),
mime_types: Vec::new(),
comment_tokens: CommentTokens::default(),
}
}
#[must_use]
pub fn with_extensions<I, S>(mut self, extensions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.extensions = extensions.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_mime_types<I, S>(mut self, mime_types: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.mime_types = mime_types.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_comments(mut self, tokens: CommentTokens) -> Self {
self.comment_tokens = tokens;
self
}
#[must_use]
pub fn matches_extension(&self, ext: &str) -> bool {
self.extensions.iter().any(|e| e.eq_ignore_ascii_case(ext))
}
#[must_use]
pub fn matches_mime(&self, mime: &str) -> bool {
self.mime_types.iter().any(|m| m.eq_ignore_ascii_case(mime))
}
#[must_use]
pub const fn has_extensions(&self) -> bool {
!self.extensions.is_empty()
}
#[must_use]
pub const fn has_comments(&self) -> bool {
self.comment_tokens.has_any()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CommentTokens {
pub line: Option<String>,
pub block_start: Option<String>,
pub block_end: Option<String>,
}
impl CommentTokens {
#[must_use]
pub fn line_only(prefix: impl Into<String>) -> Self {
Self {
line: Some(prefix.into()),
block_start: None,
block_end: None,
}
}
#[must_use]
pub fn with_block(
line: impl Into<String>,
block_start: impl Into<String>,
block_end: impl Into<String>,
) -> Self {
Self {
line: Some(line.into()),
block_start: Some(block_start.into()),
block_end: Some(block_end.into()),
}
}
#[must_use]
pub fn block_only(start: impl Into<String>, end: impl Into<String>) -> Self {
Self {
line: None,
block_start: Some(start.into()),
block_end: Some(end.into()),
}
}
#[must_use]
pub const fn has_line_comment(&self) -> bool {
self.line.is_some()
}
#[must_use]
pub const fn has_block_comment(&self) -> bool {
self.block_start.is_some() && self.block_end.is_some()
}
#[must_use]
pub const fn has_any(&self) -> bool {
self.line.is_some() || (self.block_start.is_some() && self.block_end.is_some())
}
}
pub trait LanguageRegistry: Send + Sync {
fn detect_from_path(&self, path: &str) -> Option<String>;
fn detect_from_mime(&self, mime: &str) -> Option<String>;
fn get_info(&self, language_id: &str) -> Option<&LanguageInfo>;
fn is_registered(&self, language_id: &str) -> bool;
fn language_ids(&self) -> Vec<String>;
fn len(&self) -> usize {
self.language_ids().len()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct DefaultLanguageRegistry {
languages: Vec<LanguageInfo>,
}
impl DefaultLanguageRegistry {
#[must_use]
pub const fn new(languages: Vec<LanguageInfo>) -> Self {
Self { languages }
}
}
impl LanguageRegistry for DefaultLanguageRegistry {
fn detect_from_path(&self, path: &str) -> Option<String> {
let ext = std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())?;
self.languages
.iter()
.find(|info| info.matches_extension(ext))
.map(|info| info.id.clone())
}
fn detect_from_mime(&self, mime: &str) -> Option<String> {
self.languages
.iter()
.find(|info| info.matches_mime(mime))
.map(|info| info.id.clone())
}
fn get_info(&self, language_id: &str) -> Option<&LanguageInfo> {
self.languages.iter().find(|info| info.id == language_id)
}
fn is_registered(&self, language_id: &str) -> bool {
self.languages.iter().any(|info| info.id == language_id)
}
fn language_ids(&self) -> Vec<String> {
self.languages.iter().map(|info| info.id.clone()).collect()
}
}
impl std::fmt::Debug for DefaultLanguageRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DefaultLanguageRegistry")
.field("languages", &self.language_ids())
.finish()
}
}
#[cfg(test)]
#[path = "registry_tests.rs"]
mod tests;