use crate::{
blocks::Block,
tests::prelude::{inline_file_handler::InlineFileHandler, *},
};
track_file!("ref/asciidoc-lang/docs/modules/directives/pages/include-with-leveloffset.adoc");
fn section_levels(doc: &crate::Document<'_>) -> Vec<(usize, String)> {
fn walk(block: &Block<'_>, out: &mut Vec<(usize, String)>) {
if let Block::Section(section) = block {
out.push((section.level(), section.section_title().to_string()));
}
for child in block.nested_blocks() {
walk(child, out);
}
}
let mut out = vec![];
for block in doc.nested_blocks() {
walk(block, &mut out);
}
out
}
non_normative!(
r#"
= Offset Section Levels
//Partitioning Large Documents and using leveloffset
// [#include-partitioning]
When your document gets large, you can split it up into subdocuments for easier editing.
----
= My book
\include::chapter01.adoc[]
\include::chapter02.adoc[]
\include::chapter03.adoc[]
----
TIP: Note the empty lines before and after the include directives.
This practice is recommended whenever including AsciiDoc content to avoid unexpected results (e.g., a section title getting interpreted as a line at the end of a previous paragraph).
== Manipulate heading levels with leveloffset
"#
);
#[test]
fn pushes_headings_down_by_offset() {
verifies!(
r#"
The `leveloffset` attribute can help here by pushing all headings in the included document down by the specified number of levels.
This allows you to publish each chapter as a standalone document (complete with a document title), but still be able to include the chapters into a primary document (which has its own document title).
"#
);
let handler = InlineFileHandler::from_pairs([(
"chapter01.adoc",
"= Chapter Title\n\nChapter intro.\n\n== A Section\n\nSection body.",
)]);
let doc = Parser::default()
.with_safe_mode(SafeMode::Server)
.with_include_file_handler(handler)
.parse("= My Book\n\ninclude::chapter01.adoc[leveloffset=+1]");
assert_eq!(
section_levels(&doc),
vec![
(1, "Chapter Title".to_string()),
(2, "A Section".to_string()),
]
);
let handler = InlineFileHandler::from_pairs([(
"chapter01.adoc",
"= Chapter Title\n\nChapter intro.\n\n== A Section\n\nSection body.",
)]);
let doc = Parser::default()
.with_safe_mode(SafeMode::Server)
.with_include_file_handler(handler)
.parse("= My Book\n\ninclude::chapter01.adoc[]");
assert_eq!(section_levels(&doc), vec![(1, "A Section".to_string())]);
}
non_normative!(
r#"
You can easily assemble your book so that the chapter document titles become level 1 headings using:
----
= My Book
\include::chapter01.adoc[leveloffset=+1]
\include::chapter02.adoc[leveloffset=+1]
\include::chapter03.adoc[leveloffset=+1]
----
"#
);
#[test]
fn relative_offset_accumulates_across_nested_includes() {
verifies!(
r#"
Because the leveloffset is _relative_ (it begins with + or -), this works even if the included document has its own includes and leveloffsets.
"#
);
let handler = InlineFileHandler::from_pairs([
(
"chapter.adoc",
"= Chapter Title\n\nChapter intro.\n\ninclude::section.adoc[leveloffset=+1]",
),
(
"section.adoc",
"= Section Title\n\nSection intro.\n\n== A Subsection\n\nSubsection body.",
),
]);
let doc = Parser::default()
.with_safe_mode(SafeMode::Server)
.with_include_file_handler(handler)
.parse("= My Book\n\ninclude::chapter.adoc[leveloffset=+1]");
assert_eq!(
section_levels(&doc),
vec![
(1, "Chapter Title".to_string()),
(2, "Section Title".to_string()),
(3, "A Subsection".to_string()),
]
);
}
non_normative!(
r#"
If you have lots of chapters to include and want them all to have the same offset, you can save some typing by setting `leveloffset` around the includes:
----
= My book
:leveloffset: +1
\include::chapter01.adoc[]
\include::chapter02.adoc[]
\include::chapter03.adoc[]
:leveloffset: -1
----
"#
);
#[test]
fn trailing_offset_returns_to_zero() {
verifies!(
r#"
The final line returns the level offset to 0.
"#
);
let doc = Parser::default().parse(concat!(
"= My Book\n\n",
":leveloffset: +1\n\n",
"= Wrapped Chapter\n\n",
"Chapter body.\n\n",
":leveloffset: -1\n\n",
"== After Reset\n\n",
"Body after reset.",
));
assert_eq!(
section_levels(&doc),
vec![
(1, "Wrapped Chapter".to_string()),
(1, "After Reset".to_string()),
]
);
}
non_normative!(
r#"
Alternatively, you could use absolute levels:
----
:leveloffset: 1
//includes
:leveloffset: 0
----
Relative levels are preferred.
Absolute levels become awkward when you have nested includes since they aren't context aware.
////
That's also why it's important to surround the include directive by empty lines if it imports in a discrete structure.
You only want to place include files directly adjacent to one another if the imported content should be directly adjacent.
IMPORTANT: Take note of the empty lines between the include directives.
The empty line between include directives prevents the first and last lines of the included files from being adjoined.
This practice is *strongly* encouraged when combining document parts.
If you don't include these empty lines, you might find that the AsciiDoc processor swallows section titles.
This happens because the leading section title can get interpreted as the last line of the final paragraph in the preceding include.
Only place include directives on consecutive lines if the intent is for the includes to run together (such as in a listing block).
////
"#
);
#[test]
fn absolute_offset_set_in_header_shifts_body_headings() {
let doc = Parser::default().parse(concat!(
"= My Book\n",
":leveloffset: 1\n",
"\n",
"= Chapter One\n",
"\n",
"== A Section",
));
assert_eq!(
section_levels(&doc),
vec![(1, "Chapter One".to_string()), (2, "A Section".to_string()),]
);
}
#[test]
fn malformed_relative_offset_is_ignored() {
let doc = Parser::default().parse(concat!(
"= My Book\n",
"\n",
":leveloffset: +x\n",
"\n",
"== Real Section",
));
assert_eq!(section_levels(&doc), vec![(1, "Real Section".to_string())]);
}
#[test]
fn absolute_offset_near_i32_max_does_not_overflow() {
let src = format!(
"= My Book\n:leveloffset: {}\n\n====== Deep Heading",
i32::MAX
);
let doc = Parser::default().parse(&src);
assert_eq!(section_levels(&doc), vec![(5, "Deep Heading".to_string())]);
let warnings: Vec<_> = doc.warnings().map(|w| w.warning.clone()).collect();
assert_eq!(
warnings,
vec![
WarningType::LeveloffsetExcludesAllHeadingLevels(i32::MAX),
WarningType::SectionHeadingLevelOutOfRange(i32::MAX, 5),
]
);
}
#[test]
fn relative_offset_accumulation_near_i32_max_does_not_overflow() {
let src = format!(
"= My Book\n:leveloffset: {}\n\n:leveloffset: +1\n\n====== Deep Heading",
i32::MAX
);
let doc = Parser::default().parse(&src);
assert_eq!(section_levels(&doc), vec![(5, "Deep Heading".to_string())]);
let warnings: Vec<_> = doc.warnings().map(|w| w.warning.clone()).collect();
assert_eq!(
warnings,
vec![
WarningType::LeveloffsetExcludesAllHeadingLevels(i32::MAX),
WarningType::LeveloffsetExcludesAllHeadingLevels(i32::MAX),
WarningType::SectionHeadingLevelOutOfRange(i32::MAX, 5),
]
);
}
fn warning_types(doc: &crate::Document<'_>) -> Vec<crate::warnings::WarningType> {
doc.warnings().map(|w| w.warning.clone()).collect()
}
#[test]
fn positive_offset_clamps_overshooting_child_heading_once() {
let doc = Parser::default().parse(concat!(
"= My Book\n\n",
":leveloffset: +3\n\n",
"== Parent\n\n",
"===== Child",
));
assert_eq!(
section_levels(&doc),
vec![(4, "Parent".to_string()), (5, "Child".to_string())]
);
assert_eq!(
warning_types(&doc),
vec![WarningType::SectionHeadingLevelOutOfRange(7, 5)]
);
}
#[test]
fn negative_offset_clamps_real_section_up_to_one() {
let doc = Parser::default().parse(concat!(
"= My Book\n\n",
":leveloffset: -2\n\n",
"== Pulled Up\n\n",
"body",
));
assert_eq!(section_levels(&doc), vec![(1, "Pulled Up".to_string())]);
assert_eq!(
warning_types(&doc),
vec![WarningType::SectionHeadingLevelOutOfRange(-1, 1)]
);
}
#[test]
fn document_title_with_nonpositive_offset_is_still_rejected() {
let doc = Parser::default().parse(concat!(
"= My Book\n\n",
":leveloffset: -1\n\n",
"= Orphan Title\n\n",
"body",
));
assert!(section_levels(&doc).is_empty());
assert_eq!(
warning_types(&doc),
vec![WarningType::Level0SectionHeadingNotSupported]
);
}
#[test]
fn offsets_at_the_usable_boundary_do_not_warn() {
let high =
Parser::default().parse(concat!("= My Book\n\n", ":leveloffset: 5\n\n", "= Chapter",));
assert_eq!(section_levels(&high), vec![(5, "Chapter".to_string())]);
assert!(warning_types(&high).is_empty());
let low = Parser::default().parse(concat!(
"= My Book\n\n",
":leveloffset: -4\n\n",
"====== Deep",
));
assert_eq!(section_levels(&low), vec![(1, "Deep".to_string())]);
assert!(warning_types(&low).is_empty());
}
#[test]
fn offset_outside_usable_range_warns_when_set() {
let high =
Parser::default().parse(concat!("= My Book\n\n", ":leveloffset: 6\n\n", "= Chapter",));
assert_eq!(section_levels(&high), vec![(5, "Chapter".to_string())]);
assert_eq!(
warning_types(&high),
vec![
WarningType::LeveloffsetExcludesAllHeadingLevels(6),
WarningType::SectionHeadingLevelOutOfRange(6, 5),
]
);
let low = Parser::default().parse(concat!(
"= My Book\n\n",
":leveloffset: -5\n\n",
"====== Deep",
));
assert_eq!(section_levels(&low), vec![(1, "Deep".to_string())]);
assert_eq!(
warning_types(&low),
vec![
WarningType::LeveloffsetExcludesAllHeadingLevels(-5),
WarningType::SectionHeadingLevelOutOfRange(0, 1),
]
);
}
#[test]
fn impossible_offset_set_in_header_warns() {
let doc = Parser::default().parse(concat!("= My Book\n", ":leveloffset: 9\n\n", "== Section",));
assert_eq!(section_levels(&doc), vec![(5, "Section".to_string())]);
assert_eq!(
warning_types(&doc),
vec![
WarningType::LeveloffsetExcludesAllHeadingLevels(9),
WarningType::SectionHeadingLevelOutOfRange(10, 5),
]
);
}
#[test]
fn extreme_negative_relative_offset_is_applied_not_stored_as_absolute() {
let doc = Parser::default().parse(concat!(
"= My Book\n\n",
":leveloffset: 2147483647\n\n",
":leveloffset: -2147483648\n\n",
"====== Deep",
));
assert_eq!(section_levels(&doc), vec![(4, "Deep".to_string())]);
assert_eq!(
warning_types(&doc),
vec![WarningType::LeveloffsetExcludesAllHeadingLevels(i32::MAX)]
);
}