remove treehouse-format crate and collapse everything into src
This commit is contained in:
parent
ca127a9411
commit
b792688776
66 changed files with 145 additions and 112 deletions
81
src/tree/ast.rs
Normal file
81
src/tree/ast.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use super::{
|
||||
pull::{Attributes, BranchEvent, BranchKind, Parser},
|
||||
ParseError, ParseErrorKind,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Roots {
|
||||
pub attributes: Option<Attributes>,
|
||||
pub branches: Vec<Branch>,
|
||||
}
|
||||
|
||||
impl Roots {
|
||||
pub fn parse(parser: &mut Parser) -> Result<Self, ParseError> {
|
||||
let attributes = parser.top_level_attributes()?;
|
||||
|
||||
let mut branches = vec![];
|
||||
while let Some((branch, indent_level)) = Branch::parse_with_indent_level(parser)? {
|
||||
if indent_level != 0 {
|
||||
return Err(ParseErrorKind::RootIndentLevel.at(branch.kind_span));
|
||||
}
|
||||
branches.push(branch);
|
||||
}
|
||||
Ok(Self {
|
||||
attributes,
|
||||
branches,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Branch {
|
||||
pub indent_level: usize,
|
||||
pub attributes: Option<Attributes>,
|
||||
pub kind: BranchKind,
|
||||
pub kind_span: Range<usize>,
|
||||
pub content: Range<usize>,
|
||||
pub children: Vec<Branch>,
|
||||
}
|
||||
|
||||
impl From<BranchEvent> for Branch {
|
||||
fn from(branch: BranchEvent) -> Self {
|
||||
Self {
|
||||
indent_level: branch.indent_level,
|
||||
attributes: branch.attributes,
|
||||
kind: branch.kind,
|
||||
kind_span: branch.kind_span,
|
||||
content: branch.content,
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
pub fn parse_with_indent_level(
|
||||
parser: &mut Parser,
|
||||
) -> Result<Option<(Self, usize)>, ParseError> {
|
||||
if let Some(branch_event) = parser.next_branch()? {
|
||||
let own_indent_level = branch_event.indent_level;
|
||||
let mut branch = Branch::from(branch_event);
|
||||
let children_indent_level = parser.peek_indent_level();
|
||||
if children_indent_level > own_indent_level {
|
||||
while parser.peek_indent_level() == children_indent_level {
|
||||
if let Some(child) = Branch::parse(parser)? {
|
||||
branch.children.push(child);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some((branch, own_indent_level)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(parser: &mut Parser) -> Result<Option<Self>, ParseError> {
|
||||
Ok(Self::parse_with_indent_level(parser)?.map(|(branch, _)| branch))
|
||||
}
|
||||
}
|
202
src/tree/attributes.rs
Normal file
202
src/tree/attributes.rs
Normal file
|
@ -0,0 +1,202 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{state::FileId, vfs::VPathBuf};
|
||||
|
||||
/// Top-level `%%` root attributes.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct RootAttributes {
|
||||
/// Template to use for generating the page.
|
||||
/// Defaults to `_tree.hbs`.
|
||||
#[serde(default)]
|
||||
pub template: Option<String>,
|
||||
|
||||
/// Title of the generated .html page.
|
||||
///
|
||||
/// The page's tree path is used if empty.
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
|
||||
/// Page icon used in indexes.
|
||||
/// This is an emoji name, such as `page` (default).
|
||||
#[serde(default = "default_icon")]
|
||||
pub icon: String,
|
||||
|
||||
/// Summary of the generated .html page.
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// ID of picture attached to the page, to be used as a thumbnail.
|
||||
#[serde(default)]
|
||||
pub thumbnail: Option<Picture>,
|
||||
|
||||
/// Additional scripts to load into to the page.
|
||||
/// These are relative to the /static/js directory.
|
||||
#[serde(default)]
|
||||
pub scripts: Vec<String>,
|
||||
|
||||
/// Additional styles to load into to the page.
|
||||
/// These are relative to the /static/css directory.
|
||||
#[serde(default)]
|
||||
pub styles: Vec<String>,
|
||||
|
||||
/// Visibility of a page in the parent page's index.
|
||||
#[serde(default)]
|
||||
pub visibility: Visibility,
|
||||
|
||||
/// The page's timestamps. These are automatically populated if a page has at least one branch
|
||||
/// with an ID that includes a timestamp.
|
||||
#[serde(default)]
|
||||
pub timestamps: Option<Timestamps>,
|
||||
|
||||
/// When specified, this page will have a corresponding Atom feed under `rss/{feed}.xml`.
|
||||
///
|
||||
/// In feeds, top-level branches are expected to have a single heading containing the post title.
|
||||
/// Their children are turned into the post description
|
||||
#[serde(default)]
|
||||
pub feed: Option<String>,
|
||||
}
|
||||
|
||||
/// A picture reference.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct Picture {
|
||||
/// ID of the picture.
|
||||
pub id: String,
|
||||
|
||||
/// Optional alt text.
|
||||
#[serde(default)]
|
||||
pub alt: Option<String>,
|
||||
}
|
||||
|
||||
/// Visibility of a page.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum Visibility {
|
||||
#[default]
|
||||
Public,
|
||||
/// Hidden from the parent page's index.
|
||||
Private,
|
||||
}
|
||||
|
||||
/// Timestamps for a page.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct Timestamps {
|
||||
/// When the page was created. By default, this is the timestamp of the least recent branch.
|
||||
pub created: DateTime<Utc>,
|
||||
/// When the page was last updated. By default, this is the timestamp of the most recent branch.
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
fn default_icon() -> String {
|
||||
String::from("page")
|
||||
}
|
||||
|
||||
/// Branch attributes.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct Attributes {
|
||||
/// Unique identifier of the branch.
|
||||
///
|
||||
/// Note that this must be unique to the _entire_ site, not just a single tree.
|
||||
/// This is because trees may be embedded within each other using [`Content::Link`].
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
|
||||
/// Redirect old and deleted listed in the list to this branch.
|
||||
///
|
||||
/// This can be used to keep links permanent even in case the structure of the treehouse changes.
|
||||
#[serde(default)]
|
||||
pub redirect_here: Vec<String>,
|
||||
|
||||
/// Controls how the block should be presented.
|
||||
#[serde(default)]
|
||||
pub content: Content,
|
||||
|
||||
/// Do not persist the branch in localStorage.
|
||||
#[serde(default)]
|
||||
pub do_not_persist: bool,
|
||||
|
||||
/// Strings of extra CSS class names to include in the generated HTML.
|
||||
#[serde(default)]
|
||||
pub classes: Classes,
|
||||
|
||||
/// Enable `mini_template` templating in this branch.
|
||||
#[serde(default)]
|
||||
pub template: bool,
|
||||
|
||||
/// Publishing stage; if `Draft`, the branch is invisible unless treehouse is compiled in
|
||||
/// debug mode.
|
||||
#[serde(default)]
|
||||
pub stage: Stage,
|
||||
|
||||
/// List of extra spells to cast on the branch.
|
||||
#[serde(default)]
|
||||
pub cast: String,
|
||||
|
||||
/// In feeds, specifies the list of tags to attach to an entry.
|
||||
/// This only has an effect on top-level branches.
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
/// Parses the timestamp out of the branch's ID.
|
||||
/// Returns `None` if the ID does not contain a timestamp.
|
||||
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
|
||||
Ulid::from_string(&self.id)
|
||||
.ok()
|
||||
.as_ref()
|
||||
.map(Ulid::timestamp_ms)
|
||||
.and_then(|ms| DateTime::from_timestamp_millis(ms as i64))
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls for block content presentation.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Content {
|
||||
/// Children are stored inline in the block. Nothing special happens.
|
||||
#[default]
|
||||
Inline,
|
||||
|
||||
/// Link to another tree.
|
||||
///
|
||||
/// When JavaScript is enabled, the tree's roots will be embedded inline into the branch and
|
||||
/// loaded lazily.
|
||||
///
|
||||
/// Without JavaScript, the tree will be linked with an `<a>` element.
|
||||
///
|
||||
/// The string provided as an argument is relative to the `content` root and should not contain
|
||||
/// any file extensions. For example, to link to `content/my-article.tree`,
|
||||
/// use `content.link = "my-article"`.
|
||||
///
|
||||
/// Note that `Link` branches must not contain any children. If a `Link` branch does contain
|
||||
/// children, an `attribute`-type error is raised.
|
||||
Link(VPathBuf),
|
||||
|
||||
/// Valid link to another tree.
|
||||
/// This replaces `Content::Link` during semantic analysis.
|
||||
#[serde(skip)]
|
||||
ResolvedLink(FileId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct Classes {
|
||||
/// Classes to append to the branch itself (<li data-cast="b">).
|
||||
#[serde(default)]
|
||||
pub branch: String,
|
||||
|
||||
/// Classes to append to the branch's <ul> element containing its children.
|
||||
#[serde(default)]
|
||||
pub branch_children: String,
|
||||
}
|
||||
|
||||
/// Publish stage of a branch.
|
||||
///
|
||||
/// Draft branches are not included in release builds of treehouse. In debug builds, they are also
|
||||
/// marked with an extra "draft" before the content.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum Stage {
|
||||
#[default]
|
||||
Public,
|
||||
Draft,
|
||||
}
|
1
src/tree/lib.rs
Normal file
1
src/tree/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
224
src/tree/mini_template.rs
Normal file
224
src/tree/mini_template.rs
Normal file
|
@ -0,0 +1,224 @@
|
|||
//! Minimalistic templating engine that integrates with the .tree format and Markdown.
|
||||
//!
|
||||
//! Mostly to avoid pulling in Handlebars everywhere; mini_template, unlike Handlebars, also allows
|
||||
//! for injecting *custom, stateful* context into the renderer, which is important for things like
|
||||
//! the `pic` template to work.
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
dirs::Dirs,
|
||||
html::EscapeHtml,
|
||||
state::Treehouse,
|
||||
vfs::{self, Content, VPath},
|
||||
};
|
||||
|
||||
struct Lexer<'a> {
|
||||
input: &'a str,
|
||||
position: usize,
|
||||
|
||||
// Despite this parser's intentional simplicity, a peekahead buffer needs to be used for
|
||||
// performance because tokens are usually quite long and therefore reparsing them would be
|
||||
// too expensive.
|
||||
peek_buffer: Option<(Token, usize)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum TokenKind {
|
||||
/// Verbatim text, may be inside of a template.
|
||||
Text,
|
||||
Open(EscapingMode), // {%
|
||||
Close, // %}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum EscapingMode {
|
||||
EscapeHtml,
|
||||
NoEscaping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Token {
|
||||
kind: TokenKind,
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
Self {
|
||||
input,
|
||||
position: 0,
|
||||
peek_buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn current(&self) -> Option<char> {
|
||||
self.input[self.position..].chars().next()
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.position += self.current().map(|c| c.len_utf8()).unwrap_or(0);
|
||||
}
|
||||
|
||||
fn create_token(&self, start: usize, kind: TokenKind) -> Token {
|
||||
Token {
|
||||
kind,
|
||||
range: start..self.position,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_inner(&mut self) -> Option<Token> {
|
||||
if let Some((token, after_token)) = self.peek_buffer.take() {
|
||||
self.position = after_token;
|
||||
return Some(token);
|
||||
}
|
||||
|
||||
let start = self.position;
|
||||
match self.current() {
|
||||
Some('{') => {
|
||||
self.advance();
|
||||
if self.current() == Some('%') {
|
||||
self.advance();
|
||||
if self.current() == Some('!') {
|
||||
Some(self.create_token(start, TokenKind::Open(EscapingMode::NoEscaping)))
|
||||
} else {
|
||||
Some(self.create_token(start, TokenKind::Open(EscapingMode::EscapeHtml)))
|
||||
}
|
||||
} else {
|
||||
self.advance();
|
||||
Some(self.create_token(start, TokenKind::Text))
|
||||
}
|
||||
}
|
||||
Some('%') => {
|
||||
self.advance();
|
||||
if self.current() == Some('}') {
|
||||
self.advance();
|
||||
Some(self.create_token(start, TokenKind::Close))
|
||||
} else {
|
||||
self.advance();
|
||||
Some(self.create_token(start, TokenKind::Text))
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
while !matches!(self.current(), Some('{' | '%') | None) {
|
||||
self.advance();
|
||||
}
|
||||
Some(self.create_token(start, TokenKind::Text))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_inner(&mut self) -> Option<Token> {
|
||||
let position = self.position;
|
||||
let token = self.next();
|
||||
let after_token = self.position;
|
||||
self.position = position;
|
||||
|
||||
if let Some(token) = token.clone() {
|
||||
self.peek_buffer = Some((token, after_token));
|
||||
}
|
||||
|
||||
token
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<Token> {
|
||||
self.next_inner().map(|mut token| {
|
||||
// Coalesce multiple Text tokens into one.
|
||||
if token.kind == TokenKind::Text {
|
||||
while let Some(Token {
|
||||
kind: TokenKind::Text,
|
||||
..
|
||||
}) = self.peek_inner()
|
||||
{
|
||||
let next_token = self.next_inner().unwrap();
|
||||
token.range.end = next_token.range.end;
|
||||
}
|
||||
}
|
||||
token
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Renderer<'a> {
|
||||
lexer: Lexer<'a>,
|
||||
output: String,
|
||||
}
|
||||
|
||||
struct InvalidTemplate;
|
||||
|
||||
impl Renderer<'_> {
|
||||
fn emit_token_verbatim(&mut self, token: &Token) {
|
||||
self.output.push_str(&self.lexer.input[token.range.clone()]);
|
||||
}
|
||||
|
||||
fn render(&mut self, config: &Config, treehouse: &Treehouse, dirs: &Dirs) {
|
||||
let kind_of = |token: &Token| token.kind;
|
||||
|
||||
while let Some(token) = self.lexer.next() {
|
||||
match token.kind {
|
||||
TokenKind::Open(escaping) => {
|
||||
let inside = self.lexer.next();
|
||||
let close = self.lexer.next();
|
||||
|
||||
if let Some((TokenKind::Text, TokenKind::Close)) = inside
|
||||
.as_ref()
|
||||
.map(kind_of)
|
||||
.zip(close.as_ref().map(kind_of))
|
||||
{
|
||||
match Self::render_template(
|
||||
config,
|
||||
treehouse,
|
||||
dirs,
|
||||
self.lexer.input[inside.as_ref().unwrap().range.clone()].trim(),
|
||||
) {
|
||||
Ok(s) => match escaping {
|
||||
EscapingMode::EscapeHtml => {
|
||||
_ = write!(self.output, "{}", EscapeHtml(&s));
|
||||
}
|
||||
EscapingMode::NoEscaping => self.output.push_str(&s),
|
||||
},
|
||||
Err(InvalidTemplate) => {
|
||||
inside.inspect(|token| self.emit_token_verbatim(token));
|
||||
close.inspect(|token| self.emit_token_verbatim(token));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inside.inspect(|token| self.emit_token_verbatim(token));
|
||||
close.inspect(|token| self.emit_token_verbatim(token));
|
||||
}
|
||||
}
|
||||
_ => self.emit_token_verbatim(&token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_template(
|
||||
config: &Config,
|
||||
_treehouse: &Treehouse,
|
||||
dirs: &Dirs,
|
||||
template: &str,
|
||||
) -> Result<String, InvalidTemplate> {
|
||||
let (function, arguments) = template.split_once(' ').unwrap_or((template, ""));
|
||||
match function {
|
||||
"pic" => Ok(config.pic_url(&*dirs.pic, arguments)),
|
||||
"include_static" => VPath::try_new(arguments)
|
||||
.ok()
|
||||
.and_then(|vpath| vfs::query::<Content>(&dirs.static_, vpath))
|
||||
.and_then(|c| c.string().ok())
|
||||
.ok_or(InvalidTemplate),
|
||||
_ => Err(InvalidTemplate),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(config: &Config, treehouse: &Treehouse, dirs: &Dirs, input: &str) -> String {
|
||||
let mut renderer = Renderer {
|
||||
lexer: Lexer::new(input),
|
||||
output: String::new(),
|
||||
};
|
||||
renderer.render(config, treehouse, dirs);
|
||||
renderer.output
|
||||
}
|
233
src/tree/pull.rs
Normal file
233
src/tree/pull.rs
Normal file
|
@ -0,0 +1,233 @@
|
|||
use std::{convert::identity, ops::Range};
|
||||
|
||||
use super::{ParseError, ParseErrorKind};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BranchKind {
|
||||
/// Expanded by default.
|
||||
Expanded,
|
||||
/// Folded by default.
|
||||
Collapsed,
|
||||
}
|
||||
|
||||
impl BranchKind {
|
||||
pub fn char(&self) -> char {
|
||||
match self {
|
||||
BranchKind::Expanded => '-',
|
||||
BranchKind::Collapsed => '+',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BranchEvent {
|
||||
pub indent_level: usize,
|
||||
pub kind: BranchKind,
|
||||
pub kind_span: Range<usize>,
|
||||
pub content: Range<usize>,
|
||||
pub attributes: Option<Attributes>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Attributes {
|
||||
pub percent: Range<usize>,
|
||||
pub data: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Parser<'a> {
|
||||
pub input: &'a str,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AllowCodeBlocks {
|
||||
No,
|
||||
Yes,
|
||||
}
|
||||
|
||||
impl Parser<'_> {
|
||||
fn current(&self) -> Option<char> {
|
||||
self.input[self.position..].chars().next()
|
||||
}
|
||||
|
||||
fn current_starts_with(&self, s: &str) -> bool {
|
||||
self.input[self.position..].starts_with(s)
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.position += self.current().map(|c| c.len_utf8()).unwrap_or(0);
|
||||
}
|
||||
|
||||
fn eat_as_long_as(&mut self, c: char) -> usize {
|
||||
let mut count = 0;
|
||||
while self.current() == Some(c) {
|
||||
count += 1;
|
||||
self.advance();
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn eat_while(&mut self, cond: impl Fn(char) -> bool) {
|
||||
while self.current().map(&cond).is_some_and(|x| x) {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
fn eat_until_line_break(&mut self) {
|
||||
loop {
|
||||
match self.current() {
|
||||
Some('\r') => {
|
||||
self.advance();
|
||||
if self.current() == Some('\n') {
|
||||
self.advance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some('\n') => {
|
||||
self.advance();
|
||||
break;
|
||||
}
|
||||
Some(_) => self.advance(),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek_indent_level(&mut self) -> usize {
|
||||
let position = self.position;
|
||||
let indent_level = self.eat_as_long_as(' ');
|
||||
self.position = position;
|
||||
indent_level
|
||||
}
|
||||
|
||||
fn eat_indented_lines_until(
|
||||
&mut self,
|
||||
indent_level: usize,
|
||||
cond: impl Fn(char) -> bool,
|
||||
allow_code_blocks: AllowCodeBlocks,
|
||||
) -> Result<(), ParseError> {
|
||||
let mut code_block: Option<Range<usize>> = None;
|
||||
loop {
|
||||
if let Some(range) = &code_block {
|
||||
self.eat_while(|c| c == ' ');
|
||||
if self.current_starts_with("```") {
|
||||
code_block = None;
|
||||
self.position += 3;
|
||||
self.eat_until_line_break();
|
||||
continue;
|
||||
}
|
||||
self.eat_until_line_break();
|
||||
|
||||
if self.current().is_none() {
|
||||
return Err(ParseErrorKind::UnterminatedCodeBlock.at(range.clone()));
|
||||
}
|
||||
} else {
|
||||
self.eat_while(|c| c == ' ');
|
||||
if allow_code_blocks == AllowCodeBlocks::Yes && self.current_starts_with("```") {
|
||||
code_block = Some(self.position..self.position + 3);
|
||||
self.position += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
self.eat_until_line_break();
|
||||
let before_indentation = self.position;
|
||||
let line_indent_level = self.eat_as_long_as(' ');
|
||||
let after_indentation = self.position;
|
||||
if self.current().map(&cond).is_some_and(identity) || self.current().is_none() {
|
||||
self.position = before_indentation;
|
||||
break;
|
||||
} else if !matches!(self.current(), Some('\n') | Some('\r'))
|
||||
&& line_indent_level < indent_level
|
||||
{
|
||||
return Err(ParseErrorKind::InconsistentIndentation {
|
||||
got: line_indent_level,
|
||||
expected: indent_level,
|
||||
}
|
||||
.at(before_indentation..after_indentation));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn top_level_attributes(&mut self) -> Result<Option<Attributes>, ParseError> {
|
||||
let start = self.position;
|
||||
match self.current() {
|
||||
Some('%') => {
|
||||
let after_one_percent = self.position;
|
||||
self.advance();
|
||||
if self.current() == Some('%') {
|
||||
self.advance();
|
||||
let after_two_percent = self.position;
|
||||
self.eat_indented_lines_until(
|
||||
0,
|
||||
|c| c == '-' || c == '+' || c == '%',
|
||||
AllowCodeBlocks::No,
|
||||
)?;
|
||||
let end = self.position;
|
||||
Ok(Some(Attributes {
|
||||
percent: start..after_two_percent,
|
||||
data: after_two_percent..end,
|
||||
}))
|
||||
} else {
|
||||
self.position = after_one_percent;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_branch(&mut self) -> Result<Option<BranchEvent>, ParseError> {
|
||||
if self.current().is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let indent_level = self.eat_as_long_as(' ');
|
||||
|
||||
let attributes = if self.current() == Some('%') {
|
||||
let start = self.position;
|
||||
self.advance();
|
||||
let after_percent = self.position;
|
||||
self.eat_indented_lines_until(
|
||||
indent_level,
|
||||
|c| c == '-' || c == '+',
|
||||
AllowCodeBlocks::No,
|
||||
)?;
|
||||
self.eat_as_long_as(' ');
|
||||
let end = self.position;
|
||||
Some(Attributes {
|
||||
percent: start..after_percent,
|
||||
data: after_percent..end,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let kind_start = self.position;
|
||||
let kind = match self.current() {
|
||||
Some('-') => BranchKind::Expanded,
|
||||
Some('+') => BranchKind::Collapsed,
|
||||
_ => return Err(ParseErrorKind::BranchKindExpected.at(kind_start..kind_start + 1)),
|
||||
};
|
||||
self.advance();
|
||||
let kind_end = self.position;
|
||||
|
||||
let content_start = self.position;
|
||||
self.eat_indented_lines_until(
|
||||
indent_level,
|
||||
|c| c == '-' || c == '+' || c == '%',
|
||||
AllowCodeBlocks::Yes,
|
||||
)?;
|
||||
let content_end = self.position;
|
||||
|
||||
Ok(Some(BranchEvent {
|
||||
indent_level,
|
||||
attributes,
|
||||
kind,
|
||||
kind_span: kind_start..kind_end,
|
||||
content: content_start..content_end,
|
||||
}))
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue