feat(config): define todo_keywords in tuple

This commit is contained in:
PoiScript 2019-10-27 11:40:01 +08:00
parent 3b646aa7a5
commit 2b8d2590ff
12 changed files with 111 additions and 96 deletions

View file

@ -6,7 +6,7 @@
A Rust library for parsing orgmode files. A Rust library for parsing orgmode files.
Live demo: https://orgize.herokuapp.com/ [Live demo](https://orgize.herokuapp.com/)
## Parse ## Parse
@ -27,7 +27,7 @@ Org::parse_with_config(
"* TASK Title 1", "* TASK Title 1",
&ParseConfig { &ParseConfig {
// custom todo keywords // custom todo keywords
todo_keywords: vec!["TASK".to_string()], todo_keywords: (vec!["TASK".to_string()], vec![]),
..Default::default() ..Default::default()
}, },
); );
@ -157,7 +157,7 @@ And handler will silently ignores all end events from non-container elements.
So if you want to change how a non-container element renders, just redefine the `start` So if you want to change how a non-container element renders, just redefine the `start`
function and leave the `end` function unchanged. function and leave the `end` function unchanged.
# Serde ## Serde
`Org` struct have already implemented serde's `Serialize` trait. It means you can `Org` struct have already implemented serde's `Serialize` trait. It means you can
serialize it into any format supported by serde, such as json: serialize it into any format supported by serde, such as json:
@ -195,7 +195,7 @@ println!("{}", to_string(&org).unwrap());
## Features ## Features
By now, orgize provides two features: By now, orgize provides three features:
+ `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default. + `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default.

View file

@ -1,19 +1,14 @@
//! Parse configuration module
/// Parse configuration /// Parse configuration
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ParseConfig { pub struct ParseConfig {
/// Headline's todo keywords, todo type /// Headline's todo keywords
pub todo_keywords: Vec<String>, pub todo_keywords: (Vec<String>, Vec<String>),
/// Headline's todo keywords, done type
pub done_keywords: Vec<String>,
} }
impl Default for ParseConfig { impl Default for ParseConfig {
fn default() -> Self { fn default() -> Self {
ParseConfig { ParseConfig {
todo_keywords: vec![String::from("TODO")], todo_keywords: (vec![String::from("TODO")], vec![String::from("DONE")]),
done_keywords: vec![String::from("DONE")],
} }
} }
} }

View file

@ -50,7 +50,7 @@ pub use self::{
use std::borrow::Cow; use std::borrow::Cow;
/// Orgize Element Enum /// Element Enum
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]

View file

@ -9,7 +9,7 @@ use nom::{
IResult, IResult,
}; };
/// Orgize Datetime Struct /// Datetime Struct
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))] #[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -14,9 +14,11 @@ use nom::{
Err, IResult, Err, IResult,
}; };
use crate::config::ParseConfig; use crate::{
use crate::elements::{drawer::parse_drawer, Planning, Timestamp}; config::ParseConfig,
use crate::parsers::{line, skip_empty_lines, take_one_word}; elements::{drawer::parse_drawer, Planning, Timestamp},
parsers::{line, skip_empty_lines, take_one_word},
};
/// Title Elemenet /// Title Elemenet
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
@ -28,10 +30,10 @@ pub struct Title<'a> {
/// Headline priority cookie /// Headline priority cookie
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub priority: Option<char>, pub priority: Option<char>,
/// Headline title tags, including the sparated colons /// Headline title tags
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Vec::is_empty"))] #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Vec::is_empty"))]
pub tags: Vec<Cow<'a, str>>, pub tags: Vec<Cow<'a, str>>,
/// Headline title keyword /// Headline todo keyword
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub keyword: Option<Cow<'a, str>>, pub keyword: Option<Cow<'a, str>>,
/// Raw headline's text, without the stars and the tags /// Raw headline's text, without the stars and the tags
@ -130,8 +132,8 @@ fn parse_title<'a, E: ParseError<&'a str>>(
let (input, keyword) = opt(preceded( let (input, keyword) = opt(preceded(
space1, space1,
verify(take_one_word, |s: &str| { verify(take_one_word, |s: &str| {
config.todo_keywords.iter().any(|x| x == s) config.todo_keywords.0.iter().any(|x| x == s)
|| config.done_keywords.iter().any(|x| x == s) || config.todo_keywords.1.iter().any(|x| x == s)
}), }),
))(input)?; ))(input)?;
@ -353,7 +355,7 @@ fn parse_title_() {
parse_title::<VerboseError<&str>>( parse_title::<VerboseError<&str>>(
"**** DONE Title", "**** DONE Title",
&ParseConfig { &ParseConfig {
done_keywords: vec![], todo_keywords: (vec![], vec![]),
..Default::default() ..Default::default()
} }
), ),
@ -377,7 +379,7 @@ fn parse_title_() {
parse_title::<VerboseError<&str>>( parse_title::<VerboseError<&str>>(
"**** TASK [#A] Title", "**** TASK [#A] Title",
&ParseConfig { &ParseConfig {
todo_keywords: vec!["TASK".to_string()], todo_keywords: (vec!["TASK".to_string()], vec![]),
..Default::default() ..Default::default()
} }
), ),

View file

@ -6,19 +6,32 @@ use jetscii::{bytes, BytesConst};
use crate::elements::Element; use crate::elements::Element;
use crate::export::write_datetime; use crate::export::write_datetime;
/// A wrapper for escaping sensitive characters in html.
///
/// ```rust
/// use orgize::export::html::Escape;
///
/// assert_eq!(format!("{}", Escape("< < <")), "&lt; &lt; &lt;");
/// assert_eq!(
/// format!("{}", Escape("<script>alert('Hello XSS')</script>")),
/// "&lt;script&gt;alert(&apos;Hello XSS&apos;)&lt;/script&gt;"
/// );
/// ```
pub struct Escape<S: AsRef<str>>(pub S); pub struct Escape<S: AsRef<str>>(pub S);
impl<S: AsRef<str>> fmt::Display for Escape<S> { impl<S: AsRef<str>> fmt::Display for Escape<S> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut pos = 0; let mut pos = 0;
let bytes = self.0.as_ref().as_bytes();
let content = self.0.as_ref();
let bytes = content.as_bytes();
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref ESCAPE_BYTES: BytesConst = bytes!(b'<', b'>', b'&', b'\'', b'"'); static ref ESCAPE_BYTES: BytesConst = bytes!(b'<', b'>', b'&', b'\'', b'"');
} }
while let Some(off) = ESCAPE_BYTES.find(&bytes[pos..]) { while let Some(off) = ESCAPE_BYTES.find(&bytes[pos..]) {
write!(f, "{}", &self.0.as_ref()[pos..pos + off])?; write!(f, "{}", &content[pos..pos + off])?;
pos += off + 1; pos += off + 1;
@ -26,13 +39,13 @@ impl<S: AsRef<str>> fmt::Display for Escape<S> {
b'<' => write!(f, "&lt;")?, b'<' => write!(f, "&lt;")?,
b'>' => write!(f, "&gt;")?, b'>' => write!(f, "&gt;")?,
b'&' => write!(f, "&amp;")?, b'&' => write!(f, "&amp;")?,
b'\'' => write!(f, "&#39;")?, b'\'' => write!(f, "&apos;")?,
b'"' => write!(f, "&quot;")?, b'"' => write!(f, "&quot;")?,
_ => unreachable!(), _ => unreachable!(),
} }
} }
write!(f, "{}", &self.0.as_ref()[pos..]) write!(f, "{}", &content[pos..])
} }
} }

View file

@ -3,8 +3,8 @@
pub mod html; pub mod html;
pub mod org; pub mod org;
pub use html::*; pub use html::{DefaultHtmlHandler, Escape as HtmlEscape, HtmlHandler};
pub use org::*; pub use org::{DefaultOrgHandler, OrgHandler};
use std::io::{Error, Write}; use std::io::{Error, Write};

View file

@ -63,11 +63,10 @@ impl Document {
self.0.last_child(org) self.0.last_child(org)
} }
pub fn set_section_content<'a, S: Into<Cow<'a, str>>>( pub fn set_section_content<'a, S>(&mut self, content: S, org: &mut Org<'a>)
&mut self, where
content: S, S: Into<Cow<'a, str>>,
org: &mut Org<'a>, {
) {
let sec_n = if let Some(sec_n) = self.0.sec_n { let sec_n = if let Some(sec_n) = self.0.sec_n {
let children: Vec<_> = sec_n.children(&org.arena).collect(); let children: Vec<_> = sec_n.children(&org.arena).collect();
for child in children { for child in children {
@ -188,6 +187,7 @@ impl Headline {
Element::Section => Some(n), Element::Section => Some(n),
_ => None, _ => None,
}); });
Headline { Headline {
lvl, lvl,
hdl_n, hdl_n,
@ -226,7 +226,10 @@ impl Headline {
} }
} }
pub fn set_title_content<'a, S: Into<Cow<'a, str>>>(self, content: S, org: &mut Org<'a>) { pub fn set_title_content<'a, S>(self, content: S, org: &mut Org<'a>)
where
S: Into<Cow<'a, str>>,
{
let content = content.into(); let content = content.into();
let children: Vec<_> = self.ttl_n.children(&org.arena).collect(); let children: Vec<_> = self.ttl_n.children(&org.arena).collect();
@ -258,11 +261,10 @@ impl Headline {
org.debug_validate(); org.debug_validate();
} }
pub fn set_section_content<'a, S: Into<Cow<'a, str>>>( pub fn set_section_content<'a, S>(&mut self, content: S, org: &mut Org<'a>)
&mut self, where
content: S, S: Into<Cow<'a, str>>,
org: &mut Org<'a>, {
) {
let sec_n = if let Some(sec_n) = self.sec_n { let sec_n = if let Some(sec_n) = self.sec_n {
let children: Vec<_> = sec_n.children(&org.arena).collect(); let children: Vec<_> = sec_n.children(&org.arena).collect();
for child in children { for child in children {
@ -471,3 +473,21 @@ impl Headline {
} }
} }
} }
impl Org<'_> {
/// Return a `Document`
pub fn document(&self) -> Document {
Document::from_org(self)
}
/// Return an iterator of `Headline`
pub fn headlines(&self) -> impl Iterator<Item = Headline> + '_ {
self.root
.descendants(&self.arena)
.skip(1)
.filter_map(move |node| match &self.arena[node].get() {
Element::Headline { level } => Some(Headline::from_node(node, *level, self)),
_ => None,
})
}
}

View file

@ -1,6 +1,6 @@
//! A Rust library for parsing orgmode files. //! A Rust library for parsing orgmode files.
//! //!
//! Live demo: https://orgize.herokuapp.com/ //! [Live demo](https://orgize.herokuapp.com/)
//! //!
//! # Parse //! # Parse
//! //!
@ -18,14 +18,14 @@
//! //!
//! [`Org::parse_with_config`]: org/struct.Org.html#method.parse_with_config //! [`Org::parse_with_config`]: org/struct.Org.html#method.parse_with_config
//! //!
//! ``` rust //! ```rust
//! use orgize::{Org, ParseConfig}; //! use orgize::{Org, ParseConfig};
//! //!
//! Org::parse_with_config( //! Org::parse_with_config(
//! "* TASK Title 1", //! "* TASK Title 1",
//! &ParseConfig { //! &ParseConfig {
//! // custom todo keywords //! // custom todo keywords
//! todo_keywords: vec!["TASK".to_string()], //! todo_keywords: (vec!["TASK".to_string()], vec![]),
//! ..Default::default() //! ..Default::default()
//! }, //! },
//! ); //! );
@ -206,7 +206,7 @@
//! //!
//! # Features //! # Features
//! //!
//! By now, orgize provides two features: //! By now, orgize provides three features:
//! //!
//! + `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default. //! + `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default.
//! //!

View file

@ -5,7 +5,6 @@ use crate::{
config::{ParseConfig, DEFAULT_CONFIG}, config::{ParseConfig, DEFAULT_CONFIG},
elements::Element, elements::Element,
export::{DefaultHtmlHandler, DefaultOrgHandler, HtmlHandler, OrgHandler}, export::{DefaultHtmlHandler, DefaultOrgHandler, HtmlHandler, OrgHandler},
headline::{Document, Headline},
parsers::{parse_container, Container}, parsers::{parse_container, Container},
}; };
@ -35,13 +34,13 @@ impl<'a> Org<'a> {
} }
/// Create a new Org struct from parsing `text`, using a custom ParseConfig /// Create a new Org struct from parsing `text`, using a custom ParseConfig
pub fn parse_with_config(content: &'a str, config: &ParseConfig) -> Org<'a> { pub fn parse_with_config(text: &'a str, config: &ParseConfig) -> Org<'a> {
let mut org = Org::new(); let mut org = Org::new();
parse_container( parse_container(
&mut org.arena, &mut org.arena,
Container::Document { Container::Document {
content, content: text,
node: org.root, node: org.root,
}, },
config, config,
@ -52,22 +51,6 @@ impl<'a> Org<'a> {
org org
} }
/// Return a `Document`
pub fn document(&self) -> Document {
Document::from_org(self)
}
/// Return an iterator of `Headline`
pub fn headlines<'b>(&'b self) -> impl Iterator<Item = Headline> + 'b {
self.root
.descendants(&self.arena)
.skip(1)
.filter_map(move |node| match &self.arena[node].get() {
Element::Headline { level } => Some(Headline::from_node(node, *level, self)),
_ => None,
})
}
/// Return a refrence to underlay arena /// Return a refrence to underlay arena
pub fn arena(&self) -> &Arena<Element<'a>> { pub fn arena(&self) -> &Arena<Element<'a>> {
&self.arena &self.arena
@ -86,8 +69,11 @@ impl<'a> Org<'a> {
}) })
} }
pub fn html<W: Write>(&self, wrtier: W) -> Result<(), Error> { pub fn html<W>(&self, writer: W) -> Result<(), Error>
self.html_with_handler(wrtier, &mut DefaultHtmlHandler) where
W: Write,
{
self.html_with_handler(writer, &mut DefaultHtmlHandler)
} }
pub fn html_with_handler<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E> pub fn html_with_handler<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>
@ -106,8 +92,11 @@ impl<'a> Org<'a> {
Ok(()) Ok(())
} }
pub fn org<W: Write>(&self, wrtier: W) -> Result<(), Error> { pub fn org<W>(&self, writer: W) -> Result<(), Error>
self.org_with_handler(wrtier, &mut DefaultOrgHandler) where
W: Write,
{
self.org_with_handler(writer, &mut DefaultOrgHandler)
} }
pub fn org_with_handler<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E> pub fn org_with_handler<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>

View file

@ -362,22 +362,22 @@ pub fn match_block<'a, T: ElementArena<'a>>(
parent: NodeId, parent: NodeId,
containers: &mut Vec<Container<'a>>, containers: &mut Vec<Container<'a>>,
name: Cow<'a, str>, name: Cow<'a, str>,
args: Option<Cow<'a, str>>, parameters: Option<Cow<'a, str>>,
content: &'a str, content: &'a str,
) { ) {
match &*name.to_uppercase() { match &*name.to_uppercase() {
"CENTER" => { "CENTER" => {
let node = arena.append_element(CenterBlock { parameters: args }, parent); let node = arena.append_element(CenterBlock { parameters }, parent);
containers.push(Container::Block { content, node }); containers.push(Container::Block { content, node });
} }
"QUOTE" => { "QUOTE" => {
let node = arena.append_element(QuoteBlock { parameters: args }, parent); let node = arena.append_element(QuoteBlock { parameters }, parent);
containers.push(Container::Block { content, node }); containers.push(Container::Block { content, node });
} }
"COMMENT" => { "COMMENT" => {
arena.append_element( arena.append_element(
CommentBlock { CommentBlock {
data: args, data: parameters,
contents: content.into(), contents: content.into(),
}, },
parent, parent,
@ -386,7 +386,7 @@ pub fn match_block<'a, T: ElementArena<'a>>(
"EXAMPLE" => { "EXAMPLE" => {
arena.append_element( arena.append_element(
ExampleBlock { ExampleBlock {
data: args, data: parameters,
contents: content.into(), contents: content.into(),
}, },
parent, parent,
@ -395,14 +395,14 @@ pub fn match_block<'a, T: ElementArena<'a>>(
"EXPORT" => { "EXPORT" => {
arena.append_element( arena.append_element(
ExportBlock { ExportBlock {
data: args.unwrap_or_default(), data: parameters.unwrap_or_default(),
contents: content.into(), contents: content.into(),
}, },
parent, parent,
); );
} }
"SRC" => { "SRC" => {
let (language, arguments) = match &args { let (language, arguments) = match &parameters {
Some(Cow::Borrowed(args)) => { Some(Cow::Borrowed(args)) => {
let (language, arguments) = let (language, arguments) =
args.split_at(args.find(' ').unwrap_or_else(|| args.len())); args.split_at(args.find(' ').unwrap_or_else(|| args.len()));
@ -421,17 +421,11 @@ pub fn match_block<'a, T: ElementArena<'a>>(
); );
} }
"VERSE" => { "VERSE" => {
let node = arena.append_element(VerseBlock { parameters: args }, parent); let node = arena.append_element(VerseBlock { parameters }, parent);
containers.push(Container::Block { content, node }); containers.push(Container::Block { content, node });
} }
_ => { _ => {
let node = arena.append_element( let node = arena.append_element(SpecialBlock { parameters, name }, parent);
SpecialBlock {
parameters: args,
name,
},
parent,
);
containers.push(Container::Block { content, node }); containers.push(Container::Block { content, node });
} }
} }
@ -439,7 +433,7 @@ pub fn match_block<'a, T: ElementArena<'a>>(
struct InlinePositions<'a> { struct InlinePositions<'a> {
bytes: &'a [u8], bytes: &'a [u8],
position: usize, pos: usize,
next: Option<usize>, next: Option<usize>,
} }
@ -447,7 +441,7 @@ impl InlinePositions<'_> {
fn new(bytes: &[u8]) -> InlinePositions { fn new(bytes: &[u8]) -> InlinePositions {
InlinePositions { InlinePositions {
bytes, bytes,
position: 0, pos: 0,
next: Some(0), next: Some(0),
} }
} }
@ -463,16 +457,16 @@ impl Iterator for InlinePositions<'_> {
} }
self.next.take().or_else(|| { self.next.take().or_else(|| {
PRE_BYTES.find(&self.bytes[self.position..]).map(|i| { PRE_BYTES.find(&self.bytes[self.pos..]).map(|i| {
self.position += i + 1; self.pos += i + 1;
match self.bytes[self.position - 1] { match self.bytes[self.pos - 1] {
b'{' => { b'{' => {
self.next = Some(self.position); self.next = Some(self.pos);
self.position - 1 self.pos - 1
} }
b' ' | b'(' | b'\'' | b'"' | b'\n' => self.position, b' ' | b'(' | b'\'' | b'"' | b'\n' => self.pos,
_ => self.position - 1, _ => self.pos - 1,
} }
}) })
}) })
@ -683,7 +677,9 @@ pub fn line<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E
} }
pub fn eol<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E> { pub fn eol<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&str, &str, E> {
verify(line, |s: &str| s.trim().is_empty())(input) verify(line, |s: &str| {
s.as_bytes().iter().all(|c| c.is_ascii_whitespace())
})(input)
} }
pub fn take_lines_while(predicate: impl Fn(&str) -> bool) -> impl Fn(&str) -> (&str, &str) { pub fn take_lines_while(predicate: impl Fn(&str) -> bool) -> impl Fn(&str) -> (&str, &str) {
@ -708,7 +704,7 @@ pub fn take_lines_while(predicate: impl Fn(&str) -> bool) -> impl Fn(&str) -> (&
} }
pub fn skip_empty_lines(input: &str) -> &str { pub fn skip_empty_lines(input: &str) -> &str {
take_lines_while(|line| line.trim().is_empty())(input).0 take_lines_while(|line| line.as_bytes().iter().all(|c| c.is_ascii_whitespace()))(input).0
} }
pub fn parse_headline(input: &str) -> Option<(&str, (&str, usize))> { pub fn parse_headline(input: &str) -> Option<(&str, (&str, usize))> {

View file

@ -1,7 +1,7 @@
use indextree::NodeId; use indextree::NodeId;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use crate::elements::{Element, Table, TableRow, Title}; use crate::elements::{Element, Table, TableRow};
use crate::Org; use crate::Org;
/// Validation Error /// Validation Error
@ -104,8 +104,8 @@ impl Org<'_> {
errors.push(ValidationError::ExpectedChildren { at: node_id }); errors.push(ValidationError::ExpectedChildren { at: node_id });
} }
} }
Element::Title(Title { raw, .. }) => { Element::Title(title) => {
if !raw.is_empty() && node.first_child().is_none() { if !title.raw.is_empty() && node.first_child().is_none() {
errors.push(ValidationError::ExpectedChildren { at: node_id }); errors.push(ValidationError::ExpectedChildren { at: node_id });
} }
} }