use crate::syntax::SyntaxNode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DirectiveKind {
IgnoreBoth,
IgnoreFormat,
IgnoreLint,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Directive {
Start(DirectiveKind),
End(DirectiveKind),
}
impl DirectiveKind {
pub fn affects_formatting(self) -> bool {
matches!(
self,
DirectiveKind::IgnoreBoth | DirectiveKind::IgnoreFormat
)
}
pub fn affects_linting(self) -> bool {
matches!(self, DirectiveKind::IgnoreBoth | DirectiveKind::IgnoreLint)
}
}
pub fn parse_directive(comment_text: &str) -> Option<Directive> {
let content = comment_text.trim();
if !content.starts_with("<!--") || !content.ends_with("-->") {
return None;
}
let inner = content[4..content.len() - 3].trim();
if !inner.starts_with("panache-ignore") {
return None;
}
match inner {
"panache-ignore-start" => Some(Directive::Start(DirectiveKind::IgnoreBoth)),
"panache-ignore-end" => Some(Directive::End(DirectiveKind::IgnoreBoth)),
"panache-ignore-format-start" => Some(Directive::Start(DirectiveKind::IgnoreFormat)),
"panache-ignore-format-end" => Some(Directive::End(DirectiveKind::IgnoreFormat)),
"panache-ignore-lint-start" => Some(Directive::Start(DirectiveKind::IgnoreLint)),
"panache-ignore-lint-end" => Some(Directive::End(DirectiveKind::IgnoreLint)),
_ => None, }
}
#[derive(Debug, Clone)]
pub struct DirectiveTracker {
stack: Vec<DirectiveKind>,
}
impl DirectiveTracker {
pub fn new() -> Self {
Self { stack: Vec::new() }
}
pub fn process_directive(&mut self, directive: &Directive) -> bool {
match directive {
Directive::Start(kind) => {
self.stack.push(*kind);
true
}
Directive::End(kind) => {
if let Some(top) = self.stack.last()
&& top == kind
{
self.stack.pop();
return true;
}
false
}
}
}
pub fn is_formatting_ignored(&self) -> bool {
self.stack.iter().any(|kind| kind.affects_formatting())
}
pub fn is_linting_ignored(&self) -> bool {
self.stack.iter().any(|kind| kind.affects_linting())
}
pub fn has_unclosed_regions(&self) -> bool {
!self.stack.is_empty()
}
pub fn unclosed_regions(&self) -> Vec<DirectiveKind> {
self.stack.clone()
}
pub fn reset(&mut self) {
self.stack.clear();
}
}
impl Default for DirectiveTracker {
fn default() -> Self {
Self::new()
}
}
pub fn extract_directive_from_node(node: &SyntaxNode) -> Option<Directive> {
use crate::syntax::SyntaxKind;
if node.kind() != SyntaxKind::COMMENT
&& node.kind() != SyntaxKind::HTML_BLOCK
&& node.kind() != SyntaxKind::INLINE_HTML
{
return None;
}
let text = node.text().to_string();
parse_directive(&text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_directive_ignore_both() {
assert_eq!(
parse_directive("<!-- panache-ignore-start -->"),
Some(Directive::Start(DirectiveKind::IgnoreBoth))
);
assert_eq!(
parse_directive("<!-- panache-ignore-end -->"),
Some(Directive::End(DirectiveKind::IgnoreBoth))
);
}
#[test]
fn test_parse_directive_ignore_format() {
assert_eq!(
parse_directive("<!-- panache-ignore-format-start -->"),
Some(Directive::Start(DirectiveKind::IgnoreFormat))
);
assert_eq!(
parse_directive("<!-- panache-ignore-format-end -->"),
Some(Directive::End(DirectiveKind::IgnoreFormat))
);
}
#[test]
fn test_parse_directive_ignore_lint() {
assert_eq!(
parse_directive("<!-- panache-ignore-lint-start -->"),
Some(Directive::Start(DirectiveKind::IgnoreLint))
);
assert_eq!(
parse_directive("<!-- panache-ignore-lint-end -->"),
Some(Directive::End(DirectiveKind::IgnoreLint))
);
}
#[test]
fn test_parse_directive_with_whitespace() {
assert_eq!(
parse_directive("<!-- panache-ignore-start -->"),
Some(Directive::Start(DirectiveKind::IgnoreBoth))
);
assert_eq!(
parse_directive("<!--\npanache-ignore-end\n-->"),
Some(Directive::End(DirectiveKind::IgnoreBoth))
);
}
#[test]
fn test_parse_directive_not_directive() {
assert_eq!(parse_directive("<!-- regular comment -->"), None);
assert_eq!(parse_directive("<!-- panache-something -->"), None);
assert_eq!(parse_directive("not a comment"), None);
}
#[test]
fn test_parse_directive_future_extension() {
assert_eq!(
parse_directive("<!-- panache-ignore-lint heading-hierarchy -->"),
None
);
}
#[test]
fn test_directive_kind_affects_formatting() {
assert!(DirectiveKind::IgnoreBoth.affects_formatting());
assert!(DirectiveKind::IgnoreFormat.affects_formatting());
assert!(!DirectiveKind::IgnoreLint.affects_formatting());
}
#[test]
fn test_directive_kind_affects_linting() {
assert!(DirectiveKind::IgnoreBoth.affects_linting());
assert!(!DirectiveKind::IgnoreFormat.affects_linting());
assert!(DirectiveKind::IgnoreLint.affects_linting());
}
#[test]
fn test_tracker_basic() {
let mut tracker = DirectiveTracker::new();
assert!(!tracker.is_formatting_ignored());
assert!(!tracker.is_linting_ignored());
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreBoth));
assert!(tracker.is_formatting_ignored());
assert!(tracker.is_linting_ignored());
tracker.process_directive(&Directive::End(DirectiveKind::IgnoreBoth));
assert!(!tracker.is_formatting_ignored());
assert!(!tracker.is_linting_ignored());
}
#[test]
fn test_tracker_format_only() {
let mut tracker = DirectiveTracker::new();
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreFormat));
assert!(tracker.is_formatting_ignored());
assert!(!tracker.is_linting_ignored());
}
#[test]
fn test_tracker_lint_only() {
let mut tracker = DirectiveTracker::new();
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreLint));
assert!(!tracker.is_formatting_ignored());
assert!(tracker.is_linting_ignored());
}
#[test]
fn test_tracker_mismatch() {
let mut tracker = DirectiveTracker::new();
let result = tracker.process_directive(&Directive::End(DirectiveKind::IgnoreBoth));
assert!(!result);
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreFormat));
let result = tracker.process_directive(&Directive::End(DirectiveKind::IgnoreLint));
assert!(!result);
}
#[test]
fn test_tracker_nested() {
let mut tracker = DirectiveTracker::new();
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreBoth));
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreFormat));
assert!(tracker.is_formatting_ignored());
assert!(tracker.is_linting_ignored());
tracker.process_directive(&Directive::End(DirectiveKind::IgnoreFormat));
assert!(tracker.is_formatting_ignored()); assert!(tracker.is_linting_ignored());
tracker.process_directive(&Directive::End(DirectiveKind::IgnoreBoth));
assert!(!tracker.is_formatting_ignored());
assert!(!tracker.is_linting_ignored());
}
#[test]
fn test_tracker_unclosed_regions() {
let mut tracker = DirectiveTracker::new();
assert!(!tracker.has_unclosed_regions());
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreBoth));
assert!(tracker.has_unclosed_regions());
let unclosed = tracker.unclosed_regions();
assert_eq!(unclosed.len(), 1);
assert_eq!(unclosed[0], DirectiveKind::IgnoreBoth);
tracker.process_directive(&Directive::End(DirectiveKind::IgnoreBoth));
assert!(!tracker.has_unclosed_regions());
}
#[test]
fn test_tracker_reset() {
let mut tracker = DirectiveTracker::new();
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreBoth));
tracker.process_directive(&Directive::Start(DirectiveKind::IgnoreFormat));
tracker.reset();
assert!(!tracker.has_unclosed_regions());
assert!(!tracker.is_formatting_ignored());
assert!(!tracker.is_linting_ignored());
}
}