Skip to content

feat: adds footnote support #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: v0.10
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions development.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
```shell
cargo fmt -- --check
cargo test --all-features
cargo clippy --allow-dirty --allow-staged
cargo clippy --fix --lib -p orgize --allow-dirty --allow-staged
```

## Update snapshot testing
Expand All @@ -18,8 +18,8 @@ cargo insta review

```shell
cargo install cargo-fuzz
rustup default nightly
cargo fuzz run fuzz_target_1
rustup toolchain install nightly
cargo +nightly fuzz run fuzz_target_1
```

## Benchmark
Expand Down
11 changes: 11 additions & 0 deletions src/ast/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ const nodes = [
{
struct: "FnDef",
kind: ["FN_DEF"],
token: [
["label", "FN_LABEL"],
["description", "FN_CONTENT"],
],
post_blank: true,
affiliated_keywords: true,
},
{
struct: "FnContent",
kind: ["FN_CONTENT"],
post_blank: true,
affiliated_keywords: true,
},
Expand Down Expand Up @@ -188,6 +198,7 @@ const nodes = [
{
struct: "FnRef",
kind: ["FN_REF"],
token: [["label", "FN_LABEL"]],
},
{
struct: "Macros",
Expand Down
67 changes: 67 additions & 0 deletions src/ast/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,70 @@ impl AstNode for FnDef {
}
}
impl FnDef {
/// Beginning position of this element
pub fn start(&self) -> TextSize {
self.syntax.text_range().start()
}
/// Ending position of this element
pub fn end(&self) -> TextSize {
self.syntax.text_range().end()
}
/// Range of this element
pub fn text_range(&self) -> TextRange {
self.syntax.text_range()
}
/// Raw text of this element
pub fn raw(&self) -> String {
self.syntax.to_string()
}
pub fn label(&self) -> Option<super::Token> {
super::token(&self.syntax, FN_LABEL)
}
pub fn description(&self) -> Option<super::Token> {
super::token(&self.syntax, FN_CONTENT)
}
pub fn post_blank(&self) -> usize {
super::blank_lines(&self.syntax)
}
pub fn caption(&self) -> Option<AffiliatedKeyword> {
affiliated_keyword(&self.syntax, |k| k == "CAPTION")
}
pub fn header(&self) -> Option<AffiliatedKeyword> {
affiliated_keyword(&self.syntax, |k| k == "HEADER")
}
pub fn name(&self) -> Option<AffiliatedKeyword> {
affiliated_keyword(&self.syntax, |k| k == "NAME")
}
pub fn plot(&self) -> Option<AffiliatedKeyword> {
affiliated_keyword(&self.syntax, |k| k == "PLOT")
}
pub fn results(&self) -> Option<AffiliatedKeyword> {
affiliated_keyword(&self.syntax, |k| k == "RESULTS")
}
pub fn attr(&self, backend: &str) -> Option<AffiliatedKeyword> {
affiliated_keyword(&self.syntax, |k| {
k.starts_with("ATTR_") && &k[5..] == backend
})
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FnContent {
pub(crate) syntax: SyntaxNode,
}
impl AstNode for FnContent {
type Language = OrgLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == FN_CONTENT
}
fn cast(node: SyntaxNode) -> Option<FnContent> {
Self::can_cast(node.kind()).then(|| FnContent { syntax: node })
}
fn syntax(&self) -> &SyntaxNode {
&self.syntax
}
}
impl FnContent {
/// Beginning position of this element
pub fn start(&self) -> TextSize {
self.syntax.text_range().start()
Expand Down Expand Up @@ -1680,6 +1744,9 @@ impl FnRef {
pub fn raw(&self) -> String {
self.syntax.to_string()
}
pub fn label(&self) -> Option<super::Token> {
super::token(&self.syntax, FN_LABEL)
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Expand Down
2 changes: 2 additions & 0 deletions src/export/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub enum Container {
DynBlock(DynBlock),

FnDef(FnDef),
FnContent(FnContent),
Comment(Comment),
FixedWidth(FixedWidth),
SpecialBlock(SpecialBlock),
Expand Down Expand Up @@ -57,6 +58,7 @@ pub enum Event {
Text(Token),
Macros(Macros),
Cookie(Cookie),
FnLabel(Token),
InlineCall(InlineCall),
InlineSrc(InlineSrc),
Clock(Clock),
Expand Down
56 changes: 56 additions & 0 deletions src/export/html.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use rowan::ast::AstNode;
use rowan::NodeOrToken;
use std::cmp::min;
use std::fmt;
Expand All @@ -6,6 +7,7 @@ use std::fmt::Write as _;
use super::event::{Container, Event};
use super::TraversalContext;
use super::Traverser;
use crate::ast::token;
use crate::{SyntaxElement, SyntaxKind, SyntaxNode};

/// A wrapper for escaping sensitive characters in html.
Expand Down Expand Up @@ -51,6 +53,9 @@ impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
pub struct HtmlExport {
output: String,

///TODO: track footnotes and citations within the export struct and
/// construct them after the document is fully parsed?
//footnotes: HashMap<String, String>,
in_descriptive_list: Vec<bool>,

table_row: TableRow,
Expand Down Expand Up @@ -107,6 +112,55 @@ impl Traverser for HtmlExport {
}
Event::Leave(Container::Headline(_)) => {}

Event::Enter(Container::FnRef(t)) => {
if let Some(label) = t.label() {
let _ = write!(
&mut self.output,
"<a href=\"#footnote_{}\" class=\"footnote-reference\">[{}]",
label.syntax().text(),
label.syntax().text()
);
}
self.output += "</a>";
}
Event::Leave(Container::FnRef(_)) => {}

Event::Enter(Container::FnDef(t)) => {
self.output += "<aside ";
self.output += r#"class="footnote-definition" "#;
self.output += ">";

if let Some(label) = t.label() {
self.output += "<a ";
let _ = write!(
&mut self.output,
"href=\"#footnote_{}\" ",
label.syntax().text()
);
self.output += "class=\"footnote-reference\" ";
self.output += ">";
let _ = write!(&mut self.output, "[{}]", label.syntax().text());
self.output += "</a>";
}
}
Event::Leave(Container::FnDef(_)) => {
self.output += "</aside>";
}

Event::Enter(Container::FnContent(c)) => {
self.output += "<span class=\"footnote-content\" ";
if let Some(parent) = c.syntax().parent() {
if parent.kind() == SyntaxKind::FN_REF || parent.kind() == SyntaxKind::FN_DEF {
let label = token(&parent, SyntaxKind::FN_LABEL).unwrap();
let _ = write!(&mut self.output, "id=\"footnote_{}\" ", label);
}
}
self.output += ">";
}
Event::Leave(Container::FnContent(_)) => {
self.output += "</span>";
}

Event::Enter(Container::Paragraph(_)) => self.output += "<p>",
Event::Leave(Container::Paragraph(_)) => self.output += "</p>",

Expand Down Expand Up @@ -297,6 +351,8 @@ impl Traverser for HtmlExport {
let _ = write!(&mut self.output, "{}", HtmlEscape(text));
}

Event::FnLabel(_) => {}

Event::LineBreak(_) => self.output += "<br/>",

Event::Snippet(snippet) => {
Expand Down
12 changes: 9 additions & 3 deletions src/export/traverse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ pub trait Traverser {
DYN_BLOCK => walk!(DynBlock),
FN_DEF => walk!(FnDef),
FN_REF => walk!(FnRef),
FN_CONTENT => walk!(FnContent),
MACROS => walk!(@Macros),
SNIPPET => walk!(@Snippet),
TIMESTAMP_ACTIVE | TIMESTAMP_INACTIVE | TIMESTAMP_DIARY => walk!(@Timestamp),
Expand Down Expand Up @@ -210,12 +211,17 @@ pub trait Traverser {
_ => {}
}
}
SyntaxElement::Token(token) => {
if token.kind() == TEXT {
SyntaxElement::Token(token) => match token.kind() {
TEXT => {
self.event(Event::Text(Token(token)), ctx);
take_control!();
}
}
FN_LABEL => {
self.event(Event::FnLabel(Token(token)), ctx);
take_control!();
}
_ => {}
},
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/replace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl Org {
) if level <= new_level
// non-last headline must ends with a newline
&& (headline.end() == self.document().end()
|| replace_with.ends_with(&['\n', '\r'])) =>
|| replace_with.ends_with(['\n', '\r'])) =>
{
self.replace_headline(headline, range, replace_with)
}
Expand Down
7 changes: 4 additions & 3 deletions src/syntax/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,12 @@ fn affiliated_keywords() {
TEXT@10..25 " a footnote def"
NEW_LINE@25..26 "\n"
L_BRACKET@26..27 "["
TEXT@27..29 "fn"
KEYWORD@27..29 "fn"
COLON@29..30 ":"
TEXT@30..34 "WORD"
FN_LABEL@30..34 "WORD"
R_BRACKET@34..35 "]"
TEXT@35..55 " https://orgmode.org"
FN_CONTENT@35..55
TEXT@35..55 " https://orgmode.org"
"###
);

Expand Down
Loading