27 Commits

Author SHA1 Message Date
Tom Alexander
de87b7df93 Publish version 0.1.8.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-21 23:47:48 -04:00
Tom Alexander
a267d13fd7 Merge branch 'worg'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-21 23:38:07 -04:00
Tom Alexander
a29973a110 Add a "format" makefile target.
All checks were successful
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-21 23:20:22 -04:00
Tom Alexander
31c782499e Do not match text markup end with empty contents. 2023-09-21 23:20:21 -04:00
Tom Alexander
b7c7057095 Add a test for double tilde. 2023-09-21 22:52:21 -04:00
Tom Alexander
49e3c90a3a Add a test showing a text markup condition we are not handling and significantly reduce allocations by using references for the captured marker for text markup. 2023-09-21 22:35:09 -04:00
Tom Alexander
129228c5c5 Require either eof or whitespace to line ending for valueless items.
Some checks failed
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-21 22:06:30 -04:00
Tom Alexander
f0a7493a89 Support blank lines for descriptive list with empty value before final list item. 2023-09-21 22:03:21 -04:00
Tom Alexander
dc5695ec9f Update description list test to ensure we match blank values properly for both final and non-final items. 2023-09-21 21:47:42 -04:00
Tom Alexander
4ff62fbfae Support backslash as a post character for text markup. 2023-09-21 21:25:33 -04:00
Tom Alexander
c892d406c3 Do not parse the tag for a plain list item if it is an ordered plain list item. 2023-09-21 20:58:03 -04:00
Tom Alexander
1a41cfc6c7 Support detecting line indentation when checking for contentless plain list items. 2023-09-21 20:08:04 -04:00
Tom Alexander
4f34ab9089 Support subscript/superscript wrapped in parenthesis. 2023-09-21 19:21:47 -04:00
Tom Alexander
9b2348c0ef Allow matched parenthesis inside plain links. 2023-09-21 18:51:11 -04:00
Tom Alexander
5716cbccea Remove unnecessary peak. 2023-09-21 16:34:24 -04:00
Tom Alexander
124cd50243 Add more test cases. 2023-09-21 15:36:55 -04:00
Tom Alexander
bac5d6e1d9 Add a test for parenthesis in regular links for good measure.
We are properly handling this currently, but it is good to have more test coverage.
2023-09-21 14:34:51 -04:00
Tom Alexander
ba15999534 Add a test showing we are not handling parenthesis in links properly.
Some checks failed
rust-test Build rust-test has failed
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-21 14:31:13 -04:00
Tom Alexander
61c3e6c10e Require table formulas have a value. 2023-09-21 14:12:18 -04:00
Tom Alexander
a7e130838d Add a test showing that table formulas with no value do not get associated with the table. 2023-09-21 14:10:20 -04:00
Tom Alexander
853adadf91 Do not allow unescaped opening bracket in path for link. 2023-09-21 13:41:48 -04:00
Tom Alexander
7b61329889 Add test showing we are not parsing links wrapped in brackets correctly.
Some checks failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has failed
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-20 03:48:22 -04:00
Tom Alexander
9bcfb2f1da Decide headline nesting by star count, not headline level.
It is possible to have two headlines that have the same level but different star counts when set to Odd because of rounding. Deciding nesting by star count instead of headline level avoids this issue.
2023-09-20 03:22:25 -04:00
Tom Alexander
4c8d9a3063 Do not require a colon to close dynamic blocks. 2023-09-20 02:37:26 -04:00
Tom Alexander
48cb3c4a02 Move the post-colon check into the item_tag_divider parser. 2023-09-19 23:57:40 -04:00
Tom Alexander
9e60ff6683 Support rematching on italic, underline, and strike-through. 2023-09-19 23:25:49 -04:00
Tom Alexander
c1de001786 Require a space after colon instead of tab for fixed width area. 2023-09-19 20:22:29 -04:00
37 changed files with 492 additions and 104 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "organic"
version = "0.1.7"
version = "0.1.8"
authors = ["Tom Alexander <tom@fizz.buzz>"]
description = "An org-mode parser."
edition = "2021"

View File

@@ -33,6 +33,10 @@ release:
clean:
> cargo clean
.PHONY: format
format:
> $(MAKE) -C docker/cargo_fmt run
.PHONY: test
test:
> cargo test --no-default-features --features compare --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS)

View File

@@ -1,5 +1,5 @@
FROM alpine:3.17 AS build
RUN apk add --no-cache build-base musl-dev git autoconf make texinfo gnutls-dev ncurses-dev gawk
RUN apk add --no-cache build-base musl-dev git autoconf make texinfo gnutls-dev ncurses-dev gawk libgccjit-dev
FROM build AS build-emacs
@@ -8,7 +8,7 @@ RUN git clone --depth 1 --branch $EMACS_VERSION https://git.savannah.gnu.org/git
WORKDIR /root/emacs
RUN mkdir /root/dist
RUN ./autogen.sh
RUN ./configure --prefix /usr --without-x --without-sound
RUN ./configure --prefix /usr --without-x --without-sound --with-native-compilation=aot
RUN make
RUN make DESTDIR="/root/dist" install
@@ -27,7 +27,7 @@ RUN make DESTDIR="/root/dist" install
FROM rustlang/rust:nightly-alpine3.17 AS tester
ENV LANG=en_US.UTF-8
RUN apk add --no-cache musl-dev ncurses gnutls
RUN apk add --no-cache musl-dev ncurses gnutls libgccjit
RUN cargo install --locked --no-default-features --features ci-autoclean cargo-cache
COPY --from=build-emacs /root/dist/ /
COPY --from=build-org-mode /root/dist/ /
@@ -88,7 +88,7 @@ ARG DOOMEMACS_PATH=/foreign_documents/doomemacs
ARG DOOMEMACS_REPO=https://github.com/doomemacs/doomemacs.git
RUN mkdir -p $DOOMEMACS_PATH && git -C $DOOMEMACS_PATH init --initial-branch=main && git -C $DOOMEMACS_PATH remote add origin $DOOMEMACS_REPO && git -C $DOOMEMACS_PATH fetch origin $DOOMEMACS_VERSION && git -C $DOOMEMACS_PATH checkout FETCH_HEAD
ARG WORG_VERSION=74e80b0f7600801b1d1594542602394c085cc2f9
ARG WORG_VERSION=0c8d5679b536af450b61812246a3e02b8103f4b8
ARG WORG_PATH=/foreign_documents/worg
ARG WORG_REPO=https://git.sr.ht/~bzg/worg
RUN mkdir -p $WORG_PATH && git -C $WORG_PATH init --initial-branch=main && git -C $WORG_PATH remote add origin $WORG_REPO && git -C $WORG_PATH fetch origin $WORG_VERSION && git -C $WORG_PATH checkout FETCH_HEAD

View File

@@ -41,9 +41,9 @@ function main {
set -e
if [ "$all_status" -ne 0 ]; then
echo "$(red_text "Some tests failed.")"
red_text "Some tests failed."
else
echo "$(green_text "All tests passed.")"
green_text "All tests passed."
fi
return "$all_status"
}
@@ -64,8 +64,9 @@ function indent {
local depth="$1"
local scaled_depth=$((depth * 2))
shift 1
local prefix=$(printf -- "%${scaled_depth}s")
while read l; do
local prefix
prefix=$(printf -- "%${scaled_depth}s")
while read -r l; do
(IFS=' '; printf -- '%s%s\n' "$prefix" "$l")
done
}
@@ -93,12 +94,13 @@ function compare_all_org_document {
local target_document
local all_status=0
while read target_document; do
local relative_path=$($REALPATH --relative-to "$root_dir" "$target_document")
local relative_path
relative_path=$($REALPATH --relative-to "$root_dir" "$target_document")
set +e
(run_compare "$relative_path" "$target_document")
if [ "$?" -ne 0 ]; then all_status=1; fi
set -e
done<<<$(find "$root_dir" -type f -iname '*.org')
done<<<"$(find "$root_dir" -type f -iname '*.org' | sort)"
return "$all_status"
}

View File

@@ -0,0 +1,3 @@
#+BEGIN: timestamp :format "%Y-%m-%d %H:%M"
#+END

View File

@@ -0,0 +1,6 @@
- foo ::
- bar ::
baz

View File

@@ -0,0 +1,3 @@
1. foo
- bar
- lorem :: ipsum

View File

@@ -0,0 +1,2 @@
# Since this is an ordered list, the text before the " :: " is NOT parsed as a tag.
1. foo :: bar

View File

@@ -0,0 +1,4 @@
| Name | Value |
|------+-------|
| foo | bar |
#+tblfm:

View File

@@ -0,0 +1,2 @@
# Fixed width areas must begin with colon followed by a space, not a tab, so this is not a fixed width area.
: foo

View File

@@ -0,0 +1,11 @@
# Should be a link:
https://en.wikipedia.org/wiki/Shebang_(Unix)
# No closing parenthesis, so link ends at underscore.
https://en.wikipedia.org/wiki/Shebang_(Unix
# Parenthesis only allowed to depth of 2 so link ends at underscore.
https://en.wikipedia.org/wiki/Shebang_(((Unix)))
# Even though they eventually become balanced, we hit negative parenthesis depth so link ends at )
https://en.wikipedia.org/wiki/Shebang)Unix(

View File

@@ -0,0 +1 @@
[[https://en.wikipedia.org/wiki/Shebang_(Unix)]]

View File

@@ -0,0 +1 @@
[[[http://foo.bar/baz][lorem]]]

View File

@@ -0,0 +1,7 @@
# Even though *exporting* honors the setting to require braces for subscript/superscript, the official org-mode parser still parses subscripts and superscripts.
#+OPTIONS: ^:{}
foo_this isn't a subscript when exported due to lack of braces (but its still a subscript during parsing)
bar_{this is a subscript}

View File

@@ -0,0 +1,13 @@
foo_(bar)
foo_(b(ar)
foo_(b{ar)
foo_{b(ar}
foo_(b(a)r)
foo_b(a)r
foo_(b+ar)

View File

@@ -0,0 +1 @@
foo ** bar ** baz

View File

@@ -0,0 +1 @@
foo ~~ bar ~~ baz

View File

@@ -0,0 +1,4 @@
# Since "foos" has an extra "s", this does not match the target.
the foos bar
The <<<foo>>> and stuff.

View File

@@ -5,3 +5,4 @@
*** Lorem
* Ipsum
**** Dolar
***** Cat

View File

@@ -8,7 +8,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${TRACE:="NO"} # or YES to send traces to jaeger
: ${BACKTRACE:="NO"} # or YES to print a rust backtrace when panicking
: ${NO_COLOR:=""} # Set to anything to disable color output
: ${PROFILE:="release-lto"}
: ${PROFILE:="debug"}
REALPATH=$(command -v uu-realpath || command -v realpath)
MAKE=$(command -v gmake || command -v make)

View File

@@ -64,7 +64,7 @@ function run_parse {
local lines="$1"
cd "$SOURCE_FOLDER"
head -n "$lines" "$SOURCE_FOLDER/$TARGET_DOCUMENT" | "${DIR}/run_docker_compare.bash"
head -n "$lines" "$SOURCE_FOLDER/$TARGET_DOCUMENT" | PROFILE=release-lto "${DIR}/run_docker_compare.bash"
local status=$?
return "$status"
}

View File

@@ -732,6 +732,10 @@ fn compare_plain_list<'s>(
Ok(_) => {}
};
// TODO compare :type
//
// :type is an unquoted atom of either descriptive, ordered, or unordered
for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) {
child_status.push(compare_plain_list_item(source, emacs_child, rust_child)?);
}
@@ -894,6 +898,8 @@ fn compare_dynamic_block<'s>(
Ok(_) => {}
};
// TODO: Compare :block-name :arguments
for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) {
child_status.push(compare_element(source, emacs_child, rust_child)?);
}
@@ -2475,6 +2481,8 @@ fn compare_subscript<'s>(
Ok(_) => {}
};
// TODO compare :use-brackets-p
Ok(DiffResult {
status: this_status,
name: emacs_name.to_owned(),
@@ -2506,6 +2514,8 @@ fn compare_superscript<'s>(
Ok(_) => {}
};
// TODO compare :use-brackets-p
Ok(DiffResult {
status: this_status,
name: emacs_name.to_owned(),

View File

@@ -1,14 +1,18 @@
use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::consumed;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::preceded;
use nom::sequence::tuple;
use super::org_source::OrgSource;
@@ -67,24 +71,23 @@ pub(crate) fn dynamic_block<'b, 'g, 'r, 's>(
};
let element_matcher = parser_with_context!(element(true))(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
let (remaining, children) = match tuple((
not(exit_matcher),
not(exit_matcher)(remaining)?;
let (remaining, leading_blank_lines) = opt(consumed(tuple((
blank_line,
many_till(blank_line, exit_matcher),
))(remaining)
{
Ok((remain, (_not_immediate_exit, first_line, (_trailing_whitespace, _exit_contents)))) => {
many0(preceded(not(exit_matcher), blank_line)),
))))(remaining)?;
let leading_blank_lines =
leading_blank_lines.map(|(source, (first_line, _remaining_lines))| {
let mut element = Element::Paragraph(Paragraph::of_text(first_line.into()));
let source = get_consumed(remaining, remain);
element.set_source(source.into());
(remain, vec![element])
}
Err(_) => {
let (remaining, (children, _exit_contents)) =
many_till(element_matcher, exit_matcher)(remaining)?;
(remaining, children)
}
};
element
});
let (remaining, (mut children, _exit_contents)) =
many_till(element_matcher, exit_matcher)(remaining)?;
if let Some(lines) = leading_blank_lines {
children.insert(0, lines);
}
let (remaining, _end) = dynamic_block_end(&parser_context, remaining)?;
let source = get_consumed(input, remaining);
@@ -117,7 +120,8 @@ fn dynamic_block_end<'b, 'g, 'r, 's>(
start_of_line(input)?;
let (remaining, source) = recognize(tuple((
space0,
tag_no_case("#+end:"),
tag_no_case("#+end"),
opt(tag(":")),
alt((eof, line_ending)),
)))(input)?;
Ok((remaining, source))

View File

@@ -3,7 +3,6 @@ use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::recognize;
@@ -12,6 +11,7 @@ use nom::sequence::preceded;
use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::util::only_space1;
use super::util::org_line_ending;
use crate::context::parser_with_context;
use crate::context::RefContext;
@@ -50,7 +50,7 @@ fn fixed_width_area_line<'b, 'g, 'r, 's>(
let (remaining, _indent) = space0(input)?;
let (remaining, _) = tuple((
tag(":"),
alt((recognize(tuple((space1, is_not("\r\n")))), space0)),
alt((recognize(tuple((only_space1, is_not("\r\n")))), space0)),
org_line_ending,
))(remaining)?;
let source = get_consumed(input, remaining);

View File

@@ -34,12 +34,13 @@ use crate::parser::object_parser::standard_set_object;
use crate::parser::util::blank_line;
use crate::types::DocumentElement;
use crate::types::Heading;
use crate::types::HeadlineLevel;
use crate::types::Object;
use crate::types::PriorityCookie;
use crate::types::TodoKeywordType;
pub(crate) const fn heading(
parent_level: usize,
parent_level: HeadlineLevel,
) -> impl for<'b, 'g, 'r, 's> Fn(
RefContext<'b, 'g, 'r, 's>,
OrgSource<'s>,
@@ -51,15 +52,23 @@ pub(crate) const fn heading(
fn _heading<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
parent_level: usize,
parent_star_count: HeadlineLevel,
) -> Res<OrgSource<'s>, Heading<'s>> {
not(|i| context.check_exit_matcher(i))(input)?;
let (
remaining,
(headline_level, maybe_todo_keyword, maybe_priority, maybe_comment, title, heading_tags),
) = headline(context, input, parent_level)?;
(
headline_level,
star_count,
maybe_todo_keyword,
maybe_priority,
maybe_comment,
title,
heading_tags,
),
) = headline(context, input, parent_star_count)?;
let section_matcher = parser_with_context!(section)(context);
let heading_matcher = parser_with_context!(heading(headline_level))(context);
let heading_matcher = parser_with_context!(heading(star_count))(context);
let (remaining, maybe_section) =
opt(map(section_matcher, DocumentElement::Section))(remaining)?;
let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?;
@@ -106,11 +115,12 @@ pub(crate) fn detect_headline<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()
fn headline<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
parent_level: usize,
parent_star_count: HeadlineLevel,
) -> Res<
OrgSource<'s>,
(
usize,
HeadlineLevel,
HeadlineLevel,
Option<(TodoKeywordType, OrgSource<'s>)>,
Option<(OrgSource<'s>, PriorityCookie)>,
Option<OrgSource<'s>>,
@@ -124,11 +134,11 @@ fn headline<'b, 'g, 'r, 's>(
});
let parser_context = context.with_additional_node(&parser_context);
let (remaining, (_, (star_count, _), _)) = tuple((
let (remaining, (_, (headline_level, star_count, _), _)) = tuple((
start_of_line,
verify(
parser_with_context!(headline_level)(&parser_context),
|(level, _)| *level > parent_level,
|(_, count, _)| *count > parent_star_count,
),
peek(org_space),
))(input)?;
@@ -159,6 +169,7 @@ fn headline<'b, 'g, 'r, 's>(
Ok((
remaining,
(
headline_level,
star_count,
maybe_todo_keyword.map(|(_, todo, _)| todo),
maybe_priority,
@@ -261,9 +272,9 @@ fn priority_cookie<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PriorityCooki
fn headline_level<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (usize, OrgSource<'s>)> {
) -> Res<OrgSource<'s>, (HeadlineLevel, HeadlineLevel, OrgSource<'s>)> {
let (remaining, stars) = is_a("*")(input)?;
let count = stars.len();
let count = stars.len().try_into().unwrap();
let level = match context.get_global_settings().odd_levels_only {
crate::context::HeadlineLevelFilter::Odd => {
if count % 2 == 0 {
@@ -274,5 +285,5 @@ fn headline_level<'b, 'g, 'r, 's>(
}
crate::context::HeadlineLevelFilter::OddEven => count,
};
Ok((remaining, (level, stars)))
Ok((remaining, (level, count, stars)))
}

View File

@@ -12,6 +12,7 @@ use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many_till;
use nom::sequence::tuple;
@@ -116,7 +117,9 @@ pub(crate) fn table_formula_keyword<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Keyword<'s>> {
filtered_keyword(table_formula_key)(input)
verify(filtered_keyword(table_formula_key), |kw| {
!kw.value.is_empty()
})(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -59,6 +59,10 @@ impl<'s> OrgSource<'s> {
self.end - self.start
}
pub(crate) fn get_byte_offset(&self) -> usize {
self.start
}
pub(crate) fn get_preceding_character(&self) -> Option<char> {
self.preceding_character
}

View File

@@ -5,17 +5,24 @@ use nom::character::complete::anychar;
use nom::character::complete::none_of;
use nom::character::complete::one_of;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many1;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::org_source::BracketDepth;
use super::org_source::OrgSource;
use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
use crate::context::parser_with_context;
use crate::context::ContextElement;
use crate::context::ContextMatcher;
use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::Matcher;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
@@ -130,17 +137,77 @@ fn path_plain<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
// TODO: "optionally containing parenthesis-wrapped non-whitespace non-bracket substrings up to a depth of two. The string must end with either a non-punctation non-whitespace character, a forwards slash, or a parenthesis-wrapped substring"
let path_plain_end = path_plain_end(input.get_parenthesis_depth());
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &path_plain_end,
});
let parser_context = context.with_additional_node(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
let (remaining, _components) = many1(alt((
parser_with_context!(path_plain_no_parenthesis)(&parser_context),
parser_with_context!(path_plain_parenthesis)(&parser_context),
)))(input)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
}
fn path_plain_end(starting_parenthesis_depth: BracketDepth) -> impl ContextMatcher {
move |context, input: OrgSource<'_>| _path_plain_end(context, input, starting_parenthesis_depth)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _path_plain_end<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
starting_parenthesis_depth: BracketDepth,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _leading_punctuation) = many0(verify(anychar, |c| {
!" \t\r\n[]<>()/".contains(*c) && c.is_ascii_punctuation()
}))(input)?;
let disallowed_character = recognize(one_of(" \t\r\n[]<>"))(remaining);
if disallowed_character.is_ok() {
return disallowed_character;
}
let current_depth = remaining.get_parenthesis_depth() - starting_parenthesis_depth;
if current_depth == 0 {
let close_parenthesis =
tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(remaining);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
let open_parenthesis_without_match = recognize(tuple((
peek(tag("(")),
not(parser_with_context!(path_plain_parenthesis)(context)),
)))(remaining);
if open_parenthesis_without_match.is_ok() {
return open_parenthesis_without_match;
}
}
// many0 punctuation
Err(nom::Err::Error(CustomError::MyError(MyError(
"No path plain end".into(),
))))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn path_plain_no_parenthesis<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, path) = recognize(verify(
many_till(anychar, peek(exit_matcher)),
many_till(
anychar,
alt((
peek(path_plain_no_parenthesis_disallowed_character),
parser_with_context!(exit_matcher_parser)(context),
)),
),
|(children, _exit_contents)| !children.is_empty(),
))(input)?;
@@ -148,14 +215,65 @@ fn path_plain<'b, 'g, 'r, 's>(
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn path_plain_end<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
fn path_plain_no_parenthesis_disallowed_character<'s>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(many_till(
verify(anychar, |c| {
*c != '/' && (c.is_ascii_punctuation() || c.is_whitespace())
}),
one_of(" \t\r\n()[]<>"),
))(input)
recognize(verify(anychar, |c| {
c.is_whitespace() || "()[]<>".contains(*c)
}))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn path_plain_parenthesis<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _opening) = tag("(")(input)?;
let starting_depth = remaining.get_parenthesis_depth();
let (remaining, _path) = recognize(verify(
many_till(
anychar,
alt((
peek(path_plain_parenthesis_end(starting_depth)),
parser_with_context!(exit_matcher_parser)(context),
)),
),
|(children, _exit_contents)| !children.is_empty(),
))(remaining)?;
let (remaining, _opening) = tag(")")(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
}
fn path_plain_parenthesis_end(starting_parenthesis_depth: BracketDepth) -> impl Matcher {
move |input: OrgSource<'_>| _path_plain_parenthesis_end(input, starting_parenthesis_depth)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _path_plain_parenthesis_end<'s>(
input: OrgSource<'s>,
starting_parenthesis_depth: BracketDepth,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let current_depth = input.get_parenthesis_depth() - starting_parenthesis_depth;
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing parenthesis should end the link.
unreachable!("Exceeded plain link parenthesis depth.")
}
if current_depth == 0 {
let close_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(input);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
}
if current_depth == 1 {
let open_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("(")(input);
if open_parenthesis.is_ok() {
return open_parenthesis;
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"No closing parenthesis".into(),
))))
}

View File

@@ -57,7 +57,7 @@ pub(crate) fn detect_plain_list<'b, 'g, 'r, 's>(
parser_with_context!(bullet)(context),
alt((space1, line_ending, eof)),
)),
|(_start, indent, bull, _after_whitespace)| {
|(_start, indent, (_bullet_type, bull), _after_whitespace)| {
Into::<&str>::into(bull) != "*" || indent.len() > 0
},
)(input)
@@ -151,9 +151,9 @@ fn plain_list_item<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, PlainListItem<'s>> {
start_of_line(input)?;
let (remaining, (indent_level, _leading_whitespace)) = indentation_level(context, input)?;
let (remaining, bull) = verify(
let (remaining, (bullet_type, bull)) = verify(
parser_with_context!(bullet)(context),
|bull: &OrgSource<'_>| Into::<&str>::into(bull) != "*" || indent_level > 0,
|(_bullet_type, bull)| Into::<&str>::into(bull) != "*" || indent_level > 0,
)(remaining)?;
let (remaining, _maybe_counter_set) = opt(tuple((
@@ -165,15 +165,33 @@ fn plain_list_item<'b, 'g, 'r, 's>(
let (remaining, maybe_checkbox) = opt(tuple((space1, item_checkbox)))(remaining)?;
let (remaining, maybe_tag) =
opt(tuple((space1, parser_with_context!(item_tag)(context))))(remaining)?;
let (remaining, maybe_tag) = if let BulletType::Unordered = bullet_type {
opt(tuple((space1, parser_with_context!(item_tag)(context))))(remaining)?
} else {
(remaining, None)
};
let exit_matcher = plain_list_item_end(indent_level);
let contexts = [
ContextElement::ConsumeTrailingWhitespace(true),
ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Beta,
exit_matcher: &exit_matcher,
}),
];
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let maybe_contentless_item: Res<OrgSource<'_>, ()> = peek(parser_with_context!(
detect_contentless_item_contents
)(context))(remaining);
)(&parser_context))(remaining);
match maybe_contentless_item {
Ok((_rem, _ws)) => {
let (remaining, _trailing_ws) = opt(blank_line)(remaining)?;
let (remaining, _trailing_ws) = if context.should_consume_trailing_whitespace() {
recognize(alt((recognize(many1(blank_line)), eof)))(remaining)?
} else {
recognize(alt((blank_line, eof)))(remaining)?
};
let source = get_consumed(input, remaining);
return Ok((
remaining,
@@ -191,17 +209,7 @@ fn plain_list_item<'b, 'g, 'r, 's>(
}
Err(_) => {}
};
let (remaining, _ws) = item_tag_post_gap(context, remaining)?;
let exit_matcher = plain_list_item_end(indent_level);
let contexts = [
ContextElement::ConsumeTrailingWhitespace(true),
ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Beta,
exit_matcher: &exit_matcher,
}),
];
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let (remaining, _ws) = item_tag_post_gap(&parser_context, remaining)?;
let (mut remaining, (mut children, _exit_contents)) = many_till(
include_input(parser_with_context!(element(true))(&parser_context)),
@@ -241,19 +249,28 @@ fn plain_list_item<'b, 'g, 'r, 's>(
));
}
#[derive(Debug)]
enum BulletType {
Ordered,
Unordered,
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn bullet<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
) -> Res<OrgSource<'s>, (BulletType, OrgSource<'s>)> {
alt((
tag("*"),
tag("-"),
tag("+"),
recognize(tuple((
parser_with_context!(counter)(context),
alt((tag("."), tag(")"))),
))),
map(tag("*"), |bull| (BulletType::Unordered, bull)),
map(tag("-"), |bull| (BulletType::Unordered, bull)),
map(tag("+"), |bull| (BulletType::Unordered, bull)),
map(
recognize(tuple((
parser_with_context!(counter)(context),
alt((tag("."), tag(")"))),
))),
|bull| (BulletType::Ordered, bull),
),
))(input)
}
@@ -359,22 +376,22 @@ fn item_tag_end<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((
recognize(tuple((
item_tag_divider,
alt((item_tag_divider, line_ending))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn item_tag_divider<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(tuple((
one_of(" \t"),
tag("::"),
peek(tuple((
opt(tuple((
peek(one_of(" \t")),
many_till(anychar, peek(alt((item_tag_divider, line_ending, eof)))),
))),
alt((line_ending, eof)),
))),
line_ending,
))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn item_tag_divider<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(tuple((one_of(" \t"), tag("::"))))(input)
)))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -4,11 +4,13 @@ use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::one_of;
use nom::combinator::eof;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many1;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::radio_link::RematchObject;
@@ -87,11 +89,17 @@ impl<'x> RematchObject<'x> for PlainText<'x> {
break;
}
// let is_whitespace = recognize(many1(org_space_or_line_ending))(input);
let is_not_whitespace = is_not::<&str, &str, CustomError<_>>(" \t\r\n")(goal);
match is_not_whitespace {
Ok((new_goal, payload)) => {
let (new_remaining, _) = tag_no_case(payload)(remaining)?;
let (new_remaining, _) = tuple((
tag_no_case(payload),
// TODO: Test to see what the REAL condition is. Checking for not-alphabetic works fine for now, but the real criteria might be something like the plain text exit matcher.
peek(alt((
recognize(verify(anychar, |c| !c.is_alphanumeric())),
eof,
))),
))(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;

View File

@@ -62,6 +62,22 @@ pub(crate) fn rematch_target<'x, 'b, 'g, 'r, 's>(
remaining = new_remaining;
new_matches.push(new_match);
}
Object::Italic(italic) => {
let (new_remaining, new_match) = italic.rematch_object(context, remaining)?;
remaining = new_remaining;
new_matches.push(new_match);
}
Object::Underline(underline) => {
let (new_remaining, new_match) = underline.rematch_object(context, remaining)?;
remaining = new_remaining;
new_matches.push(new_match);
}
Object::StrikeThrough(strikethrough) => {
let (new_remaining, new_match) =
strikethrough.rematch_object(context, remaining)?;
remaining = new_remaining;
new_matches.push(new_match);
}
Object::PlainText(plaintext) => {
let (new_remaining, new_match) = plaintext.rematch_object(context, remaining)?;
remaining = new_remaining;

View File

@@ -78,7 +78,7 @@ fn pathreg<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, path) = escaped(
take_till1(|c| match c {
'\\' | ']' => true,
'\\' | '[' | ']' => true,
_ => false,
}),
'\\',

View File

@@ -23,6 +23,7 @@ use crate::context::ContextElement;
use crate::context::ContextMatcher;
use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::Matcher;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
@@ -112,6 +113,10 @@ fn script_body<'b, 'g, 'r, 's>(
map(parser_with_context!(script_with_braces)(context), |body| {
ScriptBody::WithBraces(body.into())
}),
map(
parser_with_context!(script_with_parenthesis)(context),
|body| ScriptBody::Braceless(body.into()),
),
))(input)
}
@@ -199,3 +204,49 @@ fn _script_with_braces_end<'b, 'g, 'r, 's>(
}
tag("}")(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn script_with_parenthesis<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _) = tag("(")(input)?;
let exit_with_depth = script_with_parenthesis_end(remaining.get_parenthesis_depth());
let (remaining, _) = many_till(
anychar,
alt((
peek(exit_with_depth),
parser_with_context!(exit_matcher_parser)(context),
)),
)(remaining)?;
let (remaining, _) = tag(")")(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
}
fn script_with_parenthesis_end(starting_parenthesis_depth: BracketDepth) -> impl Matcher {
move |input: OrgSource<'_>| _script_with_parenthesis_end(input, starting_parenthesis_depth)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _script_with_parenthesis_end<'s>(
input: OrgSource<'s>,
starting_parenthesis_depth: BracketDepth,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let current_depth = input.get_parenthesis_depth() - starting_parenthesis_depth;
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing bracket should end the citation.
unreachable!("Exceeded citation key suffix bracket depth.")
}
if current_depth == 0 {
let close_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(input);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"No script parenthesis end.".into(),
))))
}

View File

@@ -163,8 +163,7 @@ fn text_markup_object<'c>(
OrgSource<'s>,
) -> Res<OrgSource<'s>, Vec<Object<'s>>>
+ 'c {
let marker_symbol = marker_symbol.to_owned();
move |context, input: OrgSource<'_>| _text_markup_object(context, input, marker_symbol.as_str())
move |context, input: OrgSource<'_>| _text_markup_object(context, input, marker_symbol)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -183,7 +182,7 @@ fn _text_markup_object<'b, 'g, 'r, 's, 'c>(
let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) =
peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into());
let text_markup_end_specialized = text_markup_end(open.into(), remaining.get_byte_offset());
let contexts = [
ContextElement::ContextObject(marker_symbol),
ContextElement::ExitMatcherNode(ExitMatcherNode {
@@ -245,7 +244,7 @@ fn _text_markup_string<'b, 'g, 'r, 's, 'c>(
let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) =
peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into());
let text_markup_end_specialized = text_markup_end(open.into(), remaining.get_byte_offset());
let contexts = [
ContextElement::ContextObject(marker_symbol),
ContextElement::ExitMatcherNode(ExitMatcherNode {
@@ -312,12 +311,17 @@ fn post<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> {
let (remaining, _) = alt((recognize(one_of(" \r\n\t-.,;:!?')}[\"")), line_ending))(input)?;
let (remaining, _) = alt((recognize(one_of(" \r\n\t-.,;:!?')}[\"\\")), line_ending))(input)?;
Ok((remaining, ()))
}
fn text_markup_end<'c>(marker_symbol: &'c str) -> impl ContextMatcher + 'c {
move |context, input: OrgSource<'_>| _text_markup_end(context, input, marker_symbol)
fn text_markup_end<'c>(
marker_symbol: &'c str,
contents_start_offset: usize,
) -> impl ContextMatcher + 'c {
move |context, input: OrgSource<'_>| {
_text_markup_end(context, input, marker_symbol, contents_start_offset)
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -325,7 +329,13 @@ fn _text_markup_end<'b, 'g, 'r, 's, 'c>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
marker_symbol: &'c str,
contents_start_offset: usize,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
if input.get_byte_offset() == contents_start_offset {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Text markup cannot be empty".into(),
))));
}
not(preceded_by_whitespace(false))(input)?;
let (remaining, _marker) = terminated(
tag(marker_symbol),
@@ -355,6 +365,66 @@ impl<'x> RematchObject<'x> for Bold<'x> {
}
}
impl<'x> RematchObject<'x> for Italic<'x> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn rematch_object<'b, 'g, 'r, 's>(
&'x self,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, children) =
_rematch_text_markup_object(_context, input, "/", &self.children)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::Italic(Italic {
source: source.into(),
children,
}),
))
}
}
impl<'x> RematchObject<'x> for Underline<'x> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn rematch_object<'b, 'g, 'r, 's>(
&'x self,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, children) =
_rematch_text_markup_object(_context, input, "_", &self.children)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::Underline(Underline {
source: source.into(),
children,
}),
))
}
}
impl<'x> RematchObject<'x> for StrikeThrough<'x> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn rematch_object<'b, 'g, 'r, 's>(
&'x self,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, children) =
_rematch_text_markup_object(_context, input, "+", &self.children)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::StrikeThrough(StrikeThrough {
source: source.into(),
children,
}),
))
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _rematch_text_markup_object<'b, 'g, 'r, 's, 'x>(
context: RefContext<'b, 'g, 'r, 's>,
@@ -365,7 +435,7 @@ fn _rematch_text_markup_object<'b, 'g, 'r, 's, 'x>(
let (remaining, _) = pre(context, input)?;
let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) = peek(not(multispace1))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into());
let text_markup_end_specialized = text_markup_end(open.into(), remaining.get_byte_offset());
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &text_markup_end_specialized,

View File

@@ -1,4 +1,5 @@
use nom::branch::alt;
use nom::bytes::complete::is_a;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::none_of;
@@ -228,9 +229,16 @@ where
}
}
/// Match at least one space character.
///
/// This is similar to nom's space1 parser except space1 matches both spaces and tabs whereas this only matches spaces.
pub(crate) fn only_space1<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
is_a(" ")(input)
}
/// Match single space or tab.
///
/// In org-mode syntax, spaces and tabs are interchangeable.
/// In org-mode syntax, spaces and tabs are often (but not always!) interchangeable.
pub(crate) fn org_space<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, char> {
one_of(" \t")(input)
}

View File

@@ -3,6 +3,7 @@ use super::Object;
use super::Source;
pub type PriorityCookie = u8;
pub type HeadlineLevel = u16;
#[derive(Debug)]
pub struct Document<'s> {
@@ -14,7 +15,7 @@ pub struct Document<'s> {
#[derive(Debug)]
pub struct Heading<'s> {
pub source: &'s str,
pub level: usize,
pub level: HeadlineLevel,
pub todo_keyword: Option<(TodoKeywordType, &'s str)>,
pub priority_cookie: Option<PriorityCookie>,
pub title: Vec<Object<'s>>,

View File

@@ -7,6 +7,7 @@ mod source;
pub use document::Document;
pub use document::DocumentElement;
pub use document::Heading;
pub use document::HeadlineLevel;
pub use document::PriorityCookie;
pub use document::Section;
pub use document::TodoKeywordType;