use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
fn strip_braces(text: &str) -> &str {
text.strip_prefix('{')
.and_then(|rest| rest.strip_suffix('}'))
.unwrap_or(text)
}
fn child_token_text(node: &SyntaxNode, kind: SyntaxKind) -> Option<String> {
node.children_with_tokens()
.filter_map(|element| element.into_token())
.find(|token| token.kind() == kind)
.map(|token| token.text().to_string())
}
pub struct MystTarget(SyntaxNode);
impl AstNode for MystTarget {
type Language = PanacheLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MYST_TARGET
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self(syntax))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl MystTarget {
pub fn label(&self) -> Option<String> {
child_token_text(&self.0, SyntaxKind::MYST_TARGET_LABEL)
}
}
pub struct MystRole(SyntaxNode);
impl AstNode for MystRole {
type Language = PanacheLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MYST_ROLE
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self(syntax))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl MystRole {
pub fn name(&self) -> Option<String> {
child_token_text(&self.0, SyntaxKind::MYST_ROLE_NAME)
.map(|text| strip_braces(&text).to_string())
}
pub fn content(&self) -> Option<String> {
self.0
.children()
.find(|node| node.kind() == SyntaxKind::MYST_ROLE_CONTENT)
.map(|node| node.text().to_string())
}
}
pub struct MystSubstitution(SyntaxNode);
impl AstNode for MystSubstitution {
type Language = PanacheLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MYST_SUBSTITUTION
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self(syntax))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl MystSubstitution {
pub fn name(&self) -> Option<String> {
child_token_text(&self.0, SyntaxKind::MYST_SUBSTITUTION_NAME)
.map(|text| text.trim().to_string())
}
}
pub struct MystDirective(SyntaxNode);
impl AstNode for MystDirective {
type Language = PanacheLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MYST_DIRECTIVE
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self(syntax))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl MystDirective {
fn open(&self) -> Option<SyntaxNode> {
self.0
.children()
.find(|node| node.kind() == SyntaxKind::MYST_DIRECTIVE_OPEN)
}
pub fn name(&self) -> Option<String> {
let open = self.open()?;
child_token_text(&open, SyntaxKind::MYST_DIRECTIVE_NAME)
.map(|text| strip_braces(&text).to_string())
}
pub fn argument(&self) -> Option<String> {
let open = self.open()?;
child_token_text(&open, SyntaxKind::MYST_DIRECTIVE_ARG).map(|text| text.trim().to_string())
}
pub fn options(&self) -> Vec<MystDirectiveOption> {
self.0
.children()
.filter_map(MystDirectiveOption::cast)
.collect()
}
pub fn body(&self) -> Option<String> {
self.0
.children()
.find(|node| node.kind() == SyntaxKind::MYST_DIRECTIVE_BODY)
.map(|node| node.text().to_string())
}
}
pub struct MystDirectiveOption(SyntaxNode);
impl AstNode for MystDirectiveOption {
type Language = PanacheLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MYST_DIRECTIVE_OPTION
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
if Self::can_cast(syntax.kind()) {
Some(Self(syntax))
} else {
None
}
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
impl MystDirectiveOption {
pub fn name(&self) -> Option<String> {
child_token_text(&self.0, SyntaxKind::MYST_DIRECTIVE_OPTION_NAME)
}
pub fn value(&self) -> Option<String> {
child_token_text(&self.0, SyntaxKind::MYST_DIRECTIVE_OPTION_VALUE)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
use crate::{Dialect, Extensions, Flavor, ParserOptions};
fn myst_options() -> ParserOptions {
ParserOptions {
flavor: Flavor::Myst,
dialect: Dialect::for_flavor(Flavor::Myst),
extensions: Extensions {
myst_substitutions: true,
..Extensions::for_flavor(Flavor::Myst)
},
..ParserOptions::default()
}
}
fn cast_first<T: AstNode<Language = PanacheLanguage>>(input: &str) -> T {
parse(input, Some(myst_options()))
.descendants()
.find_map(T::cast)
.expect("expected a MyST node")
}
#[test]
fn target_wrapper_extracts_label() {
let target: MystTarget = cast_first("(my-target)=\n\n# Heading");
assert_eq!(target.label().as_deref(), Some("my-target"));
}
#[test]
fn role_wrapper_extracts_name_and_content() {
let role: MystRole = cast_first("See {math}`a^2 + b^2`.");
assert_eq!(role.name().as_deref(), Some("math"));
assert_eq!(role.content().as_deref(), Some("a^2 + b^2"));
}
#[test]
fn directive_wrapper_extracts_name_arg_and_options() {
let directive: MystDirective =
cast_first("````{figure} img.png\n:alt: An image\n:width: 200px\n\nCaption\n````");
assert_eq!(directive.name().as_deref(), Some("figure"));
assert_eq!(directive.argument().as_deref(), Some("img.png"));
let options = directive.options();
assert_eq!(options.len(), 2);
assert_eq!(options[0].name().as_deref(), Some("alt"));
assert_eq!(options[0].value().as_deref(), Some("An image"));
assert_eq!(options[1].name().as_deref(), Some("width"));
assert_eq!(options[1].value().as_deref(), Some("200px"));
}
#[test]
fn directive_wrapper_extracts_verbatim_body() {
let directive: MystDirective =
cast_first("```{code} python\n:number-lines: 1\ndef five():\n return 5\n```");
assert_eq!(directive.name().as_deref(), Some("code"));
assert_eq!(directive.argument().as_deref(), Some("python"));
let body = directive.body().expect("verbatim body");
assert!(body.contains("def five():"));
assert!(body.contains("return 5"));
}
#[test]
fn directive_wrapper_handles_valueless_option() {
let directive: MystDirective = cast_first("```{toctree}\n:hidden:\n\nquickstart\n```");
let options = directive.options();
let hidden = options
.iter()
.find(|option| option.name().as_deref() == Some("hidden"))
.expect("hidden option");
assert_eq!(hidden.value(), None);
}
#[test]
fn substitution_wrapper_trims_name() {
let substitution: MystSubstitution = cast_first("Released {{ version }}.");
assert_eq!(substitution.name().as_deref(), Some("version"));
}
}