484 lines
18 KiB
Rust
484 lines
18 KiB
Rust
use nom::branch::alt;
|
|
use nom::bytes::complete::tag;
|
|
use nom::character::complete::anychar;
|
|
use nom::character::complete::line_ending;
|
|
use nom::character::complete::space0;
|
|
use nom::character::complete::space1;
|
|
use nom::combinator::all_consuming;
|
|
use nom::combinator::eof;
|
|
use nom::combinator::map;
|
|
use nom::combinator::not;
|
|
use nom::combinator::opt;
|
|
use nom::combinator::recognize;
|
|
use nom::combinator::verify;
|
|
use nom::multi::many0;
|
|
use nom::multi::many1;
|
|
use nom::multi::many1_count;
|
|
use nom::multi::many_till;
|
|
use nom::multi::separated_list1;
|
|
use nom::sequence::tuple;
|
|
|
|
use super::in_buffer_settings::apply_in_buffer_settings;
|
|
use super::in_buffer_settings::scan_for_in_buffer_settings;
|
|
use super::org_source::OrgSource;
|
|
use super::token::AllTokensIterator;
|
|
use super::token::Token;
|
|
use super::util::exit_matcher_parser;
|
|
use super::util::get_consumed;
|
|
use super::util::start_of_line;
|
|
use crate::context::parser_with_context;
|
|
use crate::context::Context;
|
|
use crate::context::ContextElement;
|
|
use crate::context::ExitClass;
|
|
use crate::context::ExitMatcherNode;
|
|
use crate::context::GlobalSettings;
|
|
use crate::context::List;
|
|
use crate::context::RefContext;
|
|
use crate::error::CustomError;
|
|
use crate::error::MyError;
|
|
use crate::error::Res;
|
|
use crate::parser::comment::comment;
|
|
use crate::parser::element_parser::element;
|
|
use crate::parser::object_parser::standard_set_object;
|
|
use crate::parser::org_source::convert_error;
|
|
use crate::parser::planning::planning;
|
|
use crate::parser::property_drawer::property_drawer;
|
|
use crate::parser::util::blank_line;
|
|
use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting;
|
|
use crate::types::Document;
|
|
use crate::types::DocumentElement;
|
|
use crate::types::Element;
|
|
use crate::types::Heading;
|
|
use crate::types::Object;
|
|
use crate::types::Section;
|
|
|
|
/// Parse a full org-mode document.
|
|
///
|
|
/// This is the main entry point for Organic. It will parse the full contents of the input string as an org-mode document.
|
|
#[allow(dead_code)]
|
|
pub fn parse<'s>(input: &'s str) -> Result<Document<'s>, String> {
|
|
parse_with_settings(input, &GlobalSettings::default())
|
|
}
|
|
|
|
/// Parse a full org-mode document with starting settings.
|
|
///
|
|
/// This is the secondary entry point for Organic. It will parse the full contents of the input string as an org-mode document starting with the settings you supplied.
|
|
///
|
|
/// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO".
|
|
#[allow(dead_code)]
|
|
pub fn parse_with_settings<'g, 's>(
|
|
input: &'s str,
|
|
global_settings: &'g GlobalSettings<'g, 's>,
|
|
) -> Result<Document<'s>, String> {
|
|
let initial_context = ContextElement::document_context();
|
|
let initial_context = Context::new(global_settings, List::new(&initial_context));
|
|
let wrapped_input = OrgSource::new(input);
|
|
let ret =
|
|
all_consuming(parser_with_context!(document_org_source)(&initial_context))(wrapped_input)
|
|
.map_err(|err| err.to_string())
|
|
.map(|(_remaining, parsed_document)| parsed_document);
|
|
ret
|
|
}
|
|
|
|
/// Parse a full org-mode document.
|
|
///
|
|
/// Use this entry point when you want to have direct control over the starting context or if you want to use this integrated with other nom parsers. For general-purpose usage, the `parse` and `parse_with_settings` functions are a lot simpler.
|
|
///
|
|
/// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO".
|
|
#[allow(dead_code)]
|
|
pub fn document<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: &'s str,
|
|
) -> Res<&'s str, Document<'s>> {
|
|
let (remaining, doc) = document_org_source(context, input.into()).map_err(convert_error)?;
|
|
Ok((Into::<&str>::into(remaining), doc))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
#[allow(dead_code)]
|
|
fn document_org_source<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, Document<'s>> {
|
|
let mut final_settings = Vec::new();
|
|
let (_, document_settings) = scan_for_in_buffer_settings(input)?;
|
|
let setup_files: Vec<String> = document_settings
|
|
.iter()
|
|
.filter(|kw| kw.key.eq_ignore_ascii_case("setupfile"))
|
|
.map(|kw| kw.value)
|
|
.map(|setup_file| {
|
|
context
|
|
.get_global_settings()
|
|
.file_access
|
|
.read_file(setup_file)
|
|
.map_err(|err| nom::Err::<CustomError<OrgSource<'_>>>::Failure(err.into()))
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
for setup_file in setup_files.iter().map(String::as_str) {
|
|
let (_, setup_file_settings) =
|
|
scan_for_in_buffer_settings(setup_file.into()).map_err(|_err| {
|
|
nom::Err::Error(CustomError::MyError(MyError(
|
|
"TODO: make this take an owned string so I can dump err.to_string() into it."
|
|
.into(),
|
|
)))
|
|
})?;
|
|
final_settings.extend(setup_file_settings);
|
|
}
|
|
final_settings.extend(document_settings);
|
|
let new_settings = apply_in_buffer_settings(final_settings, context.get_global_settings())
|
|
.map_err(|_err| {
|
|
nom::Err::Error(CustomError::MyError(MyError(
|
|
"TODO: make this take an owned string so I can dump err.to_string() into it."
|
|
.into(),
|
|
)))
|
|
})?;
|
|
let new_context = context.with_global_settings(&new_settings);
|
|
let context = &new_context;
|
|
|
|
// TODO: read the keywords into settings and apply them to the GlobalSettings.
|
|
|
|
let (remaining, document) =
|
|
_document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?;
|
|
{
|
|
// If there are radio targets in this document then we need to parse the entire document again with the knowledge of the radio targets.
|
|
let all_radio_targets: Vec<&Vec<Object<'_>>> = document
|
|
.iter_tokens()
|
|
.filter_map(|tkn| match tkn {
|
|
Token::Object(obj) => Some(obj),
|
|
_ => None,
|
|
})
|
|
.filter_map(|obj| match obj {
|
|
Object::RadioTarget(rt) => Some(rt),
|
|
_ => None,
|
|
})
|
|
.map(|rt| &rt.children)
|
|
.collect();
|
|
if !all_radio_targets.is_empty() {
|
|
let mut new_global_settings = context.get_global_settings().clone();
|
|
new_global_settings.radio_targets = all_radio_targets;
|
|
let parser_context = context.with_global_settings(&new_global_settings);
|
|
let (remaining, document) = _document(&parser_context, input)
|
|
.map(|(rem, out)| (Into::<&str>::into(rem), out))?;
|
|
return Ok((remaining.into(), document));
|
|
}
|
|
}
|
|
Ok((remaining.into(), document))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn _document<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, Document<'s>> {
|
|
let zeroth_section_matcher = parser_with_context!(zeroth_section)(context);
|
|
let heading_matcher = parser_with_context!(heading(0))(context);
|
|
let (remaining, _blank_lines) = many0(blank_line)(input)?;
|
|
let (remaining, zeroth_section) = opt(zeroth_section_matcher)(remaining)?;
|
|
let (remaining, children) = many0(heading_matcher)(remaining)?;
|
|
let source = get_consumed(input, remaining);
|
|
Ok((
|
|
remaining,
|
|
Document {
|
|
source: source.into(),
|
|
zeroth_section,
|
|
children,
|
|
},
|
|
))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn zeroth_section<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, Section<'s>> {
|
|
// TODO: The zeroth section is specialized so it probably needs its own parser
|
|
let contexts = [
|
|
ContextElement::ConsumeTrailingWhitespace(true),
|
|
ContextElement::Context("section"),
|
|
ContextElement::ExitMatcherNode(ExitMatcherNode {
|
|
class: ExitClass::Document,
|
|
exit_matcher: §ion_end,
|
|
}),
|
|
];
|
|
let parser_context = context.with_additional_node(&contexts[0]);
|
|
let parser_context = parser_context.with_additional_node(&contexts[1]);
|
|
let parser_context = parser_context.with_additional_node(&contexts[2]);
|
|
let without_consuming_whitespace_context = ContextElement::ConsumeTrailingWhitespace(false);
|
|
let without_consuming_whitespace_context =
|
|
parser_context.with_additional_node(&without_consuming_whitespace_context);
|
|
|
|
let element_matcher = parser_with_context!(element(true))(&parser_context);
|
|
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
|
|
|
|
let (remaining, comment_and_property_drawer_element) = opt(tuple((
|
|
opt(parser_with_context!(comment)(
|
|
&without_consuming_whitespace_context,
|
|
)),
|
|
parser_with_context!(property_drawer)(context),
|
|
many0(blank_line),
|
|
)))(input)?;
|
|
|
|
let (remaining, (mut children, _exit_contents)) = verify(
|
|
many_till(element_matcher, exit_matcher),
|
|
|(children, _exit_contents)| {
|
|
!children.is_empty() || comment_and_property_drawer_element.is_some()
|
|
},
|
|
)(remaining)?;
|
|
|
|
comment_and_property_drawer_element.map(|(comment, property_drawer, _ws)| {
|
|
children.insert(0, Element::PropertyDrawer(property_drawer));
|
|
comment
|
|
.map(Element::Comment)
|
|
.map(|ele| children.insert(0, ele));
|
|
});
|
|
|
|
let (remaining, _trailing_ws) =
|
|
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
|
|
|
|
let source = get_consumed(input, remaining);
|
|
Ok((
|
|
remaining,
|
|
Section {
|
|
source: source.into(),
|
|
children,
|
|
},
|
|
))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn section<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
mut input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, Section<'s>> {
|
|
// TODO: The zeroth section is specialized so it probably needs its own parser
|
|
let contexts = [
|
|
ContextElement::ConsumeTrailingWhitespace(true),
|
|
ContextElement::Context("section"),
|
|
ContextElement::ExitMatcherNode(ExitMatcherNode {
|
|
class: ExitClass::Document,
|
|
exit_matcher: §ion_end,
|
|
}),
|
|
];
|
|
let parser_context = context.with_additional_node(&contexts[0]);
|
|
let parser_context = parser_context.with_additional_node(&contexts[1]);
|
|
let parser_context = parser_context.with_additional_node(&contexts[2]);
|
|
let element_matcher = parser_with_context!(element(true))(&parser_context);
|
|
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
|
|
let (mut remaining, (planning_element, property_drawer_element)) = tuple((
|
|
opt(parser_with_context!(planning)(&parser_context)),
|
|
opt(parser_with_context!(property_drawer)(&parser_context)),
|
|
))(input)?;
|
|
if planning_element.is_none() && property_drawer_element.is_none() {
|
|
let (remain, _ws) = many0(blank_line)(remaining)?;
|
|
remaining = remain;
|
|
input = remain;
|
|
}
|
|
let (remaining, (mut children, _exit_contents)) = verify(
|
|
many_till(element_matcher, exit_matcher),
|
|
|(children, _exit_contents)| {
|
|
!children.is_empty() || property_drawer_element.is_some() || planning_element.is_some()
|
|
},
|
|
)(remaining)?;
|
|
property_drawer_element
|
|
.map(Element::PropertyDrawer)
|
|
.map(|ele| children.insert(0, ele));
|
|
planning_element
|
|
.map(Element::Planning)
|
|
.map(|ele| children.insert(0, ele));
|
|
|
|
let (remaining, _trailing_ws) =
|
|
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
|
|
|
|
let source = get_consumed(input, remaining);
|
|
Ok((
|
|
remaining,
|
|
Section {
|
|
source: source.into(),
|
|
children,
|
|
},
|
|
))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn section_end<'b, 'g, 'r, 's>(
|
|
_context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
|
recognize(detect_headline)(input)
|
|
}
|
|
|
|
const fn heading(
|
|
parent_stars: usize,
|
|
) -> impl for<'b, 'g, 'r, 's> Fn(
|
|
RefContext<'b, 'g, 'r, 's>,
|
|
OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, Heading<'s>> {
|
|
move |context, input: OrgSource<'_>| _heading(context, input, parent_stars)
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn _heading<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
parent_stars: usize,
|
|
) -> Res<OrgSource<'s>, Heading<'s>> {
|
|
not(|i| context.check_exit_matcher(i))(input)?;
|
|
let (remaining, (star_count, _ws, maybe_todo_keyword, title, heading_tags)) =
|
|
headline(context, input, parent_stars)?;
|
|
let section_matcher = parser_with_context!(section)(context);
|
|
let heading_matcher = parser_with_context!(heading(star_count))(context);
|
|
let (remaining, maybe_section) =
|
|
opt(map(section_matcher, DocumentElement::Section))(remaining)?;
|
|
let (remaining, mut children) =
|
|
many0(map(heading_matcher, DocumentElement::Heading))(remaining)?;
|
|
if let Some(section) = maybe_section {
|
|
children.insert(0, section);
|
|
}
|
|
let remaining = if children.is_empty() {
|
|
// Support empty headings
|
|
let (remain, _ws) = many0(blank_line)(remaining)?;
|
|
remain
|
|
} else {
|
|
remaining
|
|
};
|
|
|
|
let source = get_consumed(input, remaining);
|
|
Ok((
|
|
remaining,
|
|
Heading {
|
|
source: source.into(),
|
|
stars: star_count,
|
|
todo_keyword: maybe_todo_keyword
|
|
.map(|(todo_keyword, _ws)| Into::<&str>::into(todo_keyword)),
|
|
title,
|
|
tags: heading_tags,
|
|
children,
|
|
},
|
|
))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn detect_headline<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
|
|
tuple((start_of_line, many1(tag("*")), space1))(input)?;
|
|
Ok((input, ()))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn headline<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
parent_stars: usize,
|
|
) -> Res<
|
|
OrgSource<'s>,
|
|
(
|
|
usize,
|
|
OrgSource<'s>,
|
|
Option<(OrgSource<'s>, OrgSource<'s>)>,
|
|
Vec<Object<'s>>,
|
|
Vec<&'s str>,
|
|
),
|
|
> {
|
|
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
|
|
class: ExitClass::Document,
|
|
exit_matcher: &headline_title_end,
|
|
});
|
|
let parser_context = context.with_additional_node(&parser_context);
|
|
|
|
let (
|
|
remaining,
|
|
(_sol, star_count, ws, maybe_todo_keyword, title, maybe_tags, _ws, _line_ending),
|
|
) = tuple((
|
|
start_of_line,
|
|
verify(many1_count(tag("*")), |star_count| {
|
|
*star_count > parent_stars
|
|
}),
|
|
space1,
|
|
opt(tuple((
|
|
parser_with_context!(heading_keyword)(&parser_context),
|
|
space1,
|
|
))),
|
|
many1(parser_with_context!(standard_set_object)(&parser_context)),
|
|
opt(tuple((space0, tags))),
|
|
space0,
|
|
alt((line_ending, eof)),
|
|
))(input)?;
|
|
Ok((
|
|
remaining,
|
|
(
|
|
star_count,
|
|
ws,
|
|
maybe_todo_keyword,
|
|
title,
|
|
maybe_tags
|
|
.map(|(_ws, tags)| {
|
|
tags.into_iter()
|
|
.map(|single_tag| Into::<&str>::into(single_tag))
|
|
.collect()
|
|
})
|
|
.unwrap_or(Vec::new()),
|
|
),
|
|
))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn headline_title_end<'b, 'g, 'r, 's>(
|
|
_context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
|
recognize(tuple((
|
|
opt(tuple((space0, tags, space0))),
|
|
alt((line_ending, eof)),
|
|
)))(input)
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn tags<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Vec<OrgSource<'s>>> {
|
|
let (remaining, (_open, tags, _close)) =
|
|
tuple((tag(":"), separated_list1(tag(":"), single_tag), tag(":")))(input)?;
|
|
Ok((remaining, tags))
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
|
recognize(many1(verify(anychar, |c| {
|
|
c.is_alphanumeric() || "_@#%".contains(*c)
|
|
})))(input)
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
fn heading_keyword<'b, 'g, 'r, 's>(
|
|
context: RefContext<'b, 'g, 'r, 's>,
|
|
input: OrgSource<'s>,
|
|
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
|
let global_settings = context.get_global_settings();
|
|
if global_settings.in_progress_todo_keywords.is_empty()
|
|
&& global_settings.complete_todo_keywords.is_empty()
|
|
{
|
|
alt((tag("TODO"), tag("DONE")))(input)
|
|
} else {
|
|
for todo_keyword in global_settings
|
|
.in_progress_todo_keywords
|
|
.iter()
|
|
.chain(global_settings.complete_todo_keywords.iter())
|
|
.map(String::as_str)
|
|
{
|
|
let result = tag::<_, _, CustomError<_>>(todo_keyword)(input);
|
|
match result {
|
|
Ok((remaining, ent)) => {
|
|
return Ok((remaining, ent));
|
|
}
|
|
Err(_) => {}
|
|
}
|
|
}
|
|
Err(nom::Err::Error(CustomError::MyError(MyError(
|
|
"NoTodoKeyword".into(),
|
|
))))
|
|
}
|
|
}
|
|
|
|
impl<'s> Document<'s> {
|
|
pub fn iter_tokens<'r>(&'r self) -> impl Iterator<Item = Token<'r, 's>> {
|
|
AllTokensIterator::new(Token::Document(self))
|
|
}
|
|
}
|