feat: introduce Token struct

This commit is contained in:
PoiScript 2023-11-20 14:56:38 +08:00
parent 9004de9930
commit 6c27a9257f
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
23 changed files with 586 additions and 262 deletions

View file

@ -1,6 +1,6 @@
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxToken};
use crate::syntax::SyntaxKind;
use super::AffiliatedKeyword;
use super::{filter_token, AffiliatedKeyword, Token};
impl AffiliatedKeyword {
///
@ -8,13 +8,16 @@ impl AffiliatedKeyword {
/// use orgize::{Org, ast::AffiliatedKeyword};
///
/// let keyword = Org::parse("#+CAPTION: VALUE\nabc").first_node::<AffiliatedKeyword>().unwrap();
/// assert_eq!(keyword.key().unwrap().text(), "CAPTION");
/// assert_eq!(keyword.key(), "CAPTION");
/// ```
pub fn key(&self) -> Option<SyntaxToken> {
self.syntax.children_with_tokens().find_map(|it| match it {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::TEXT => Some(t),
_ => None,
})
pub fn key(&self) -> Token {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::TEXT))
.unwrap_or_else(|| {
debug_assert!(false, "keyword must contains TEXT");
Token::default()
})
}
///
@ -24,17 +27,14 @@ impl AffiliatedKeyword {
/// let keyword = Org::parse("#+CAPTION: VALUE\nabc").first_node::<AffiliatedKeyword>().unwrap();
/// assert!(keyword.optional().is_none());
/// let keyword = Org::parse("#+CAPTION[OPTIONAL]: VALUE\nabc").first_node::<AffiliatedKeyword>().unwrap();
/// assert_eq!(keyword.optional().unwrap().text(), "OPTIONAL");
/// assert_eq!(keyword.optional().unwrap(), "OPTIONAL");
/// ```
pub fn optional(&self) -> Option<SyntaxToken> {
pub fn optional(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.skip_while(|it| it.kind() != SyntaxKind::L_BRACKET)
.nth(1)
.and_then(|it| match it {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::TEXT => Some(t),
_ => None,
})
.and_then(filter_token(SyntaxKind::TEXT))
}
///
@ -42,17 +42,14 @@ impl AffiliatedKeyword {
/// use orgize::{Org, ast::AffiliatedKeyword};
///
/// let keyword = Org::parse("#+CAPTION: VALUE\nabc").first_node::<AffiliatedKeyword>().unwrap();
/// assert_eq!(keyword.value().unwrap().text(), " VALUE");
/// assert_eq!(keyword.value().unwrap(), " VALUE");
/// let keyword = Org::parse("#+CAPTION[OPTIONAL]:VALUE\nabc").first_node::<AffiliatedKeyword>().unwrap();
/// assert_eq!(keyword.value().unwrap().text(), "VALUE");
/// assert_eq!(keyword.value().unwrap(), "VALUE");
/// ```
pub fn value(&self) -> Option<SyntaxToken> {
pub fn value(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.filter_map(|it| match it {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::TEXT => Some(t),
_ => None,
})
.filter_map(filter_token(SyntaxKind::TEXT))
.last()
}
}

58
src/ast/clock.rs Normal file
View file

@ -0,0 +1,58 @@
use rowan::ast::support;
use crate::{ast::Token, SyntaxKind};
use super::{Clock, Timestamp};
impl Clock {
pub fn value(&self) -> Option<Timestamp> {
support::child(&self.syntax)
}
/// ```rust
/// use orgize::{Org, ast::Clock};
///
/// let clock = Org::parse("CLOCK: [2003-09-16 Tue 09:39]").first_node::<Clock>().unwrap();
/// assert!(clock.duration().is_none());
/// let clock = Org::parse("CLOCK: [2003-09-16 Tue 09:39] =>12:00").first_node::<Clock>().unwrap();
/// assert_eq!(clock.duration().unwrap(), "12:00");
///
/// ```
pub fn duration(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.skip_while(|t| t.kind() != SyntaxKind::DOUBLE_ARROW)
.skip(1)
.find(|t| t.kind() != SyntaxKind::WHITESPACE)
.map(|e| {
debug_assert!(e.kind() == SyntaxKind::TEXT);
Token(e.into_token())
})
}
/// ```rust
/// use orgize::{Org, ast::Clock};
///
/// let clock = Org::parse("CLOCK: [2003-09-16 Tue 09:39]").first_node::<Clock>().unwrap();
/// assert!(!clock.is_closed());
/// let clock = Org::parse("CLOCK: [2003-09-16 Tue 09:39] =>12:00").first_node::<Clock>().unwrap();
/// assert!(clock.is_closed());
/// ```
pub fn is_closed(&self) -> bool {
self.syntax
.children_with_tokens()
.any(|t| t.kind() == SyntaxKind::DOUBLE_ARROW)
}
/// ```rust
/// use orgize::{Org, ast::Clock};
///
/// let clock = Org::parse("CLOCK: [2003-09-16 Tue 09:39]").first_node::<Clock>().unwrap();
/// assert!(clock.is_running());
/// let clock = Org::parse("CLOCK: [2003-09-16 Tue 09:39] =>12:00").first_node::<Clock>().unwrap();
/// assert!(!clock.is_running());
/// ```
pub fn is_running(&self) -> bool {
!self.is_closed()
}
}

View file

@ -1,7 +1,7 @@
use std::collections::HashMap;
use super::{filter_token, SyntaxKind::*};
use crate::{ast::PropertyDrawer, syntax::SyntaxToken};
use super::{filter_token, SyntaxKind, Token};
use crate::ast::PropertyDrawer;
impl PropertyDrawer {
/// ```rust
@ -11,12 +11,12 @@ impl PropertyDrawer {
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
/// assert_eq!(drawer.iter().count(), 2);
/// ```
pub fn iter(&self) -> impl Iterator<Item = (SyntaxToken, SyntaxToken)> {
pub fn iter(&self) -> impl Iterator<Item = (Token, Token)> {
self.node_properties().filter_map(|property| {
let mut texts = property
.syntax
.children_with_tokens()
.filter_map(filter_token(TEXT));
.filter_map(filter_token(SyntaxKind::TEXT));
Some((texts.next()?, texts.next()?))
})
@ -27,12 +27,11 @@ impl PropertyDrawer {
///
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
/// assert_eq!(drawer.get("CUSTOM_ID").unwrap().text(), "someid");
/// assert_eq!(drawer.get("ID").unwrap().text(), "id");
/// assert_eq!(drawer.get("CUSTOM_ID").unwrap(), "someid");
/// assert_eq!(drawer.get("ID").unwrap(), "id");
/// ```
pub fn get(&self, key: &str) -> Option<SyntaxToken> {
self.iter()
.find_map(|(k, v)| (k.text() == key).then_some(v))
pub fn get(&self, key: &str) -> Option<Token> {
self.iter().find_map(|(k, v)| (k == key).then_some(v))
}
/// ```rust
@ -44,10 +43,8 @@ impl PropertyDrawer {
/// assert_eq!(map.len(), 1);
/// assert_eq!(map.get("CUSTOM_ID").unwrap(), "id");
/// ```
pub fn to_hash_map(&self) -> HashMap<String, String> {
self.iter()
.map(|(k, v)| (k.text().into(), v.text().into()))
.collect()
pub fn to_hash_map(&self) -> HashMap<Token, Token> {
self.iter().collect()
}
#[cfg(feature = "indexmap")]
@ -57,11 +54,11 @@ impl PropertyDrawer {
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
/// let map = drawer.to_index_map();
/// assert_eq!(map.get_index(1).unwrap(), (&"ID".to_string(), &"id".to_string()));
/// let item1 = map.get_index(1).unwrap();
/// assert_eq!(item1.0, "ID");
/// assert_eq!(item1.1, "id");
/// ```
pub fn to_index_map(&self) -> indexmap::IndexMap<String, String> {
self.iter()
.map(|(k, v)| (k.text().into(), v.text().into()))
.collect()
pub fn to_index_map(&self) -> indexmap::IndexMap<Token, Token> {
self.iter().collect()
}
}

View file

@ -8,9 +8,8 @@ impl Entity {
.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::TEXT))?;
let token = token.text();
ENTITIES.iter().find(|i| i.0 == token)
ENTITIES.iter().find(|i| i.0 == token.as_ref())
}
/// Entity name
@ -24,10 +23,13 @@ impl Entity {
/// assert_eq!(e.name(), " ");
/// ```
pub fn name(&self) -> &str {
self.entity().map(|e| e.0).unwrap_or_else(|| {
debug_assert!(false);
""
})
self.entity().map_or_else(
|| {
debug_assert!(false);
""
},
|e| e.0,
)
}
/// Entity LaTeX representation
@ -39,10 +41,13 @@ impl Entity {
/// assert_eq!(e.latex(), "\\textperiodcentered{}");
/// ```
pub fn latex(&self) -> &str {
self.entity().map(|e| e.1).unwrap_or_else(|| {
debug_assert!(false);
""
})
self.entity().map_or_else(
|| {
debug_assert!(false);
""
},
|e| e.1,
)
}
/// Whether entity needs to be in math mode
@ -56,10 +61,13 @@ impl Entity {
/// assert!(e.is_latex_math());
/// ```
pub fn is_latex_math(&self) -> bool {
self.entity().map(|e| e.2).unwrap_or_else(|| {
debug_assert!(false);
false
})
self.entity().map_or_else(
|| {
debug_assert!(false);
false
},
|e| e.2,
)
}
/// Entity HTML representation
@ -71,10 +79,13 @@ impl Entity {
/// assert_eq!(e.html(), "&sect;");
/// ```
pub fn html(&self) -> &str {
self.entity().map(|e| e.3).unwrap_or_else(|| {
debug_assert!(false);
""
})
self.entity().map_or_else(
|| {
debug_assert!(false);
""
},
|e| e.3,
)
}
/// Entity ASCII representation
@ -86,10 +97,13 @@ impl Entity {
/// assert_eq!(e.ascii(), "section");
/// ```
pub fn ascii(&self) -> &str {
self.entity().map(|e| e.4).unwrap_or_else(|| {
debug_assert!(false);
""
})
self.entity().map_or_else(
|| {
debug_assert!(false);
""
},
|e| e.4,
)
}
/// Entity Latin1 encoding representation
@ -103,10 +117,13 @@ impl Entity {
/// assert_eq!(e.latin1(), ">");
/// ```
pub fn latin1(&self) -> &str {
self.entity().map(|e| e.5).unwrap_or_else(|| {
debug_assert!(false);
""
})
self.entity().map_or_else(
|| {
debug_assert!(false);
""
},
|e| e.5,
)
}
/// Entity UTF-8 encoding representation
@ -120,10 +137,13 @@ impl Entity {
/// assert_eq!(e.utf8(), "");
/// ```
pub fn utf8(&self) -> &str {
self.entity().map(|e| e.6).unwrap_or_else(|| {
debug_assert!(false);
""
})
self.entity().map_or_else(
|| {
debug_assert!(false);
""
},
|e| e.6,
)
}
/// Entity contains optional brackets

View file

@ -180,7 +180,6 @@ const nodes = [
{
struct: "Link",
kind: ["LINK"],
token: [["path", "LINK_PATH"]],
},
{
struct: "Cookie",
@ -198,14 +197,9 @@ const nodes = [
struct: "Macros",
kind: ["MACROS"],
},
{
struct: "MacrosArgument",
kind: ["MACROS_ARGUMENT"],
},
{
struct: "Snippet",
kind: ["SNIPPET"],
token: [["name", "TEXT"]],
},
{
struct: "Target",
@ -276,10 +270,10 @@ use rowan::ast::{support, AstChildren, AstNode};
use crate::syntax::{OrgLanguage, SyntaxKind, SyntaxKind::*, SyntaxNode, SyntaxToken};
fn affiliated_keyword(node: &SyntaxNode, filter: impl Fn(&str) -> bool) -> Option<AffiliatedKeyword> {
node.children()
.take_while(|n| n.kind() == SyntaxKind::AFFILIATED_KEYWORD)
.filter_map(AffiliatedKeyword::cast)
.find(|k| matches!(k.key(), Some(k) if filter(k.text())))
node.children()
.take_while(|n| n.kind() == SyntaxKind::AFFILIATED_KEYWORD)
.filter_map(AffiliatedKeyword::cast)
.find(|k| filter(&k.key()))
}
`;
@ -308,10 +302,10 @@ impl ${node.struct} {
}
`;
for (const [method, kind] of node.token || []) {
content += ` pub fn ${method}(&self) -> Option<SyntaxToken> { support::token(&self.syntax, ${kind}) }\n`;
content += ` pub fn ${method}(&self) -> Option<super::Token> { super::token(&self.syntax, ${kind}) }\n`;
}
for (const [method, kind] of node.last_token || []) {
content += ` pub fn ${method}(&self) -> Option<SyntaxToken> { super::last_token(&self.syntax, ${kind}) }\n`;
content += ` pub fn ${method}(&self) -> Option<super::Token> { super::last_token(&self.syntax, ${kind}) }\n`;
}
for (const [method, kind] of node.parent || []) {
content += ` pub fn ${method}(&self) -> Option<${kind}> { self.syntax.parent().and_then(${kind}::cast) }\n`;

View file

@ -12,7 +12,7 @@ fn affiliated_keyword(
node.children()
.take_while(|n| n.kind() == SyntaxKind::AFFILIATED_KEYWORD)
.filter_map(AffiliatedKeyword::cast)
.find(|k| matches!(k.key(), Some(k) if filter(k.text())))
.find(|k| filter(&k.key()))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -717,8 +717,8 @@ impl Comment {
pub fn end(&self) -> u32 {
self.syntax.text_range().end().into()
}
pub fn text(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TEXT)
pub fn text(&self) -> Option<super::Token> {
super::token(&self.syntax, TEXT)
}
pub fn post_blank(&self) -> usize {
super::blank_lines(&self.syntax)
@ -796,8 +796,8 @@ impl FixedWidth {
pub fn end(&self) -> u32 {
self.syntax.text_range().end().into()
}
pub fn text(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TEXT)
pub fn text(&self) -> Option<super::Token> {
super::token(&self.syntax, TEXT)
}
pub fn post_blank(&self) -> usize {
super::blank_lines(&self.syntax)
@ -1257,9 +1257,6 @@ impl Link {
pub fn end(&self) -> u32 {
self.syntax.text_range().end().into()
}
pub fn path(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, LINK_PATH)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -1362,31 +1359,6 @@ impl Macros {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MacrosArgument {
pub(crate) syntax: SyntaxNode,
}
impl AstNode for MacrosArgument {
type Language = OrgLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == MACROS_ARGUMENT
}
fn cast(node: SyntaxNode) -> Option<MacrosArgument> {
Self::can_cast(node.kind()).then(|| MacrosArgument { syntax: node })
}
fn syntax(&self) -> &SyntaxNode {
&self.syntax
}
}
impl MacrosArgument {
pub fn begin(&self) -> u32 {
self.syntax.text_range().start().into()
}
pub fn end(&self) -> u32 {
self.syntax.text_range().end().into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Snippet {
pub(crate) syntax: SyntaxNode,
@ -1410,9 +1382,6 @@ impl Snippet {
pub fn end(&self) -> u32 {
self.syntax.text_range().end().into()
}
pub fn name(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TEXT)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -1588,8 +1557,8 @@ impl Code {
pub fn end(&self) -> u32 {
self.syntax.text_range().end().into()
}
pub fn text(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TEXT)
pub fn text(&self) -> Option<super::Token> {
super::token(&self.syntax, TEXT)
}
}
@ -1616,34 +1585,34 @@ impl Timestamp {
pub fn end(&self) -> u32 {
self.syntax.text_range().end().into()
}
pub fn year_start(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TIMESTAMP_YEAR)
pub fn year_start(&self) -> Option<super::Token> {
super::token(&self.syntax, TIMESTAMP_YEAR)
}
pub fn month_start(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TIMESTAMP_MONTH)
pub fn month_start(&self) -> Option<super::Token> {
super::token(&self.syntax, TIMESTAMP_MONTH)
}
pub fn day_start(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TIMESTAMP_DAY)
pub fn day_start(&self) -> Option<super::Token> {
super::token(&self.syntax, TIMESTAMP_DAY)
}
pub fn hour_start(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TIMESTAMP_HOUR)
pub fn hour_start(&self) -> Option<super::Token> {
super::token(&self.syntax, TIMESTAMP_HOUR)
}
pub fn minute_start(&self) -> Option<SyntaxToken> {
support::token(&self.syntax, TIMESTAMP_MINUTE)
pub fn minute_start(&self) -> Option<super::Token> {
super::token(&self.syntax, TIMESTAMP_MINUTE)
}
pub fn year_end(&self) -> Option<SyntaxToken> {
pub fn year_end(&self) -> Option<super::Token> {
super::last_token(&self.syntax, TIMESTAMP_YEAR)
}
pub fn month_end(&self) -> Option<SyntaxToken> {
pub fn month_end(&self) -> Option<super::Token> {
super::last_token(&self.syntax, TIMESTAMP_MONTH)
}
pub fn day_end(&self) -> Option<SyntaxToken> {
pub fn day_end(&self) -> Option<super::Token> {
super::last_token(&self.syntax, TIMESTAMP_DAY)
}
pub fn hour_end(&self) -> Option<SyntaxToken> {
pub fn hour_end(&self) -> Option<super::Token> {
super::last_token(&self.syntax, TIMESTAMP_HOUR)
}
pub fn minute_end(&self) -> Option<SyntaxToken> {
pub fn minute_end(&self) -> Option<super::Token> {
super::last_token(&self.syntax, TIMESTAMP_MINUTE)
}
}

View file

@ -1,11 +1,8 @@
use rowan::NodeOrToken;
use crate::{
syntax::{SyntaxKind, SyntaxToken},
SyntaxElement,
};
use crate::{syntax::SyntaxKind, SyntaxElement};
use super::{filter_token, Headline, Timestamp};
use super::{filter_token, Headline, Timestamp, Token};
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum TodoType {
@ -28,20 +25,22 @@ impl Headline {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::HEADLINE_STARS))
.map(|stars| stars.text().len())
.unwrap_or_else(|| {
debug_assert!(false, "headline must contains starts token");
0
})
.map_or_else(
|| {
debug_assert!(false, "headline must contains starts token");
0
},
|stars| stars.len(),
)
}
/// ```rust
/// use orgize::{Org, ast::Headline};
///
/// let hdl = Org::parse("* TODO a").first_node::<Headline>().unwrap();
/// assert_eq!(hdl.todo_keyword().unwrap().text(), "TODO");
/// assert_eq!(hdl.todo_keyword().unwrap(), "TODO");
/// ```
pub fn todo_keyword(&self) -> Option<SyntaxToken> {
pub fn todo_keyword(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.find_map(|elem| match elem {
@ -49,7 +48,7 @@ impl Headline {
if tk.kind() == SyntaxKind::HEADLINE_KEYWORD_TODO
|| tk.kind() == SyntaxKind::HEADLINE_KEYWORD_DONE =>
{
Some(tk)
Some(Token(Some(tk)))
}
_ => None,
})
@ -131,7 +130,7 @@ impl Headline {
/// assert!(!hdl.is_archived());
/// ```
pub fn is_archived(&self) -> bool {
self.tags().any(|t| t.text() == "ARCHIVE")
self.tags().any(|t| t == "ARCHIVE")
}
/// Returns this headline's closed timestamp, or `None` if not set.
@ -165,7 +164,7 @@ impl Headline {
/// assert_eq!(tags_vec("* TODO :tag: :a2%:"), vec!["tag".to_string(), "a2%".to_string()]);
/// assert_eq!(tags_vec("* title :tag:a2%:"), vec!["tag".to_string(), "a2%".to_string()]);
/// ```
pub fn tags(&self) -> impl Iterator<Item = SyntaxToken> {
pub fn tags(&self) -> impl Iterator<Item = Token> {
self.syntax
.children()
.find(|n| n.kind() == SyntaxKind::HEADLINE_TAGS)
@ -180,13 +179,13 @@ impl Headline {
/// use orgize::{Org, ast::Headline};
///
/// let hdl = Org::parse("* [#A]").first_node::<Headline>().unwrap();
/// assert_eq!(hdl.priority().unwrap().text(), "A");
/// assert_eq!(hdl.priority().unwrap(), "A");
/// let hdl = Org::parse("** DONE [#B]::").first_node::<Headline>().unwrap();
/// assert_eq!(hdl.priority().unwrap().text(), "B");
/// assert_eq!(hdl.priority().unwrap(), "B");
/// let hdl = Org::parse("* [#破]").first_node::<Headline>().unwrap();
/// assert_eq!(hdl.priority().unwrap().text(), "破");
/// assert_eq!(hdl.priority().unwrap(), "破");
/// ```
pub fn priority(&self) -> Option<SyntaxToken> {
pub fn priority(&self) -> Option<Token> {
self.syntax
.children()
.find(|n| n.kind() == SyntaxKind::HEADLINE_PRIORITY)

View file

@ -1,6 +1,6 @@
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxToken};
use crate::syntax::SyntaxKind;
use super::InlineCall;
use super::{filter_token, InlineCall, Token};
impl InlineCall {
///
@ -8,15 +8,78 @@ impl InlineCall {
/// use orgize::{Org, ast::InlineCall};
///
/// let call = Org::parse("call_square(4)").first_node::<InlineCall>().unwrap();
/// assert_eq!(call.call().unwrap().text(), "square");
/// assert_eq!(call.call(), "square");
/// ```
pub fn call(&self) -> Option<SyntaxToken> {
pub fn call(&self) -> Token {
self.syntax
.children_with_tokens()
.filter_map(|it| match it {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::TEXT => Some(t),
_ => None,
})
.filter_map(filter_token(SyntaxKind::TEXT))
.nth(1)
.unwrap_or_else(|| {
debug_assert!(false, "inline call must contains two TEXT");
Token::default()
})
}
///
/// ```rust
/// use orgize::{Org, ast::InlineCall};
///
/// let call = Org::parse("call_square[:results output](4)").first_node::<InlineCall>().unwrap();
/// assert_eq!(call.inside_header().unwrap(), ":results output");
/// ```
pub fn inside_header(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.skip_while(|e| e.kind() != SyntaxKind::L_BRACKET)
.nth(1)
.map(|e| {
debug_assert!(e.kind() == SyntaxKind::TEXT);
Token(e.into_token())
})
}
///
/// ```rust
/// use orgize::{Org, ast::InlineCall};
///
/// let call = Org::parse("call_square(4)").first_node::<InlineCall>().unwrap();
/// assert_eq!(call.arguments(), "4");
/// ```
pub fn arguments(&self) -> Token {
self.syntax
.children_with_tokens()
.skip_while(|e| e.kind() != SyntaxKind::L_PARENS)
.nth(1)
.map_or_else(
|| {
debug_assert!(false);
Token::default()
},
|e| {
debug_assert!(e.kind() == SyntaxKind::TEXT);
Token(e.into_token())
},
)
}
///
/// ```rust
/// use orgize::{Org, ast::InlineCall};
///
/// let call = Org::parse("call_square[:results output](4)[:results html]").first_node::<InlineCall>().unwrap();
/// assert_eq!(call.end_header().unwrap(), ":results html");
/// ```
pub fn end_header(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.skip_while(|e| e.kind() != SyntaxKind::L_BRACKET)
.skip(1)
.skip_while(|e| e.kind() != SyntaxKind::L_BRACKET)
.nth(1)
.map(|e| {
debug_assert!(e.kind() == SyntaxKind::TEXT);
Token(e.into_token())
})
}
}

68
src/ast/inline_src.rs Normal file
View file

@ -0,0 +1,68 @@
use crate::SyntaxKind;
use super::{filter_token, InlineSrc, Token};
impl InlineSrc {
/// Language of the code
///
/// ```rust
/// use orgize::{Org, ast::InlineSrc};
///
/// let s = Org::parse("src_C{int a = 0;}").first_node::<InlineSrc>().unwrap();
/// assert_eq!(s.language(), "C");
/// let s = Org::parse("src_xml[:exports code]{<tag>text</tag>}").first_node::<InlineSrc>().unwrap();
/// assert_eq!(s.language(), "xml");
/// ```
pub fn language(&self) -> Token {
self.syntax
.children_with_tokens()
.nth(1)
.and_then(filter_token(SyntaxKind::TEXT))
.unwrap_or_else(|| {
debug_assert!(false, "inline src must contains TEXT");
Token::default()
})
}
/// Optional header arguments
///
/// ```rust
/// use orgize::{Org, ast::InlineSrc};
///
/// let s = Org::parse("src_C{int a = 0;}").first_node::<InlineSrc>().unwrap();
/// assert!(s.parameters().is_none());
/// let s = Org::parse("src_xml[:exports code]{<tag>text</tag>}").first_node::<InlineSrc>().unwrap();
/// assert_eq!(s.parameters().unwrap(), ":exports code");
/// ```
pub fn parameters(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.skip_while(|n| n.kind() != SyntaxKind::L_BRACKET)
.nth(1)
.map(|n| {
debug_assert!(n.kind() == SyntaxKind::TEXT);
Token(n.into_token())
})
}
/// Source code
///
/// ```rust
/// use orgize::{Org, ast::InlineSrc};
///
/// let s = Org::parse("src_C{int a = 0;}").first_node::<InlineSrc>().unwrap();
/// assert_eq!(s.value(), "int a = 0;");
/// let s = Org::parse("src_xml[:exports code]{<tag>text</tag>}").first_node::<InlineSrc>().unwrap();
/// assert_eq!(s.value(), "<tag>text</tag>");
/// ```
pub fn value(&self) -> Token {
self.syntax
.children_with_tokens()
.filter_map(filter_token(SyntaxKind::TEXT))
.last()
.unwrap_or_else(|| {
debug_assert!(false, "inline src must contains TEXT");
Token::default()
})
}
}

View file

@ -1,9 +1,31 @@
use rowan::ast::{support, AstNode};
use super::{AffiliatedKeyword, Link, Paragraph};
use super::{AffiliatedKeyword, Link, Paragraph, Token};
use crate::syntax::SyntaxKind;
impl Link {
/// Returns link destination
///
/// ```rust
/// use orgize::{Org, ast::Link};
///
/// let link = Org::parse("[[#id]]").first_node::<Link>().unwrap();
/// assert_eq!(link.path(), "#id");
/// let link = Org::parse("[[https://google.com]]").first_node::<Link>().unwrap();
/// assert_eq!(link.path(), "https://google.com");
/// let link = Org::parse("[[https://google.com][Google]]").first_node::<Link>().unwrap();
/// assert_eq!(link.path(), "https://google.com");
/// ```
pub fn path(&self) -> Token {
support::token(&self.syntax, SyntaxKind::LINK_PATH).map_or_else(
|| {
debug_assert!(false, "link must contains LINK_PATH");
Token::default()
},
|e| Token(Some(e)),
)
}
/// Returns `true` if link contains description
///
/// ```rust
@ -35,10 +57,9 @@ impl Link {
".ppm", ".webp", ".avif", ".svg",
];
self.path()
.map(|path| IMAGE_SUFFIX.iter().any(|e| path.text().ends_with(e)))
.unwrap_or_default()
&& !self.has_description()
let path = self.path();
IMAGE_SUFFIX.iter().any(|e| path.ends_with(e)) && !self.has_description()
}
/// Returns caption keyword in this link
@ -47,7 +68,7 @@ impl Link {
/// use orgize::{Org, ast::Link};
///
/// let link = Org::parse("#+CAPTION: image link\n[[file:/home/dominik/images/jupiter.jpg]]").first_node::<Link>().unwrap();
/// assert_eq!(link.caption().unwrap().value().unwrap().text(), " image link");
/// assert_eq!(link.caption().unwrap().value().unwrap(), " image link");
/// ```
pub fn caption(&self) -> Option<AffiliatedKeyword> {
// TODO: support other element type

View file

@ -1,5 +1,5 @@
use super::{filter_token, List, ListItem};
use crate::{syntax::SyntaxKind, SyntaxElement, SyntaxToken};
use super::{filter_token, List, ListItem, Token};
use crate::{syntax::SyntaxKind, SyntaxElement};
impl List {
/// Returns `true` if this list is an ordered link
@ -17,11 +17,13 @@ impl List {
/// assert!(list.is_ordered());
/// ```
pub fn is_ordered(&self) -> bool {
self.items()
.next()
.and_then(|item| item.bullet())
.map(|bullet| bullet.text().starts_with(|c: char| c.is_ascii_digit()))
.unwrap_or_default()
self.items().next().map_or_else(
|| {
debug_assert!(false, "list muts contains LIST_ITEM");
false
},
|item| item.bullet().starts_with(|c: char| c.is_ascii_digit()),
)
}
/// Returns `true` if this list contains a TAG
@ -35,14 +37,17 @@ impl List {
/// assert!(!list.is_descriptive());
/// ```
pub fn is_descriptive(&self) -> bool {
self.items()
.next()
.map(|item| {
self.items().next().map_or_else(
|| {
debug_assert!(false, "list must contains LIST_ITEM");
false
},
|item| {
item.syntax
.children()
.any(|it| it.kind() == SyntaxKind::LIST_ITEM_TAG)
})
.unwrap_or_default()
},
)
}
}
@ -59,38 +64,44 @@ impl ListItem {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::LIST_ITEM_INDENT))
.map(|t| t.text().len())
.unwrap_or_else(|| {
debug_assert!(false, "list must contains indent token");
0
})
.map_or_else(
|| {
debug_assert!(false, "list item must contains LIST_ITEM_INDENT");
0
},
|t| t.len(),
)
}
/// ```rust
/// use orgize::{Org, ast::ListItem};
///
/// let item = Org::parse("- some tag").first_node::<ListItem>().unwrap();
/// assert_eq!(item.bullet().unwrap().text(), "- ");
/// assert_eq!(item.bullet(), "- ");
/// let item = Org::parse("2. [X] item 2").first_node::<ListItem>().unwrap();
/// assert_eq!(item.bullet().unwrap().text(), "2. ");
/// assert_eq!(item.bullet(), "2. ");
/// ```
pub fn bullet(&self) -> Option<SyntaxToken> {
pub fn bullet(&self) -> Token {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::LIST_ITEM_BULLET))
.unwrap_or_else(|| {
debug_assert!(false, "list item must contains LIST_ITEM_BULLET");
Token::default()
})
}
/// ```rust
/// use orgize::{Org, ast::ListItem};
///
/// let item = Org::parse("- [-] item 1").first_node::<ListItem>().unwrap();
/// assert_eq!(item.checkbox().unwrap().text(), "-");
/// assert_eq!(item.checkbox().unwrap(), "-");
/// let item = Org::parse("2. [X] item 2").first_node::<ListItem>().unwrap();
/// assert_eq!(item.checkbox().unwrap().text(), "X");
/// assert_eq!(item.checkbox().unwrap(), "X");
/// let item = Org::parse("3) [ ] item 3").first_node::<ListItem>().unwrap();
/// assert_eq!(item.checkbox().unwrap().text(), " ");
/// assert_eq!(item.checkbox().unwrap(), " ");
/// ```
pub fn checkbox(&self) -> Option<SyntaxToken> {
pub fn checkbox(&self) -> Option<Token> {
self.syntax
.children()
.find(|n| n.kind() == SyntaxKind::LIST_ITEM_CHECK_BOX)
@ -100,7 +111,7 @@ impl ListItem {
})
}
pub fn counter(&self) -> Option<SyntaxToken> {
pub fn counter(&self) -> Option<Token> {
self.syntax
.children()
.find(|n| n.kind() == SyntaxKind::LIST_ITEM_COUNTER)

38
src/ast/macros.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::SyntaxKind;
use super::{filter_token, Macros, Token};
impl Macros {
/// ```rust
/// use orgize::{Org, ast::Macros};
///
/// let m = Org::parse("{{{title}}}").first_node::<Macros>().unwrap();
/// assert_eq!(m.key(), "title");
/// let m = Org::parse("{{{two_arg_macro(1, 2)}}}").first_node::<Macros>().unwrap();
/// assert_eq!(m.key(), "two_arg_macro");
/// ```
pub fn key(&self) -> Token {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::TEXT))
.unwrap_or_else(|| {
debug_assert!(false, "macros must contains TEXT");
Token::default()
})
}
/// ```rust
/// use orgize::{Org, ast::Macros};
///
/// let m = Org::parse("{{{title}}}").first_node::<Macros>().unwrap();
/// assert!(m.args().is_none());
/// let m = Org::parse("{{{two_arg_macro(1, 2)}}}").first_node::<Macros>().unwrap();
/// assert_eq!(m.args().unwrap(), "1, 2");
/// ```
pub fn args(&self) -> Option<Token> {
self.syntax
.children_with_tokens()
.filter_map(filter_token(SyntaxKind::TEXT))
.nth(1)
}
}

View file

@ -3,24 +3,37 @@ mod generated;
mod affiliated_keyword;
mod clock;
mod drawer;
mod entity;
mod headline;
mod inline_call;
mod inline_src;
mod link;
mod list;
mod macros;
mod planning;
mod snippet;
mod table;
mod timestamp;
use std::{
borrow::{Borrow, Cow},
fmt::Debug,
hash::Hash,
ops::Deref,
};
pub use generated::*;
pub use headline::*;
pub use rowan::ast::support::*;
pub use timestamp::*;
use crate::syntax::{SyntaxKind, SyntaxNode};
use rowan::{ast::AstNode, Language, NodeOrToken};
use crate::{
syntax::{SyntaxKind, SyntaxNode},
SyntaxToken,
};
use rowan::{ast::AstNode, NodeOrToken};
pub fn blank_lines(parent: &SyntaxNode) -> usize {
parent
@ -33,22 +46,94 @@ pub fn last_child<N: AstNode>(parent: &rowan::SyntaxNode<N::Language>) -> Option
parent.children().filter_map(N::cast).last()
}
pub fn last_token<L: Language>(
parent: &rowan::SyntaxNode<L>,
kind: L::Kind,
) -> Option<rowan::SyntaxToken<L>> {
pub fn last_token(parent: &SyntaxNode, kind: SyntaxKind) -> Option<Token> {
parent
.children_with_tokens()
.filter_map(filter_token(kind))
.last()
}
pub fn filter_token<L: Language>(
kind: L::Kind,
) -> impl Fn(NodeOrToken<rowan::SyntaxNode<L>, rowan::SyntaxToken<L>>) -> Option<rowan::SyntaxToken<L>>
{
pub fn token(parent: &SyntaxNode, kind: SyntaxKind) -> Option<Token> {
rowan::ast::support::token(parent, kind).map(|t| Token(Some(t)))
}
pub fn filter_token(
kind: SyntaxKind,
) -> impl Fn(NodeOrToken<SyntaxNode, SyntaxToken>) -> Option<Token> {
move |elem| match elem {
NodeOrToken::Token(tk) if tk.kind() == kind => Some(tk),
NodeOrToken::Token(tk) if tk.kind() == kind => Some(Token(Some(tk))),
_ => None,
}
}
/// A simple wrapper of `Option<SyntaxToken>`
///
/// It acts like a `token.text()` when inner is `Some(token)`, and an empty string when `None`.
#[derive(Default, Eq)]
pub struct Token(pub(crate) Option<SyntaxToken>);
impl AsRef<str> for Token {
fn as_ref(&self) -> &str {
match &self.0 {
Some(t) => t.text(),
None => "",
}
}
}
impl Borrow<str> for Token {
fn borrow(&self) -> &str {
self.as_ref()
}
}
impl Debug for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_ref().fmt(f)
}
}
impl<'a> PartialEq<&'a str> for Token {
fn eq(&self, other: &&'a str) -> bool {
self.as_ref() == *other
}
}
impl PartialEq<String> for Token {
fn eq(&self, other: &String) -> bool {
self.as_ref() == other
}
}
impl PartialEq<Token> for Token {
fn eq(&self, other: &Token) -> bool {
self.as_ref() == other.as_ref()
}
}
impl Hash for Token {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_ref().hash(state)
}
}
impl<'a> PartialEq<Cow<'a, str>> for Token {
fn eq(&self, other: &Cow<'a, str>) -> bool {
self.as_ref() == other
}
}
impl PartialEq<str> for Token {
fn eq(&self, other: &str) -> bool {
self.as_ref() == other
}
}
impl Deref for Token {
type Target = str;
#[inline]
fn deref(&self) -> &str {
self.as_ref()
}
}

View file

@ -15,7 +15,7 @@ impl Planning {
/// .unwrap()
/// .deadline()
/// .unwrap();
/// assert_eq!(s.day_start().unwrap().text(), "08");
/// assert_eq!(s.day_start().unwrap(), "08");
/// ```
pub fn deadline(&self) -> Option<Timestamp> {
self.syntax
@ -35,7 +35,7 @@ impl Planning {
/// .unwrap()
/// .scheduled()
/// .unwrap();
/// assert_eq!(s.year_start().unwrap().text(), "2019");
/// assert_eq!(s.year_start().unwrap(), "2019");
/// ```
pub fn scheduled(&self) -> Option<Timestamp> {
self.syntax
@ -55,7 +55,7 @@ impl Planning {
/// .unwrap()
/// .closed()
/// .unwrap();
/// assert_eq!(s.month_start().unwrap().text(), "04");
/// assert_eq!(s.month_start().unwrap(), "04");
/// ```
pub fn closed(&self) -> Option<Timestamp> {
self.syntax

View file

@ -1,18 +1,40 @@
use crate::syntax::{SyntaxKind, SyntaxToken};
use crate::syntax::SyntaxKind;
use super::{filter_token, Snippet};
use super::{filter_token, Snippet, Token};
impl Snippet {
/// ```rust
/// use orgize::{Org, ast::Snippet};
///
/// let snippet = Org::parse("@@BACKEND:VALUE@@").first_node::<Snippet>().unwrap();
/// assert_eq!(snippet.value().unwrap().text(), "VALUE");
/// assert_eq!(snippet.backend(), "BACKEND");
/// ```
pub fn value(&self) -> Option<SyntaxToken> {
pub fn backend(&self) -> Token {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::TEXT))
.unwrap_or_else(|| {
debug_assert!(false, "snippet must contains TEXT");
Token::default()
})
}
/// ```rust
/// use orgize::{Org, ast::Snippet};
///
/// let snippet = Org::parse("@@BACKEND:@@").first_node::<Snippet>().unwrap();
/// assert_eq!(snippet.value(), "");
/// let snippet = Org::parse("@@BACKEND:VALUE@@").first_node::<Snippet>().unwrap();
/// assert_eq!(snippet.value(), "VALUE");
/// ```
pub fn value(&self) -> Token {
self.syntax
.children_with_tokens()
.filter_map(filter_token(SyntaxKind::TEXT))
.nth(1)
.unwrap_or_else(|| {
debug_assert!(false, "snippet must contains two TEXT");
Token::default()
})
}
}

View file

@ -98,7 +98,7 @@ impl Timestamp {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::TIMESTAMP_REPEATER_MARK))
.map(|t| match t.text() {
.map(|t| match t.as_ref() {
"++" => RepeaterType::CatchUp,
"+" => RepeaterType::Cumulate,
".+" => RepeaterType::Restart,
@ -178,7 +178,7 @@ impl Timestamp {
self.syntax
.children_with_tokens()
.find_map(filter_token(SyntaxKind::TIMESTAMP_DELAY_MARK))
.map(|t| match t.text() {
.map(|t| match t.as_ref() {
"-" => DelayType::All,
"--" => DelayType::First,
_ => {
@ -259,13 +259,13 @@ impl Timestamp {
pub fn start_to_chrono(&self) -> Option<chrono::NaiveDateTime> {
Some(chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(
self.year_start()?.text().parse().ok()?,
self.month_start()?.text().parse().ok()?,
self.day_start()?.text().parse().ok()?,
self.year_start()?.parse().ok()?,
self.month_start()?.parse().ok()?,
self.day_start()?.parse().ok()?,
)?,
chrono::NaiveTime::from_hms_opt(
self.hour_start()?.text().parse().ok()?,
self.minute_start()?.text().parse().ok()?,
self.hour_start()?.parse().ok()?,
self.minute_start()?.parse().ok()?,
0,
)?,
))
@ -284,13 +284,13 @@ impl Timestamp {
pub fn end_to_chrono(&self) -> Option<chrono::NaiveDateTime> {
Some(chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(
self.year_end()?.text().parse().ok()?,
self.month_end()?.text().parse().ok()?,
self.day_end()?.text().parse().ok()?,
self.year_end()?.parse().ok()?,
self.month_end()?.parse().ok()?,
self.day_end()?.parse().ok()?,
)?,
chrono::NaiveTime::from_hms_opt(
self.hour_end()?.text().parse().ok()?,
self.minute_end()?.text().parse().ok()?,
self.hour_end()?.parse().ok()?,
self.minute_end()?.parse().ok()?,
0,
)?,
))

View file

@ -168,10 +168,8 @@ impl Traverser for HtmlExport {
#[tracing::instrument(skip(self, ctx))]
fn snippet(&mut self, event: WalkEvent<&Snippet>, ctx: &mut TraversalContext) {
if let WalkEvent::Enter(snippet) = event {
if matches!(snippet.name(), Some(name) if name.text().eq_ignore_ascii_case("html")) {
if let Some(value) = snippet.value() {
self.output += value.text()
}
if snippet.backend().eq_ignore_ascii_case("html") {
self.output += &snippet.value();
}
return ctx.skip();
};
@ -238,17 +236,16 @@ impl Traverser for HtmlExport {
match event {
WalkEvent::Enter(link) => {
let path = link.path();
let path = path.as_ref().map(|path| path.text()).unwrap_or_default();
if link.is_image() {
let _ = write!(&mut self.output, r#"<img src="{}">"#, HtmlEscape(path));
let _ = write!(&mut self.output, r#"<img src="{}">"#, HtmlEscape(&path));
return ctx.skip();
}
let _ = write!(&mut self.output, r#"<a href="{}">"#, HtmlEscape(path));
let _ = write!(&mut self.output, r#"<a href="{}">"#, HtmlEscape(&path));
if !link.has_description() {
let _ = write!(&mut self.output, "{}</a>", HtmlEscape(path));
let _ = write!(&mut self.output, "{}</a>", HtmlEscape(&path));
return ctx.skip();
}
}

View file

@ -2,7 +2,7 @@ use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{digit1, line_ending, space0},
combinator::{eof, map, opt},
combinator::{eof, map, opt, recognize},
sequence::tuple,
IResult,
};
@ -26,9 +26,7 @@ pub fn clock_node(input: Input) -> IResult<Input, GreenElement, ()> {
space0,
double_arrow_token,
space0,
digit1,
colon_token,
digit1,
recognize(tuple((digit1, colon_token, digit1))),
))),
space0,
alt((line_ending, eof)),
@ -41,13 +39,11 @@ pub fn clock_node(input: Input) -> IResult<Input, GreenElement, ()> {
b.text(clock);
b.ws(ws_);
b.push(timestamp);
if let Some((ws, double_arrow, ws_, hour, colon, minute)) = duration {
if let Some((ws, double_arrow, ws_, time)) = duration {
b.ws(ws);
b.push(double_arrow);
b.ws(ws_);
b.text(hour);
b.push(colon);
b.text(minute);
b.text(time);
}
b.ws(ws__);
b.nl(nl);
@ -125,9 +121,7 @@ fn parse() {
WHITESPACE@53..54 " "
DOUBLE_ARROW@54..56 "=>"
WHITESPACE@56..58 " "
TEXT@58..59 "1"
COLON@59..60 ":"
TEXT@60..62 "00"
TEXT@58..62 "1:00"
NEW_LINE@62..63 "\n"
BLANK_LINE@63..64 "\n"
"###

View file

@ -141,8 +141,7 @@ impl<'a> Iterator for ElementPositions<'a> {
let previous = self.pos;
self.pos = iter
.next()
.map(|i| i + self.pos)
.unwrap_or_else(|| self.input.s.len());
.map_or_else(|| self.input.s.len(), |i| i + self.pos);
debug_assert!(
previous < self.pos && self.pos <= self.input.s.len(),

View file

@ -48,7 +48,6 @@ fn parse() {
let to_link = to_ast::<Link>(link_node);
let link = to_link("[[#id]]");
assert_eq!(link.path().as_ref().map(|x| x.text()), Some("#id"));
insta::assert_debug_snapshot!(
link.syntax,
@r###"

View file

@ -30,10 +30,7 @@ pub fn macros_node(input: Input) -> IResult<Input, GreenElement, ()> {
children.push(l_curly3);
children.push(name.text_token());
if let Some((l_parens, argument, r_parens)) = argument {
children.push(node(
MACROS_ARGUMENT,
[l_parens, argument.text_token(), r_parens],
));
children.extend([l_parens, argument.text_token(), r_parens]);
}
children.push(r_curly3);
node(MACROS, children)
@ -64,10 +61,9 @@ fn test() {
MACROS@0..22
L_CURLY3@0..3 "{{{"
TEXT@3..16 "one_arg_macro"
MACROS_ARGUMENT@16..19
L_PARENS@16..17 "("
TEXT@17..18 "1"
R_PARENS@18..19 ")"
L_PARENS@16..17 "("
TEXT@17..18 "1"
R_PARENS@18..19 ")"
R_CURLY3@19..22 "}}}"
"###
);
@ -78,10 +74,9 @@ fn test() {
MACROS@0..25
L_CURLY3@0..3 "{{{"
TEXT@3..16 "two_arg_macro"
MACROS_ARGUMENT@16..22
L_PARENS@16..17 "("
TEXT@17..21 "1, 2"
R_PARENS@21..22 ")"
L_PARENS@16..17 "("
TEXT@17..21 "1, 2"
R_PARENS@21..22 ")"
R_CURLY3@22..25 "}}}"
"###
);
@ -92,10 +87,9 @@ fn test() {
MACROS@0..28
L_CURLY3@0..3 "{{{"
TEXT@3..16 "two_arg_macro"
MACROS_ARGUMENT@16..25
L_PARENS@16..17 "("
TEXT@17..24 "1\\,a, 2"
R_PARENS@24..25 ")"
L_PARENS@16..17 "("
TEXT@17..24 "1\\,a, 2"
R_PARENS@24..25 ")"
R_CURLY3@25..28 "}}}"
"###
);

View file

@ -189,7 +189,6 @@ pub enum SyntaxKind {
FN_REF,
LATEX_FRAGMENT,
MACROS,
MACROS_ARGUMENT,
SNIPPET,
TARGET,
BOLD,