From f8fd1306e2f21cd1cf4b99e2243cf57e760e366f Mon Sep 17 00:00:00 2001 From: PoiScript Date: Fri, 24 Nov 2023 16:15:11 +0800 Subject: [PATCH] feat: support formulas in org table --- src/ast/table.rs | 33 +++++++++++++++++++++- src/syntax/keyword.rs | 56 +++++++++++++++++++++++++------------ src/syntax/table.rs | 64 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 129 insertions(+), 24 deletions(-) diff --git a/src/ast/table.rs b/src/ast/table.rs index b215cbb..39999ee 100644 --- a/src/ast/table.rs +++ b/src/ast/table.rs @@ -1,6 +1,6 @@ use rowan::ast::AstNode; -use super::{OrgTable, OrgTableRow}; +use super::{filter_token, OrgTable, OrgTableRow, Token}; use crate::syntax::SyntaxKind; impl OrgTable { @@ -48,6 +48,37 @@ impl OrgTable { .skip_while(|row| row.is_standard()) .any(|row| !row.is_rule()) } + + /// Formulas associated to the table + /// + /// ```rust + /// use orgize::{Org, ast::OrgTable}; + /// + /// let table = Org::parse("| a |").first_node::().unwrap(); + /// assert_eq!(table.tblfm().count(), 0); + /// + /// let table = Org::parse("| a |\n#+tblfm: test").first_node::().unwrap(); + /// let tblfm = table.tblfm().collect::>(); + /// assert_eq!(tblfm.len(), 1); + /// assert_eq!(tblfm[0], " test"); + /// + /// let table = Org::parse("| a |\n#+TBLFM: test1\n#+TBLFM: test2").first_node::().unwrap(); + /// let tblfm = table.tblfm().collect::>(); + /// assert_eq!(tblfm.len(), 2); + /// assert_eq!(tblfm[0], " test1"); + /// assert_eq!(tblfm[1], " test2"); + /// ``` + pub fn tblfm(&self) -> impl Iterator { + self.syntax.children().filter_map(|n| { + if n.kind() == SyntaxKind::KEYWORD { + n.children_with_tokens() + .filter_map(filter_token(SyntaxKind::TEXT)) + .last() + } else { + None + } + }) + } } impl OrgTableRow { diff --git a/src/syntax/keyword.rs b/src/syntax/keyword.rs index be4d519..ecc3af0 100644 --- a/src/syntax/keyword.rs +++ b/src/syntax/keyword.rs @@ -8,10 +8,9 @@ use nom::{ sequence::tuple, IResult, InputLength, InputTake, }; -use rowan::GreenNode; use super::{ - combinator::{blank_lines, hash_plus_token, trim_line_end, GreenElement}, + combinator::{blank_lines, hash_plus_token, node, trim_line_end, GreenElement}, input::Input, SyntaxKind, }; @@ -19,18 +18,19 @@ use super::{ #[tracing::instrument(level = "debug", skip(input), fields(input = input.s))] pub fn keyword_node(input: Input) -> IResult { fn f(input: Input) -> IResult { - let (input, (key, mut nodes, post_blank)) = keyword_node_base(input)?; + let (input, (key, mut nodes)) = keyword_node_base(input)?; + let (input, post_blank) = blank_lines(input)?; nodes.extend(post_blank); Ok(( input, - GreenElement::Node(GreenNode::new( + node( if key == "CALL" { - SyntaxKind::BABEL_CALL.into() + SyntaxKind::BABEL_CALL } else { - SyntaxKind::KEYWORD.into() + SyntaxKind::KEYWORD }, nodes, - )), + ), )) } crate::lossless_parser!(f, input) @@ -44,10 +44,12 @@ pub fn affiliated_keyword_nodes(input: Input) -> IResult IResult IResult, Vec), ()> { +pub fn tblfm_keyword_nodes(input: Input) -> IResult, ()> { + let mut children = vec![]; + let mut i = input; + + while !i.is_empty() { + let Ok((input, (key, nodes))) = keyword_node_base(i) else { + break; + }; + + if !key.eq_ignore_ascii_case("TBLFM") { + break; + } + + debug_assert!( + i.input_len() > input.input_len(), + "{} > {}", + i.input_len(), + input.input_len() + ); + i = input; + children.push(node(SyntaxKind::KEYWORD, nodes)); + } + + Ok((i, children)) +} + +fn keyword_node_base(input: Input) -> IResult), ()> { let (input, (ws, hash_plus)) = tuple((space0, hash_plus_token))(input)?; let (input, (key, optional, colon)) = alt((key_with_optional, key))(input)?; let (input, (value, ws_, nl)) = trim_line_end(input)?; - let (input, post_blank) = blank_lines(input)?; let mut children = vec![]; if !ws.is_empty() { @@ -103,7 +125,7 @@ fn keyword_node_base( children.push(nl.nl_token()); } - Ok((input, (key.s, children, post_blank))) + Ok((input, (key.s, children))) } fn key(input: Input) -> IResult, Input), ()> { diff --git a/src/syntax/table.rs b/src/syntax/table.rs index 93e937d..81b3c9b 100644 --- a/src/syntax/table.rs +++ b/src/syntax/table.rs @@ -9,6 +9,7 @@ use nom::{ use super::{ combinator::{blank_lines, line_ends_iter, node, pipe_token, GreenElement, NodeBuilder}, input::Input, + keyword::tblfm_keyword_nodes, object::standard_object_nodes, SyntaxKind::*, }; @@ -23,11 +24,7 @@ fn org_table_node_base(input: Input) -> IResult { // Org tables end at the first line not starting with a vertical bar. if !trimmed.starts_with('|') { - if start == 0 { - return Err(nom::Err::Error(())); - } else { - break; - } + break; } if trimmed.starts_with("|-") { @@ -39,8 +36,17 @@ fn org_table_node_base(input: Input) -> IResult { start = i; } - let (input, post_blank) = blank_lines(input.slice(start..))?; + if start == 0 { + return Err(nom::Err::Error(())); + } + let input = input.slice(start..); + + let (input, tblfm) = tblfm_keyword_nodes(input)?; + + let (input, post_blank) = blank_lines(input)?; + + children.extend(tblfm); children.extend(post_blank); Ok((input, node(ORG_TABLE, children))) @@ -176,6 +182,52 @@ r#"| WHITESPACE@19..20 "\n" "### ); + + insta::assert_debug_snapshot!( + to_org_table("| a |\n#+tblfm: test").syntax, + @r###" + ORG_TABLE@0..19 + ORG_TABLE_STANDARD_ROW@0..6 + PIPE@0..1 "|" + WHITESPACE@1..2 " " + ORG_TABLE_CELL@2..3 + TEXT@2..3 "a" + WHITESPACE@3..4 " " + PIPE@4..5 "|" + WHITESPACE@5..6 "\n" + KEYWORD@6..19 + HASH_PLUS@6..8 "#+" + TEXT@8..13 "tblfm" + COLON@13..14 ":" + TEXT@14..19 " test" + "### + ); + + insta::assert_debug_snapshot!( + to_org_table("| a |\n#+TBLFM: test1\n#+TBLFM: test2").syntax, + @r###" + ORG_TABLE@0..35 + ORG_TABLE_STANDARD_ROW@0..6 + PIPE@0..1 "|" + WHITESPACE@1..2 " " + ORG_TABLE_CELL@2..3 + TEXT@2..3 "a" + WHITESPACE@3..4 " " + PIPE@4..5 "|" + WHITESPACE@5..6 "\n" + KEYWORD@6..21 + HASH_PLUS@6..8 "#+" + TEXT@8..13 "TBLFM" + COLON@13..14 ":" + TEXT@14..20 " test1" + NEW_LINE@20..21 "\n" + KEYWORD@21..35 + HASH_PLUS@21..23 "#+" + TEXT@23..28 "TBLFM" + COLON@28..29 ":" + TEXT@29..35 " test2" + "### + ); } #[test]