Skip to content

Commit

Permalink
Add Header struct (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten authored Dec 31, 2023
1 parent 197f080 commit c8588a5
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 11 deletions.
30 changes: 23 additions & 7 deletions src/document/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use std::slice::Iter;

use nom::IResult;

use crate::{blocks::Block, primitives::consume_empty_lines, Error, HasSpan, Span};
use crate::{
blocks::Block, document::Header, primitives::consume_empty_lines, Error, HasSpan, Span,
};

/// A document represents the top-level block element in AsciiDoc. It consists
/// of an optional document header and either a) one or more sections preceded
Expand All @@ -13,9 +15,9 @@ use crate::{blocks::Block, primitives::consume_empty_lines, Error, HasSpan, Span
/// The document can be configured using a document header. The header is not a
/// block itself, but contributes metadata to the document, such as the document
/// title and document attributes.
#[allow(dead_code)] // TEMPORARY while building
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Document<'a> {
header: Option<Header<'a>>,
blocks: Vec<Block<'a>>,
source: Span<'a>,
}
Expand All @@ -26,16 +28,30 @@ impl<'a> Document<'a> {
/// Note that the document references the underlying source string and
/// necessarily has the same lifetime as the source.
pub fn parse(source: &'a str) -> Result<Self, Error> {
// TO DO: Add option for best-guess parsing?

let source = Span::new(source, true);
let i = source;
let i = consume_empty_lines(source);

// TO DO: Look for document header.
// TO DO: Add option for best-guess parsing?
let (i, header) = if i.starts_with("= ") {
let (i, header) = Header::parse(i)?;
(i, Some(header))
} else {
(i, None)
};

let (_rem, blocks) = parse_blocks(i)?;

// let blocks: Vec<Block<'a>> = vec![]; // TEMPORARY
Ok(Self { source, blocks })
Ok(Self {
header,
blocks,
source,
})
}

/// Return the document header if there is one.
pub fn header(&'a self) -> Option<&'a Header<'a>> {
self.header.as_ref()
}

/// Return an iterator over the blocks in this document.
Expand Down
55 changes: 55 additions & 0 deletions src/document/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use nom::{bytes::complete::tag, character::complete::space0, IResult};

use crate::{
primitives::{consume_empty_lines, non_empty_line, trim_input_for_rem},
HasSpan, Span,
};

/// An AsciiDoc document may begin with a document header. The document header
/// encapsulates the document title, author and revision information,
/// document-wide attributes, and other document metadata.
#[allow(dead_code)] // TEMPORARY while building
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Header<'a> {
title: Option<Span<'a>>,
source: Span<'a>,
}

impl<'a> Header<'a> {
#[allow(dead_code)] // TEMPORARY
pub(crate) fn parse(i: Span<'a>) -> IResult<Span, Self> {
let source = consume_empty_lines(i);

// TEMPORARY: Titles are optional, but we're not prepared for that yet.
let (rem, title) = parse_title(source)?;

let source = trim_input_for_rem(source, rem);
Ok((
rem,
Self {
title: Some(title),
source,
},
))
}

/// Return a [`Span`] describing the document title, if there was one.
pub fn title(&'a self) -> Option<Span<'a>> {
self.title
}
}

impl<'a> HasSpan<'a> for Header<'a> {
fn span(&'a self) -> &'a Span<'a> {
&self.source
}
}

fn parse_title(i: Span<'_>) -> IResult<Span, Span<'_>> {
let (rem, line) = non_empty_line(i)?;

let (title, _) = tag("= ")(line)?;
let (title, _) = space0(title)?;

Ok((rem, title))
}
3 changes: 3 additions & 0 deletions src/document/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
#[allow(clippy::module_inception)]
mod document;
pub use document::Document;

mod header;
pub use header::Header;
3 changes: 0 additions & 3 deletions src/primitives/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use crate::Span;
/// A line is terminated by end-of-input or a single `\n` character
/// or a single `\r\n` sequence. The end of line sequence is consumed
/// but not included in the returned line.
#[allow(dead_code)] // TEMPORARY
pub(crate) fn line(input: Span<'_>) -> IResult<Span, Span> {
take_till(|c| c == '\n')(input)
.map(|ri| trim_rem_start_matches(ri, '\n'))
Expand Down Expand Up @@ -62,7 +61,6 @@ pub(crate) fn non_empty_line(input: Span<'_>) -> IResult<Span, Span> {
/// An empty line may contain any number of white space characters.
///
/// Returns an error if the line contains any non-white-space characters.
#[allow(dead_code)]
pub(crate) fn empty_line(input: Span<'_>) -> IResult<Span, Span> {
let (i, line) = line(input)?;

Expand All @@ -76,7 +74,6 @@ pub(crate) fn empty_line(input: Span<'_>) -> IResult<Span, Span> {
/// Consumes zero or more empty lines.
///
/// Returns the original input if any error occurs or no empty lines are found.
#[allow(dead_code)]
pub(crate) fn consume_empty_lines(mut input: Span<'_>) -> Span {
while !input.data().is_empty() {
match empty_line(input) {
Expand Down
2 changes: 2 additions & 0 deletions src/tests/asciidoc_lang/root/document_structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod documents {
assert_eq!(
Document::parse("This is a basic AsciiDoc document.\n").unwrap(),
TDocument {
header: None,
source: TSpan {
data: "This is a basic AsciiDoc document.\n",
line: 1,
Expand Down Expand Up @@ -89,6 +90,7 @@ mod documents {
)
.unwrap(),
TDocument {
header: None,
source: TSpan {
data: "This is a basic AsciiDoc document.\n\nThis document contains two paragraphs.\n",
line: 1,
Expand Down
65 changes: 64 additions & 1 deletion src/tests/document/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
document::Document,
tests::fixtures::{
blocks::{TBlock, TSimpleBlock},
document::TDocument,
document::{TDocument, THeader},
TSpan,
},
};
Expand All @@ -22,6 +22,7 @@ fn empty_source() {
assert_eq!(
Document::parse("").unwrap(),
TDocument {
header: None,
source: TSpan {
data: "",
line: 1,
Expand All @@ -38,6 +39,7 @@ fn only_spaces() {
assert_eq!(
Document::parse(" ").unwrap(),
TDocument {
header: None,
source: TSpan {
data: " ",
line: 1,
Expand All @@ -54,6 +56,7 @@ fn one_simple_block() {
assert_eq!(
Document::parse("abc").unwrap(),
TDocument {
header: None,
source: TSpan {
data: "abc",
line: 1,
Expand Down Expand Up @@ -83,6 +86,7 @@ fn two_simple_blocks() {
assert_eq!(
Document::parse("abc\n\ndef").unwrap(),
TDocument {
header: None,
source: TSpan {
data: "abc\n\ndef",
line: 1,
Expand Down Expand Up @@ -122,3 +126,62 @@ fn two_simple_blocks() {
}
);
}

#[test]
fn two_blocks_and_title() {
assert_eq!(
Document::parse("= Example Title\n\nabc\n\ndef").unwrap(),
TDocument {
header: Some(THeader {
title: Some(TSpan {
data: "Example Title",
line: 1,
col: 3,
offset: 2,
}),
source: TSpan {
data: "= Example Title\n",
line: 1,
col: 1,
offset: 0,
}
}),
blocks: vec![
TBlock::Simple(TSimpleBlock {
inlines: vec![TSpan {
data: "abc",
line: 3,
col: 1,
offset: 17,
},],
source: TSpan {
data: "abc\n",
line: 3,
col: 1,
offset: 17,
}
}),
TBlock::Simple(TSimpleBlock {
inlines: vec![TSpan {
data: "def",
line: 5,
col: 1,
offset: 22,
},],
source: TSpan {
data: "def",
line: 5,
col: 1,
offset: 22,
}
}),
],
source: TSpan {
data: "= Example Title\n\nabc\n\ndef",
line: 1,
col: 1,
offset: 0
},
}
);
}
116 changes: 116 additions & 0 deletions src/tests/document/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use pretty_assertions_sorted::assert_eq;

use crate::{
document::Header,
tests::fixtures::{document::THeader, TSpan},
Span,
};

#[test]
fn impl_clone() {
// Silly test to mark the #[derive(...)] line as covered.
let h1 = Header::parse(Span::new("= Title", true)).unwrap();
let h2 = h1.clone();
assert_eq!(h1, h2);
}

#[test]
fn only_title() {
let (rem, block) = Header::parse(Span::new("= Just the Title", true)).unwrap();

assert_eq!(
rem,
TSpan {
data: "",
line: 1,
col: 17,
offset: 16
}
);

assert_eq!(
block,
THeader {
title: Some(TSpan {
data: "Just the Title",
line: 1,
col: 3,
offset: 2,
}),
source: TSpan {
data: "= Just the Title",
line: 1,
col: 1,
offset: 0,
}
}
);
}

#[test]
fn trims_leading_spaces_in_title() {
// This is totally a judgement call on my part. As far as I can tell,
// the language doesn't describe behavior here.
let (rem, block) = Header::parse(Span::new("= Just the Title", true)).unwrap();

assert_eq!(
rem,
TSpan {
data: "",
line: 1,
col: 20,
offset: 19
}
);

assert_eq!(
block,
THeader {
title: Some(TSpan {
data: "Just the Title",
line: 1,
col: 6,
offset: 5,
}),
source: TSpan {
data: "= Just the Title",
line: 1,
col: 1,
offset: 0,
}
}
);
}

#[test]
fn trims_trailing_spaces_in_title() {
let (rem, block) = Header::parse(Span::new("= Just the Title ", true)).unwrap();

assert_eq!(
rem,
TSpan {
data: "",
line: 1,
col: 20,
offset: 19
}
);

assert_eq!(
block,
THeader {
title: Some(TSpan {
data: "Just the Title",
line: 1,
col: 3,
offset: 2,
}),
source: TSpan {
data: "= Just the Title ",
line: 1,
col: 1,
offset: 0,
}
}
);
}
2 changes: 2 additions & 0 deletions src/tests/document/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
// circumstance.
#[allow(clippy::module_inception)]
mod document;

mod header;
Loading

0 comments on commit c8588a5

Please sign in to comment.