Skip to content

Commit

Permalink
Emphasis node (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lurk authored Feb 9, 2025
1 parent cce6ab7 commit 5f0c9e6
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 67 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "yamd"
description = "Yet Another Markdown Document (flavour)"
version = "0.15.0"
version = "0.16.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/Lurk/yamd"
Expand Down
28 changes: 28 additions & 0 deletions src/nodes/emphasis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use serde::Serialize;

/// # Emphasis
///
/// Any token except [Terminator](type@crate::lexer::TokenKind::Terminator) surrounded by
/// [Start](type@crate::lexer::TokenKind::Star).
///
/// Example:
///
/// ```text
/// *Emphasis can contain any token
/// even EOL*
/// ```
///
/// HTML equivalent:
///
/// ```html
/// <em>Emphasis can contain any token
/// even EOL</em>
/// ```
#[derive(Debug, PartialEq, Serialize, Clone, Eq)]
pub struct Emphasis(pub String);

impl Emphasis {
pub fn new<Body: Into<String>>(body: Body) -> Self {
Emphasis(body.into())
}
}
2 changes: 2 additions & 0 deletions src/nodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod code;
mod code_span;
mod collapsible;
mod embed;
mod emphasis;
mod heading;
mod highlight;
mod image;
Expand All @@ -22,6 +23,7 @@ pub use code::Code;
pub use code_span::CodeSpan;
pub use collapsible::Collapsible;
pub use embed::Embed;
pub use emphasis::Emphasis;
pub use heading::{Heading, HeadingNodes};
pub use highlight::Highlight;
pub use image::Image;
Expand Down
13 changes: 11 additions & 2 deletions src/nodes/paragraph.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::Serialize;

use super::{Anchor, Bold, CodeSpan, Italic, Strikethrough};
use super::{Anchor, Bold, CodeSpan, Emphasis, Italic, Strikethrough};

#[derive(Debug, PartialEq, Serialize, Clone, Eq)]
#[serde(tag = "type", content = "value")]
Expand All @@ -11,6 +11,7 @@ pub enum ParagraphNodes {
Strikethrough(Strikethrough),
Text(String),
CodeSpan(CodeSpan),
Emphasis(Emphasis),
}

impl From<Anchor> for ParagraphNodes {
Expand Down Expand Up @@ -49,6 +50,12 @@ impl From<CodeSpan> for ParagraphNodes {
}
}

impl From<Emphasis> for ParagraphNodes {
fn from(value: Emphasis) -> Self {
ParagraphNodes::Emphasis(value)
}
}

/// # Paragraph
///
/// Any token until [Terminator](type@crate::lexer::TokenKind::Terminator) or end of input.
Expand All @@ -60,13 +67,14 @@ impl From<CodeSpan> for ParagraphNodes {
/// - [Bold]
/// - [Italic]
/// - [Strikethrough]
/// - [Emphasis]
/// - [String]
///
/// Example:
///
/// ```text
/// Paragraph can contain an [anchor](#), a `code span`, and **bold**, or _italic_, or ~~strikethrough~~, or
/// regular text.
/// *emphasis*, or regular text.
/// ```
///
/// HTML equivalent:
Expand All @@ -83,6 +91,7 @@ impl From<CodeSpan> for ParagraphNodes {
/// <i>italic</i>
/// , or
/// <s>strikethrough</s>
/// , or <em>emphasis</em>
/// , or regular text.
/// </p>
/// ```
Expand Down
44 changes: 44 additions & 0 deletions src/parser/emphasis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use crate::{lexer::TokenKind, nodes::Emphasis};

use super::Parser;

pub(crate) fn emphasis(p: &mut Parser) -> Option<Emphasis> {
p.advance_or_backtrack(|t| t.kind == TokenKind::Star && t.slice.len() == 1)
.map(|(start, end)| Emphasis::new(p.range_to_string(start + 1..end)))
}

#[cfg(test)]
mod tests {

use crate::{
lexer::{Position, Token, TokenKind},
nodes::Emphasis,
parser::{emphasis, Parser},
};

#[test]
fn happy_path() {
let mut p = Parser::new("*happy*");
assert_eq!(emphasis(&mut p), Some(Emphasis::new("happy")));
}

#[test]
fn no_closing_token() {
let mut p = Parser::new("*happy");
assert_eq!(emphasis(&mut p), None);
assert_eq!(
p.peek(),
Some((&Token::new(TokenKind::Literal, "*", Position::default()), 0))
)
}

#[test]
fn terminator() {
let mut p = Parser::new("*ha\n\nppy*");
assert_eq!(emphasis(&mut p), None);
assert_eq!(
p.peek(),
Some((&Token::new(TokenKind::Literal, "*", Position::default()), 0))
);
}
}
2 changes: 2 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod code;
mod code_span;
mod collapsible;
mod embed;
mod emphasis;
mod heading;
mod highlight;
mod image;
Expand All @@ -26,6 +27,7 @@ pub(crate) use code::code;
pub(crate) use code_span::code_span;
pub(crate) use collapsible::collapsible;
pub(crate) use embed::embed;
pub(crate) use emphasis::emphasis;
pub(crate) use heading::heading;
pub(crate) use highlight::highlight;
pub(crate) use image::images;
Expand Down
121 changes: 57 additions & 64 deletions src/parser/paragraph.rs
Original file line number Diff line number Diff line change
@@ -1,90 +1,79 @@
use crate::{
lexer::{Token, TokenKind},
nodes::Paragraph,
nodes::{Paragraph, ParagraphNodes},
};

use super::{anchor, bold, code_span, italic, strikethrough, Parser};
use super::{anchor, bold, code_span, emphasis, italic, strikethrough, Parser};

#[derive(Default)]
struct ParagraphBuilder {
nodes: Vec<ParagraphNodes>,
text_start: Option<usize>,
}

impl ParagraphBuilder {
fn push<N: Into<ParagraphNodes>>(&mut self, n: Option<N>, p: &Parser, pos: usize) {
if let Some(n) = n {
self.consume_text(p, pos);
self.nodes.push(n.into());
}
}

fn start_text(&mut self, pos: usize) {
self.text_start.get_or_insert(pos);
}

#[inline]
fn consume_text(&mut self, p: &Parser, end: usize) {
if let Some(start) = self.text_start.take() {
self.nodes.push(p.range_to_string(start..end).into());
}
}

fn clear_text_if_shorter_than(&mut self, pos: usize, size: usize) {
self.text_start.take_if(|start| pos - *start < size);
}

fn build(self) -> Option<Paragraph> {
if self.nodes.is_empty() {
return None;
}
Some(Paragraph::new(self.nodes))
}
}

pub(crate) fn paragraph<Callback>(p: &mut Parser<'_>, new_line_check: Callback) -> Option<Paragraph>
where
Callback: Fn(&Token) -> bool,
{
let start = p.pos();
let mut paragraph = Paragraph::default();
let mut text_start: Option<usize> = None;
let mut bulder = ParagraphBuilder::default();
let mut end_modifier = 0;

while let Some((t, pos)) = p.peek() {
match t.kind {
TokenKind::Terminator => break,
TokenKind::Star if t.slice.len() == 2 => {
if let Some(n) = bold(p) {
if let Some(start) = text_start.take() {
paragraph.body.push(p.range_to_string(start..pos).into());
}

paragraph.body.push(n.into());
}
}
TokenKind::Underscore if t.slice.len() == 1 => {
if let Some(n) = italic(p) {
if let Some(start) = text_start.take() {
paragraph.body.push(p.range_to_string(start..pos).into());
}

paragraph.body.push(n.into());
}
}
TokenKind::Tilde if t.slice.len() == 2 => {
if let Some(n) = strikethrough(p) {
if let Some(start) = text_start.take() {
paragraph.body.push(p.range_to_string(start..pos).into());
}

paragraph.body.push(n.into());
}
}
TokenKind::LeftSquareBracket => {
if let Some(n) = anchor(p) {
if let Some(start) = text_start.take() {
paragraph.body.push(p.range_to_string(start..pos).into());
}

paragraph.body.push(n.into());
}
}
TokenKind::Backtick if t.slice.len() == 1 => {
if let Some(n) = code_span(p) {
if let Some(start) = text_start.take() {
paragraph.body.push(p.range_to_string(start..pos).into());
}

paragraph.body.push(n.into());
}
}
TokenKind::Star if t.slice.len() == 2 => bulder.push(bold(p), p, pos),
TokenKind::Star if t.slice.len() == 1 => bulder.push(emphasis(p), p, pos),
TokenKind::Underscore if t.slice.len() == 1 => bulder.push(italic(p), p, pos),
TokenKind::Tilde if t.slice.len() == 2 => bulder.push(strikethrough(p), p, pos),
TokenKind::LeftSquareBracket => bulder.push(anchor(p), p, pos),
TokenKind::Backtick if t.slice.len() == 1 => bulder.push(code_span(p), p, pos),
_ if pos != start && t.position.column == 0 && new_line_check(t) => {
end_modifier = 1;
text_start.take_if(|start| pos - *start < 2);
bulder.clear_text_if_shorter_than(pos, 2);
break;
}
_ => {
text_start.get_or_insert(pos);
bulder.start_text(pos);
p.next_token();
}
}
}

if let Some(start) = text_start.take() {
paragraph
.body
.push(p.range_to_string(start..p.pos() - end_modifier).into());
}
bulder.consume_text(p, p.pos() - end_modifier);

if end_modifier != 0 && paragraph.body.is_empty() {
return None;
}

Some(paragraph)
bulder.build()
}

#[cfg(test)]
Expand All @@ -93,13 +82,13 @@ mod tests {

use crate::{
lexer::{Position, Token, TokenKind},
nodes::{Anchor, Bold, CodeSpan, Italic, Paragraph, Strikethrough},
nodes::{Anchor, Bold, CodeSpan, Emphasis, Italic, Paragraph, Strikethrough},
parser::{paragraph, Parser},
};

#[test]
pub fn terminated() {
let mut p = Parser::new("**b** _i_ ~~s~~ [a](u) `c` \n\n");
let mut p = Parser::new("**b** _i_ ~~s~~ [a](u) `c` *e* \n\n");
assert_eq!(
paragraph(&mut p, |_| false),
Some(Paragraph::new(vec![
Expand All @@ -113,13 +102,15 @@ mod tests {
String::from(" ").into(),
CodeSpan::new("c").into(),
String::from(" ").into(),
Emphasis::new("e").into(),
String::from(" ").into()
]))
)
}

#[test]
pub fn unterminated() {
let mut p = Parser::new("_i_ ~~s~~ **b**[a](u) `c` ");
let mut p = Parser::new("_i_ ~~s~~ **b**[a](u) `c` *e* ");
assert_eq!(
paragraph(&mut p, |_| false),
Some(Paragraph::new(vec![
Expand All @@ -132,6 +123,8 @@ mod tests {
String::from(" ").into(),
CodeSpan::new("c").into(),
String::from(" ").into(),
Emphasis::new("e").into(),
String::from(" ").into()
]))
)
}
Expand Down

0 comments on commit 5f0c9e6

Please sign in to comment.