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.
This commit is contained in:
Tom Alexander 2023-09-20 03:22:25 -04:00
parent 4c8d9a3063
commit 9bcfb2f1da
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
5 changed files with 35 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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