Compare commits

..

No commits in common. "8239b2b51d2ad3517bb0e71440e7bcd5114e08d2" and "5ddbd728a8766ecc4816e0f417c089b19cc91598" have entirely different histories.

18 changed files with 112 additions and 548 deletions

View file

@ -81,7 +81,7 @@ jobs:
run: | run: |
rustup update nightly rustup update nightly
rustup default nightly rustup default nightly
cargo install cargo-afl cargo install afl
- name: "Fuzz" - name: "Fuzz"
run: | run: |
echo core | sudo tee /proc/sys/kernel/core_pattern echo core | sudo tee /proc/sys/kernel/core_pattern

View file

@ -1,104 +0,0 @@
name: release
on:
push:
tags: ["[0-9]+.[0-9]+.[0-9]+*"]
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
create:
name: create release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
- name: verify version matches
shell: bash
run: grep -q 'version = "${{ github.ref_name }}"' Cargo.toml || { echo version mismatch >&2 && exit 1; }
- name: create release
run: gh release create ${{ github.ref_name }} --draft --verify-tag --title "Release ${{ github.ref_name }}"
build:
name: build
needs: ['create']
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-unknown-linux-musl
- os: macos-latest
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: i686-pc-windows-msvc
steps:
- name: checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
- name: install rust
shell: bash
run: |
rustup update stable
rustup target add ${{ matrix.target }}
- name: build
shell: bash
run: |
if [ -n "${{ matrix.linker }}" ]; then
export RUSTFLAGS="-Clinker=${{ matrix.linker }}"
fi
cargo build --verbose --release --target ${{ matrix.target }}
find .
bin="target/${{ matrix.target }}/release/jotdown"
[ "${{ matrix.os }}" = "windows-latest" ] && bin="$bin.exe"
echo "BIN=$bin" >> $GITHUB_ENV
- name: strip
if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: strip $BIN
- name: set archive name
shell: bash
run: echo "ARCHIVE=jotdown-${{ github.ref_name }}-${{ matrix.target }}" >> $GITHUB_ENV
- name: init archive dir
shell: bash
run: |
mkdir "$ARCHIVE"/
cp "$BIN" "$ARCHIVE"/
cp {COPYING,CHANGELOG.md,README.md} "$ARCHIVE"/
- name: archive (win)
if: ${{ startsWith(matrix.os, 'windows') }}
shell: bash
run: |
7z a "$ARCHIVE.zip" "$ARCHIVE"
echo "ASSET=$ARCHIVE.zip" >> $GITHUB_ENV
- name: archive (unix)
if: ${{ ! startsWith(matrix.os, 'windows') }}
shell: bash
run: |
tar czf "$ARCHIVE.tar.gz" "$ARCHIVE"
echo "ASSET=$ARCHIVE.tar.gz" >> $GITHUB_ENV
- name: Upload release archive
shell: bash
run: |
gh release upload ${{ github.ref_name }} ${{ env.ASSET }}

View file

@ -1,24 +1,3 @@
## [0.3.2](https://github.com/hellux/jotdown/releases/tag/0.3.2) - 2023-09-06
### Changed
- Alphabetic list markers can only be one character long.
## [0.3.1](https://github.com/hellux/jotdown/releases/tag/0.3.1) - 2023-08-05
### Changed
- Block parser performance improved, up to 15% faster.
- Last `unsafe` block removed (#5).
### Fixed
- Do not require indent for continuing footnotes.
- Transfer classes from reference definitions to links.
- Allow line breaks in reference links (still match reference label).
- Remove excess newline after raw blocks.
- HTML renderer: fix missing `<p>` tags after ordered lists (#44).
## [0.3.0](https://github.com/hellux/jotdown/releases/tag/0.3.0) - 2023-05-16 ## [0.3.0](https://github.com/hellux/jotdown/releases/tag/0.3.0) - 2023-05-16
### Added ### Added

62
Cargo.lock generated
View file

@ -162,29 +162,6 @@ dependencies = [
"itertools", "itertools",
] ]
[[package]]
name = "databake"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82175d72e69414ceafbe2b49686794d3a8bed846e0d50267355f83ea8fdd953a"
dependencies = [
"databake-derive",
"proc-macro2",
"quote",
]
[[package]]
name = "databake-derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377af281d8f23663862a7c84623bc5dcf7f8c44b13c7496a590bdc157f941a43"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
"synstructure",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.8.1" version = "1.8.1"
@ -286,14 +263,11 @@ dependencies = [
[[package]] [[package]]
name = "jotdown" name = "jotdown"
version = "0.3.2" version = "0.3.0"
dependencies = [
"databake",
]
[[package]] [[package]]
name = "jotdown_wasm" name = "jotdown_wasm"
version = "0.3.2" version = "0.3.0"
dependencies = [ dependencies = [
"git2", "git2",
"jotdown", "jotdown",
@ -396,9 +370,9 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.79" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -459,7 +433,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -484,28 +458,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "syn"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
]
[[package]] [[package]]
name = "test-html-ref" name = "test-html-ref"
version = "0.1.0" version = "0.1.0"
@ -621,7 +573,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.107", "syn",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -643,7 +595,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.107", "syn",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View file

@ -2,7 +2,7 @@
name = "jotdown" name = "jotdown"
description = "A parser for the Djot markup language" description = "A parser for the Djot markup language"
authors = ["Noah Hellman <noah@hllmn.net>"] authors = ["Noah Hellman <noah@hllmn.net>"]
version = "0.3.2" version = "0.3.0"
license = "MIT" license = "MIT"
edition = "2021" edition = "2021"
keywords = ["djot", "markup"] keywords = ["djot", "markup"]
@ -35,14 +35,10 @@ exclude = [
[[bin]] [[bin]]
name = "jotdown" name = "jotdown"
required-features = ["html", "parser"] required-features = ["html"]
doc = false doc = false
[features] [features]
default = ["html"] default = ["html"]
html = [] # html renderer and minimal cli binary html = [] # html renderer and minimal cli binary
deterministic = [] # for stable fuzzing deterministic = [] # for stable fuzzing
parser = []
[dependencies]
databake = { version = "0.1.7", features = ["derive"] }

View file

@ -3,7 +3,7 @@ name = "jotdown_wasm"
description = "Web demo of Jotdown" description = "Web demo of Jotdown"
authors = ["Noah Hellman <noah@hllmn.net>"] authors = ["Noah Hellman <noah@hllmn.net>"]
license = "MIT" license = "MIT"
version = "0.3.2" version = "0.3.0"
edition = "2021" edition = "2021"
homepage = "https://hllmn.net/projects/jotdown" homepage = "https://hllmn.net/projects/jotdown"
repository = "https://github.com/hellux/jotdown" repository = "https://github.com/hellux/jotdown"

View file

@ -1,17 +1,13 @@
use databake::Bake;
use crate::CowStr; use crate::CowStr;
use std::{borrow::Cow, fmt}; use std::fmt;
/// Parse attributes, assumed to be valid. /// Parse attributes, assumed to be valid.
#[cfg(feature = "parser")]
pub(crate) fn parse(src: &str) -> Attributes { pub(crate) fn parse(src: &str) -> Attributes {
let mut a = Attributes::new(); let mut a = Attributes::new();
a.parse(src); a.parse(src);
a a
} }
#[cfg(feature = "parser")]
pub fn valid(src: &str) -> usize { pub fn valid(src: &str) -> usize {
use State::*; use State::*;
@ -35,8 +31,7 @@ pub fn valid(src: &str) -> usize {
/// Stores an attribute value that supports backslash escapes of ASCII punctuation upon displaying, /// Stores an attribute value that supports backslash escapes of ASCII punctuation upon displaying,
/// without allocating. /// without allocating.
#[derive(Clone, Debug, Eq, PartialEq, Bake)] #[derive(Clone, Debug, Eq, PartialEq)]
#[databake(path = jotdown)]
pub struct AttributeValue<'s> { pub struct AttributeValue<'s> {
raw: CowStr<'s>, raw: CowStr<'s>,
} }
@ -123,9 +118,8 @@ impl<'s> Iterator for AttributeValueParts<'s> {
// Attributes are relatively rare, we choose to pay 8 bytes always and sometimes an extra // Attributes are relatively rare, we choose to pay 8 bytes always and sometimes an extra
// indirection instead of always 24 bytes. // indirection instead of always 24 bytes.
#[allow(clippy::box_vec)] #[allow(clippy::box_vec)]
#[derive(Clone, PartialEq, Eq, Default, Bake)] #[derive(Clone, PartialEq, Eq, Default)]
#[databake(path = jotdown)] pub struct Attributes<'s>(Option<Box<Vec<(&'s str, AttributeValue<'s>)>>>);
pub struct Attributes<'s>(pub Option<Cow<'s, [(&'s str, AttributeValue<'s>)]>>);
impl<'s> Attributes<'s> { impl<'s> Attributes<'s> {
/// Create an empty collection. /// Create an empty collection.
@ -135,13 +129,11 @@ impl<'s> Attributes<'s> {
} }
#[must_use] #[must_use]
#[cfg(feature = "parser")]
pub(crate) fn take(&mut self) -> Self { pub(crate) fn take(&mut self) -> Self {
Self(self.0.take()) Self(self.0.take())
} }
/// Parse and append attributes, assumed to be valid. /// Parse and append attributes, assumed to be valid.
#[cfg(feature = "parser")]
pub(crate) fn parse(&mut self, input: &'s str) { pub(crate) fn parse(&mut self, input: &'s str) {
let mut parser = Parser::new(self.take()); let mut parser = Parser::new(self.take());
parser.parse(input); parser.parse(input);
@ -149,13 +141,12 @@ impl<'s> Attributes<'s> {
} }
/// Combine all attributes from both objects, prioritizing self on conflicts. /// Combine all attributes from both objects, prioritizing self on conflicts.
#[cfg(feature = "parser")]
pub(crate) fn union(&mut self, other: Self) { pub(crate) fn union(&mut self, other: Self) {
if let Some(attrs0) = &mut self.0 { if let Some(attrs0) = &mut self.0 {
if let Some(mut attrs1) = other.0 { if let Some(mut attrs1) = other.0 {
for (key, val) in attrs1.to_mut().drain(..) { for (key, val) in attrs1.drain(..) {
if key == "class" || !attrs0.iter().any(|(k, _)| *k == key) { if key == "class" || !attrs0.iter().any(|(k, _)| *k == key) {
attrs0.to_mut().push((key, val)); attrs0.push((key, val));
} }
} }
} }
@ -179,7 +170,7 @@ impl<'s> Attributes<'s> {
let attrs = self.0.as_mut().unwrap(); let attrs = self.0.as_mut().unwrap();
if let Some(i) = attrs.iter().position(|(k, _)| *k == key) { if let Some(i) = attrs.iter().position(|(k, _)| *k == key) {
let prev = &mut attrs.to_mut()[i].1; let prev = &mut attrs[i].1;
if key == "class" { if key == "class" {
match val.raw { match val.raw {
CowStr::Borrowed(s) => prev.extend(s), CowStr::Borrowed(s) => prev.extend(s),
@ -193,7 +184,7 @@ impl<'s> Attributes<'s> {
i i
} else { } else {
let i = attrs.len(); let i = attrs.len();
attrs.to_mut().push((key, val)); attrs.push((key, val));
i i
} }
} }
@ -247,12 +238,10 @@ impl<'s> std::fmt::Debug for Attributes<'s> {
} }
#[derive(Clone)] #[derive(Clone)]
#[cfg(feature = "parser")]
pub struct Validator { pub struct Validator {
state: State, state: State,
} }
#[cfg(feature = "parser")]
impl Validator { impl Validator {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -285,14 +274,12 @@ impl Validator {
/// ///
/// Input is assumed to contain a valid series of attribute sets, the attributes are added as they /// Input is assumed to contain a valid series of attribute sets, the attributes are added as they
/// are encountered. /// are encountered.
#[cfg(feature = "parser")]
pub struct Parser<'s> { pub struct Parser<'s> {
attrs: Attributes<'s>, attrs: Attributes<'s>,
i_prev: usize, i_prev: usize,
state: State, state: State,
} }
#[cfg(feature = "parser")]
impl<'s> Parser<'s> { impl<'s> Parser<'s> {
pub fn new(attrs: Attributes<'s>) -> Self { pub fn new(attrs: Attributes<'s>) -> Self {
Self { Self {
@ -323,7 +310,7 @@ impl<'s> Parser<'s> {
Identifier => self.attrs.insert("id", content.into()), Identifier => self.attrs.insert("id", content.into()),
Key => self.i_prev = self.attrs.insert_pos(content, "".into()), Key => self.i_prev = self.attrs.insert_pos(content, "".into()),
Value | ValueQuoted | ValueContinued => { Value | ValueQuoted | ValueContinued => {
self.attrs.0.as_mut().unwrap().to_mut()[self.i_prev] self.attrs.0.as_mut().unwrap()[self.i_prev]
.1 .1
.extend(&content[usize::from(matches!(st, ValueQuoted))..]); .extend(&content[usize::from(matches!(st, ValueQuoted))..]);
} }
@ -351,7 +338,6 @@ impl<'s> Parser<'s> {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg(feature = "parser")]
enum State { enum State {
Start, Start,
Whitespace, Whitespace,
@ -371,7 +357,6 @@ enum State {
Invalid, Invalid,
} }
#[cfg(feature = "parser")]
impl State { impl State {
fn step(self, c: u8) -> State { fn step(self, c: u8) -> State {
use State::*; use State::*;
@ -414,12 +399,11 @@ impl State {
} }
} }
#[cfg(feature = "parser")]
pub fn is_name(c: u8) -> bool { pub fn is_name(c: u8) -> bool {
c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'-') c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'-')
} }
#[cfg(all(test, feature = "parser"))] #[cfg(test)]
mod test { mod test {
macro_rules! test_attr { macro_rules! test_attr {
($src:expr $(,$($av:expr),* $(,)?)?) => { ($src:expr $(,$($av:expr),* $(,)?)?) => {

View file

@ -353,7 +353,7 @@ impl<'s> TreeParser<'s> {
span_end: Range<usize>, span_end: Range<usize>,
mut lines: &mut [Range<usize>], mut lines: &mut [Range<usize>],
) { ) {
if let Kind::Fenced { indent, spec, .. } = k { if let Kind::Fenced { indent, .. } = k {
for line in lines.iter_mut() { for line in lines.iter_mut() {
let indent_line = self.src.as_bytes()[line.clone()] let indent_line = self.src.as_bytes()[line.clone()]
.iter() .iter()
@ -361,14 +361,6 @@ impl<'s> TreeParser<'s> {
.count(); .count();
line.start += (*indent).min(indent_line); line.start += (*indent).min(indent_line);
} }
// trim ending whitespace of raw block
if spec.starts_with('=') {
let l = lines.len();
if l > 0 {
lines[l - 1] = self.trim_end(lines[l - 1].clone());
}
}
} else { } else {
// trim starting whitespace of each inline // trim starting whitespace of each inline
for line in lines.iter_mut() { for line in lines.iter_mut() {
@ -1019,22 +1011,21 @@ impl<'s> IdentifiedBlock<'s> {
let numbering = if first.is_ascii_digit() { let numbering = if first.is_ascii_digit() {
Decimal Decimal
} else if is_roman_lower_digit(first) {
RomanLower
} else if is_roman_upper_digit(first) {
RomanUpper
} else if first.is_ascii_lowercase() { } else if first.is_ascii_lowercase() {
AlphaLower AlphaLower
} else if first.is_ascii_uppercase() { } else if first.is_ascii_uppercase() {
AlphaUpper AlphaUpper
} else if is_roman_lower_digit(first) {
RomanLower
} else if is_roman_upper_digit(first) {
RomanUpper
} else { } else {
return None; return None;
}; };
let max_len = match numbering { let max_len = match numbering {
AlphaLower | AlphaUpper => 1,
Decimal => 19, Decimal => 19,
RomanLower | RomanUpper => 13, AlphaLower | AlphaUpper | RomanLower | RomanUpper => 13,
}; };
let chars_num = chars.clone(); let chars_num = chars.clone();
@ -1066,6 +1057,17 @@ impl<'s> IdentifiedBlock<'s> {
}; };
let len_style = usize::from(start_paren) + 1; let len_style = usize::from(start_paren) + 1;
let chars_num = std::iter::once(first).chain(chars_num.take(len_num - 1));
let numbering = if matches!(numbering, AlphaLower)
&& chars_num.clone().all(is_roman_lower_digit)
{
RomanLower
} else if matches!(numbering, AlphaUpper) && chars_num.clone().all(is_roman_upper_digit) {
RomanUpper
} else {
numbering
};
if chars.next().map_or(true, |c| c.is_ascii_whitespace()) { if chars.next().map_or(true, |c| c.is_ascii_whitespace()) {
Some((numbering, style, len_num + len_style)) Some((numbering, style, len_num + len_style))
} else { } else {
@ -3119,6 +3121,16 @@ mod test {
"I.", "I.",
1 1
); );
test_block!(
"IJ. abc\n",
Kind::ListItem {
indent: 0,
ty: Ordered(AlphaUpper, Period),
last_blankline: false,
},
"IJ.",
1
);
test_block!( test_block!(
"(a) abc\n", "(a) abc\n",
Kind::ListItem { Kind::ListItem {

View file

@ -295,13 +295,17 @@ impl<'s> Writer<'s> {
} }
match c { match c {
Container::Blockquote => out.write_str("</blockquote>")?, Container::Blockquote => out.write_str("</blockquote>")?,
Container::List { kind, .. } => { Container::List {
kind: ListKind::Unordered | ListKind::Task,
..
} => {
self.list_tightness.pop(); self.list_tightness.pop();
match kind { out.write_str("</ul>")?;
ListKind::Unordered | ListKind::Task => out.write_str("</ul>")?,
ListKind::Ordered { .. } => out.write_str("</ol>")?,
}
} }
Container::List {
kind: ListKind::Ordered { .. },
..
} => out.write_str("</ol>")?,
Container::ListItem | Container::TaskListItem { .. } => { Container::ListItem | Container::TaskListItem { .. } => {
out.write_str("</li>")?; out.write_str("</li>")?;
} }

View file

@ -732,36 +732,41 @@ impl<'s> Parser<'s> {
image, image,
} => { } => {
let span_spec = self.events[e_opener].span.end..self.input.span.start; let span_spec = self.events[e_opener].span.end..self.input.span.start;
let multiline_spec = let multiline =
self.events[e_opener].span.start < self.input.span_line.start; self.events[e_opener].span.start < self.input.span_line.start;
let spec: CowStr = if span_spec.is_empty() && !inline { let spec: CowStr = if span_spec.is_empty() && !inline {
let span_spec = self.events[event_span].span.end
..self.events[e_opener - 1].span.start;
let events_text = self let events_text = self
.events .events
.iter() .iter()
.skip(event_span + 1) .skip(event_span + 1)
.take(e_opener - event_span - 2); .take(e_opener - event_span - 2);
let mut spec = String::new(); if multiline
let mut span = 0..0; || events_text.clone().any(|ev| {
for ev in events_text.filter(|ev| { !matches!(ev.kind, EventKind::Str | EventKind::Atom(..))
matches!(ev.kind, EventKind::Str | EventKind::Atom(..)) })
&& !matches!(ev.kind, EventKind::Atom(Escape)) {
}) { let mut spec = String::new();
if matches!(ev.kind, EventKind::Atom(Softbreak | Hardbreak)) { let mut span = 0..0;
spec.push_str(&self.input.src[span.clone()]); for ev in events_text.filter(|ev| {
spec.push(' '); matches!(ev.kind, EventKind::Str | EventKind::Atom(..))
span = ev.span.end..ev.span.end; }) {
} else if span.end == ev.span.start { if span.end == ev.span.start {
span.end = ev.span.end; span.end = ev.span.end;
} else { } else {
spec.push_str(&self.input.src[span.clone()]); spec.push_str(&self.input.src[span.clone()]);
span = ev.span.clone(); span = ev.span.clone();
}
} }
spec.push_str(&self.input.src[span]);
spec.into()
} else {
self.input.src[span_spec].into()
} }
spec.push_str(&self.input.src[span]); } else if multiline {
spec.into()
} else if multiline_spec {
let mut spec = String::new(); let mut spec = String::new();
let mut first_part = true; let mut first_part = true;
let mut span = let mut span =

View file

@ -49,25 +49,19 @@
#![allow(clippy::blocks_in_if_conditions)] #![allow(clippy::blocks_in_if_conditions)]
use std::fmt; use std::fmt;
#[cfg(feature = "parser")]
use std::fmt::Write as FmtWrite; use std::fmt::Write as FmtWrite;
use std::io; use std::io;
#[cfg(feature = "parser")]
use std::ops::Range; use std::ops::Range;
#[cfg(feature = "html")] #[cfg(feature = "html")]
pub mod html; pub mod html;
mod attr; mod attr;
#[cfg(feature = "parser")]
mod block; mod block;
#[cfg(feature = "parser")]
mod inline; mod inline;
#[cfg(feature = "parser")]
mod lex; mod lex;
pub use attr::{AttributeValue, AttributeValueParts, Attributes}; pub use attr::{AttributeValue, AttributeValueParts, Attributes};
use databake::Bake;
type CowStr<'s> = std::borrow::Cow<'s, str>; type CowStr<'s> = std::borrow::Cow<'s, str>;
@ -203,8 +197,7 @@ impl<'s> AsRef<Event<'s>> for &Event<'s> {
/// multiple events. [`Container`] elements are represented by a [`Event::Start`] followed by /// multiple events. [`Container`] elements are represented by a [`Event::Start`] followed by
/// events representing its content, and finally a [`Event::End`]. Atomic elements without any /// events representing its content, and finally a [`Event::End`]. Atomic elements without any
/// inside elements are represented by a single event. /// inside elements are represented by a single event.
#[derive(Debug, Clone, PartialEq, Eq, Bake)] #[derive(Debug, Clone, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum Event<'s> { pub enum Event<'s> {
/// Start of a container. /// Start of a container.
Start(Container<'s>, Attributes<'s>), Start(Container<'s>, Attributes<'s>),
@ -251,8 +244,7 @@ pub enum Event<'s> {
/// - inline, may only contain inline elements, /// - inline, may only contain inline elements,
/// - block leaf, may only contain inline elements, /// - block leaf, may only contain inline elements,
/// - block container, may contain any block-level elements. /// - block container, may contain any block-level elements.
#[derive(Debug, Clone, PartialEq, Eq, Bake)] #[derive(Debug, Clone, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum Container<'s> { pub enum Container<'s> {
/// A blockquote element. /// A blockquote element.
Blockquote, Blockquote,
@ -406,8 +398,7 @@ impl<'s> Container<'s> {
} }
/// Alignment of a table column. /// Alignment of a table column.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Bake)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum Alignment { pub enum Alignment {
Unspecified, Unspecified,
Left, Left,
@ -416,8 +407,7 @@ pub enum Alignment {
} }
/// The type of an inline span link. /// The type of an inline span link.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Bake)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum SpanLinkType { pub enum SpanLinkType {
/// E.g. `[text](url)` /// E.g. `[text](url)`
Inline, Inline,
@ -428,8 +418,7 @@ pub enum SpanLinkType {
} }
/// The type of an inline link. /// The type of an inline link.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Bake)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum LinkType { pub enum LinkType {
/// E.g. `[text](url)`. /// E.g. `[text](url)`.
Span(SpanLinkType), Span(SpanLinkType),
@ -440,8 +429,7 @@ pub enum LinkType {
} }
/// The type of a list. /// The type of a list.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Bake)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum ListKind { pub enum ListKind {
/// A bullet list. /// A bullet list.
Unordered, Unordered,
@ -456,8 +444,7 @@ pub enum ListKind {
} }
/// Numbering type of an ordered list. /// Numbering type of an ordered list.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Bake)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum OrderedListNumbering { pub enum OrderedListNumbering {
/// Decimal numbering, e.g. `1)`. /// Decimal numbering, e.g. `1)`.
Decimal, Decimal,
@ -472,8 +459,7 @@ pub enum OrderedListNumbering {
} }
/// Style of an ordered list. /// Style of an ordered list.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Bake)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[databake(path = jotdown)]
pub enum OrderedListStyle { pub enum OrderedListStyle {
/// Number is followed by a period, e.g. `1.`. /// Number is followed by a period, e.g. `1.`.
Period, Period,
@ -484,7 +470,6 @@ pub enum OrderedListStyle {
} }
impl OrderedListNumbering { impl OrderedListNumbering {
#[cfg(feature = "parser")]
fn parse_number(self, n: &str) -> u64 { fn parse_number(self, n: &str) -> u64 {
match self { match self {
Self::Decimal => n.parse().unwrap(), Self::Decimal => n.parse().unwrap(),
@ -539,7 +524,6 @@ impl OrderedListNumbering {
} }
impl OrderedListStyle { impl OrderedListStyle {
#[cfg(feature = "parser")]
fn number(self, marker: &str) -> &str { fn number(self, marker: &str) -> &str {
&marker[usize::from(matches!(self, Self::ParenParen))..marker.len() - 1] &marker[usize::from(matches!(self, Self::ParenParen))..marker.len() - 1]
} }
@ -550,9 +534,9 @@ type Map<K, V> = std::collections::HashMap<K, V>;
#[cfg(feature = "deterministic")] #[cfg(feature = "deterministic")]
type Map<K, V> = std::collections::BTreeMap<K, V>; type Map<K, V> = std::collections::BTreeMap<K, V>;
#[cfg(all(not(feature = "deterministic"), feature = "parser"))] #[cfg(not(feature = "deterministic"))]
type Set<T> = std::collections::HashSet<T>; type Set<T> = std::collections::HashSet<T>;
#[cfg(all(feature = "deterministic", feature = "parser"))] #[cfg(feature = "deterministic")]
type Set<T> = std::collections::BTreeSet<T>; type Set<T> = std::collections::BTreeSet<T>;
/// A parser that generates [`Event`]s from a Djot document. /// A parser that generates [`Event`]s from a Djot document.
@ -564,7 +548,6 @@ type Set<T> = std::collections::BTreeSet<T>;
/// ///
/// It is possible to clone the parser to e.g. avoid performing the block parsing multiple times. /// It is possible to clone the parser to e.g. avoid performing the block parsing multiple times.
#[derive(Clone)] #[derive(Clone)]
#[cfg(feature = "parser")]
pub struct Parser<'s> { pub struct Parser<'s> {
src: &'s str, src: &'s str,
@ -589,7 +572,6 @@ pub struct Parser<'s> {
} }
#[derive(Clone)] #[derive(Clone)]
#[cfg(feature = "parser")]
struct Heading { struct Heading {
/// Location of heading in src. /// Location of heading in src.
location: u32, location: u32,
@ -603,7 +585,6 @@ struct Heading {
/// Because of potential future references, an initial pass is required to obtain all definitions. /// Because of potential future references, an initial pass is required to obtain all definitions.
#[derive(Clone)] #[derive(Clone)]
#[cfg(feature = "parser")]
struct PrePass<'s> { struct PrePass<'s> {
/// Link definitions and their attributes. /// Link definitions and their attributes.
link_definitions: Map<&'s str, (CowStr<'s>, attr::Attributes<'s>)>, link_definitions: Map<&'s str, (CowStr<'s>, attr::Attributes<'s>)>,
@ -613,61 +594,53 @@ struct PrePass<'s> {
headings_lex: Vec<usize>, headings_lex: Vec<usize>,
} }
#[cfg(feature = "parser")]
impl<'s> PrePass<'s> { impl<'s> PrePass<'s> {
#[must_use] #[must_use]
fn new( fn new(
src: &'s str, src: &'s str,
mut blocks: std::slice::Iter<block::Event<'s>>, blocks: std::slice::Iter<block::Event<'s>>,
inline_parser: &mut inline::Parser<'s>, inline_parser: &mut inline::Parser<'s>,
) -> Self { ) -> Self {
let mut link_definitions = Map::new(); let mut link_definitions = Map::new();
let mut headings: Vec<Heading> = Vec::new(); let mut headings: Vec<Heading> = Vec::new();
let mut used_ids: Set<String> = Set::new(); let mut used_ids: Set<String> = Set::new();
let mut blocks = blocks.peekable();
let mut attr_prev: Option<Range<usize>> = None; let mut attr_prev: Option<Range<usize>> = None;
while let Some(e) = blocks.next() { while let Some(e) = blocks.next() {
match e.kind { match e.kind {
block::EventKind::Enter(block::Node::Leaf(block::Leaf::LinkDefinition { block::EventKind::Enter(block::Node::Leaf(block::Leaf::LinkDefinition {
label, label,
})) => { })) => {
fn next_is_inline(
bs: &mut std::iter::Peekable<std::slice::Iter<block::Event>>,
) -> bool {
matches!(bs.peek().map(|e| &e.kind), Some(block::EventKind::Inline))
}
// All link definition tags have to be obtained initially, as references can // All link definition tags have to be obtained initially, as references can
// appear before the definition. // appear before the definition.
let attrs = attr_prev let attrs = attr_prev
.as_ref() .as_ref()
.map_or_else(Attributes::new, |sp| attr::parse(&src[sp.clone()])); .map_or_else(Attributes::new, |sp| attr::parse(&src[sp.clone()]));
let url = if let Some(block::Event { let url = if !next_is_inline(&mut blocks) {
kind: block::EventKind::Inline, "".into()
span, } else {
}) = blocks.next() let start = src[blocks.next().as_ref().unwrap().span.clone()]
{ .trim_matches(|c: char| c.is_ascii_whitespace());
let start = if !next_is_inline(&mut blocks) {
src[span.clone()].trim_matches(|c: char| c.is_ascii_whitespace()); start.into()
if let Some(block::Event { } else {
kind: block::EventKind::Inline,
span,
}) = blocks.next()
{
let mut url = start.to_string(); let mut url = start.to_string();
url.push_str( while next_is_inline(&mut blocks) {
src[span.clone()].trim_matches(|c: char| c.is_ascii_whitespace()),
);
while let Some(block::Event {
kind: block::EventKind::Inline,
span,
}) = blocks.next()
{
url.push_str( url.push_str(
src[span.clone()] src[blocks.next().as_ref().unwrap().span.clone()]
.trim_matches(|c: char| c.is_ascii_whitespace()), .trim_matches(|c: char| c.is_ascii_whitespace()),
); );
} }
url.into() // owned url.into()
} else {
start.into() // borrowed
} }
} else {
"".into() // static
}; };
link_definitions.insert(label, (url, attrs)); link_definitions.insert(label, (url, attrs));
} }
@ -802,7 +775,6 @@ impl<'s> PrePass<'s> {
} }
} }
#[cfg(feature = "parser")]
impl<'s> Parser<'s> { impl<'s> Parser<'s> {
#[must_use] #[must_use]
pub fn new(src: &'s str) -> Self { pub fn new(src: &'s str) -> Self {
@ -1186,7 +1158,6 @@ impl<'s> Parser<'s> {
} }
} }
#[cfg(feature = "parser")]
impl<'s> Iterator for Parser<'s> { impl<'s> Iterator for Parser<'s> {
type Item = Event<'s>; type Item = Event<'s>;
@ -1199,12 +1170,10 @@ impl<'s> Iterator for Parser<'s> {
/// event within the input. /// event within the input.
/// ///
/// See the documentation of [`Parser::into_offset_iter`] for more information. /// See the documentation of [`Parser::into_offset_iter`] for more information.
#[cfg(feature = "parser")]
pub struct OffsetIter<'s> { pub struct OffsetIter<'s> {
parser: Parser<'s>, parser: Parser<'s>,
} }
#[cfg(feature = "parser")]
impl<'s> Iterator for OffsetIter<'s> { impl<'s> Iterator for OffsetIter<'s> {
type Item = (Event<'s>, Range<usize>); type Item = (Event<'s>, Range<usize>);
@ -1214,7 +1183,6 @@ impl<'s> Iterator for OffsetIter<'s> {
} }
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "parser")]
mod test { mod test {
use super::Attributes; use super::Attributes;
use super::Container::*; use super::Container::*;
@ -1559,39 +1527,7 @@ mod test {
test_parse!( test_parse!(
"``` =html\n<table>\n```", "``` =html\n<table>\n```",
Start(RawBlock { format: "html" }, Attributes::new()), Start(RawBlock { format: "html" }, Attributes::new()),
Str("<table>".into()), Str("<table>\n".into()),
End(RawBlock { format: "html" }),
);
}
#[test]
fn raw_block_whitespace() {
test_parse!(
concat!(
"```=html\n", //
"<tag1>\n", //
"<tag2>\n", //
"```\n", //
"\n", //
"paragraph\n", //
"\n", //
"```=html\n", //
"</tag2>\n", //
"</tag1>\n", //
"```\n", //
),
Start(RawBlock { format: "html" }, Attributes::new()),
Str("<tag1>\n".into()),
Str("<tag2>".into()),
End(RawBlock { format: "html" }),
Blankline,
Start(Paragraph, Attributes::new()),
Str("paragraph".into()),
End(Paragraph),
Blankline,
Start(RawBlock { format: "html" }, Attributes::new()),
Str("</tag2>\n".into()),
Str("</tag1>".into()),
End(RawBlock { format: "html" }), End(RawBlock { format: "html" }),
); );
} }
@ -1754,46 +1690,6 @@ mod test {
); );
} }
#[test]
fn link_reference_multiline_empty() {
test_parse!(
concat!(
"> [a\n", //
"> b][]\n", //
"> [a\\\n", //
"> b][]\n", //
"\n", //
"[a b]: url\n", //
),
Start(Blockquote, Attributes::new()),
Start(Paragraph, Attributes::new()),
Start(
Link("url".into(), LinkType::Span(SpanLinkType::Reference)),
Attributes::new()
),
Str("a".into()),
Softbreak,
Str("b".into()),
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
Softbreak,
Start(
Link("url".into(), LinkType::Span(SpanLinkType::Reference)),
Attributes::new()
),
Str("a".into()),
Escape,
Hardbreak,
Str("b".into()),
End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))),
End(Paragraph),
End(Blockquote),
Blankline,
Start(LinkDefinition { label: "a b" }, Attributes::new()),
Str("url".into()),
End(LinkDefinition { label: "a b" }),
);
}
#[test] #[test]
fn link_definition_multiline() { fn link_definition_multiline() {
test_parse!( test_parse!(

View file

@ -1 +0,0 @@

View file

@ -1,66 +0,0 @@
use crate::compare;
// Footnote references may appear within a footnote.
#[test]
fn test_1c8325a() {
let src = r##"[^a]
[^a]: a[^b][^c]
[^b]: b
"##;
let expected = r##"<p><a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<section role="doc-endnotes">
<hr>
<ol>
<li id="fn1">
<p>a<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a><a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a><a href="#fnref1" role="doc-backlink"></a></p>
</li>
<li id="fn2">
<p>b<a href="#fnref2" role="doc-backlink"></a></p>
</li>
<li id="fn3">
<p><a href="#fnref3" role="doc-backlink"></a></p>
</li>
</ol>
</section>
"##;
compare!(src, expected);
}
// Footnote references in unreferenced footnotes are ignored.
#[test]
fn test_9eab5c8() {
let src = r##"para
[^a]: a[^b][^c]
[^b]: b
"##;
let expected = r##"<p>para</p>
"##;
compare!(src, expected);
}
// Footnotes may appear within footnotes.
#[test]
fn test_041f54c() {
let src = r##"[^b]
[^a]
[^a]: [^b]: inner
"##;
let expected = r##"<p><a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>
<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a></p>
<section role="doc-endnotes">
<hr>
<ol>
<li id="fn1">
<p>inner<a href="#fnref1" role="doc-backlink"></a></p>
</li>
<li id="fn2">
<p><a href="#fnref2" role="doc-backlink"></a></p>
</li>
</ol>
</section>
"##;
compare!(src, expected);
}

View file

@ -1,27 +0,0 @@
use crate::compare;
#[test]
fn test_fefa2dc() {
let src = r##"1. item
para
"##;
let expected = r##"<ol>
<li>
item
</li>
</ol>
<p>para</p>
"##;
compare!(src, expected);
}
// Only single letter alphabetic list markers.
#[test]
fn test_2a0aa95() {
let src = r##"word. Continuing paragraph.
"##;
let expected = r##"<p>word. Continuing paragraph.</p>
"##;
compare!(src, expected);
}

View file

@ -1,20 +0,0 @@
```
1. item
para
.
<ol>
<li>
item
</li>
</ol>
<p>para</p>
```
Only single letter alphabetic list markers.
```
word. Continuing paragraph.
.
<p>word. Continuing paragraph.</p>
```

View file

@ -1,3 +0,0 @@
mod footnotes;
mod lists;
mod raw_blocks;

View file

@ -1,24 +0,0 @@
use crate::compare;
#[test]
fn test_bf9dbab() {
let src = r##"```=html
<tag1>
<tag2>
```
paragraph
```=html
</tag2>
</tag1>
```
"##;
let expected = r##"<tag1>
<tag2>
<p>paragraph</p>
</tag2>
</tag1>
"##;
compare!(src, expected);
}

View file

@ -1,19 +0,0 @@
````
```=html
<tag1>
<tag2>
```
paragraph
```=html
</tag2>
</tag1>
```
.
<tag1>
<tag2>
<p>paragraph</p>
</tag2>
</tag1>
````