]> OzVa Git service - gn-parser/commitdiff
Added proper TOC for HTML and gem
authorMax Value <greenwoodw50@gmail.com>
Fri, 9 May 2025 00:04:09 +0000 (01:04 +0100)
committerMax Value <greenwoodw50@gmail.com>
Fri, 9 May 2025 00:04:09 +0000 (01:04 +0100)
Renamed schema to goodnight

goodnight.gn [new file with mode: 0644]
schema.gn [deleted file]
src/parser.rs
src/types/contents.rs
src/types/document.rs
src/types/heading.rs
src/types/reference_list.rs

diff --git a/goodnight.gn b/goodnight.gn
new file mode 100644 (file)
index 0000000..dec3edf
--- /dev/null
@@ -0,0 +1,126 @@
+---
+title: The Goodnight Markdown Specification - Version 0.1.0
+author: Goodnight Publishing
+style: default
+---
+
+[META]
+
+[TOC]
+
+# Features
+
+Along with the features below, some features where removed:
+
+- Line breaks have been removed.
+- Horizontal rules have been removed.
+
+I don't belive in line breaks and horizontal rules are the responsibility of the stylesheet.
+
+## Headings
+
+Headings are prefixed with the `#` hastag character as with standard markdown. There must not be a space before the `#` character. A space may or may not be put after the `#`. Inline formatting will be properly parsed. Inline references will be properly parsed.
+
+## Links
+
+Still to be implemented
+
+## Email addresses
+
+Still to be implemented
+
+## Paragraphs
+
+`text about paragraphs`
+
+## Images
+
+Still to be implemented
+
+## Inline code
+
+Inline code blocks can be used with the `\`` backtick character.
+
+## Code blocks
+
+Code blocks can be used with 3 backtick        characters: `\`\`\``. This sequence must start on the first character on a new line.
+
+## Block quotes
+
+Block quotes are started with the tab character `\t`. They can contain any other elements.
+
+       Here is an example of a blockquote.
+
+       - Here is a list element in a blockquote
+
+       Tabbed lines with a space between them are joined into one single blockquote. If two different blockquotes are needed next to each other, a backslash at the first character of a newline can split the two up.
+
+## Inline formatting
+
+- Bold styling will work only with a single asterisk character.
+- Italic styling will work only with a single underscore character.
+
+## Lists
+
+- Unordered lists will work only with the hyphen character.
+- Ordered lists will work only with the period character.
+
+## Macros
+
+To generate the title, table of contents and bibliography, the following macros can be used:
+
+- `[TITLE]` generates the title block
+- `[TOC]` generates the table of contents
+- `[META]` generates the bibliography
+
+## Metadata
+
+The metadata format used is the YAML format. To generate the title, the standard LaTeX keys of "title", "author" and "date", should be used. If "style" is used, the parser will introduce the stylesheet with the relevant extension. For example, `style: stylesheet` will introduce `stylesheet.css` for html and `stylesheet.sty` for LaTeX.
+
+```
+---
+title: The Goodnight Markdown Specification
+author: William Greenwood
+---
+```
+
+### This is a test of the heading levels
+
+#### Some further testing
+
+## Referencing
+
+All referencing in Goodnight is APA 7th [lindsey2015]. It can be done similar to referenced links with a syntax similar to YAML metadata:
+
+```
+"Placeholder quote" [1]
+
+\[1]
+title: Placeholder title
+author: Placeholder name
+```
+
+If one of the keys is placed in square brackets after the in-text reference, it will be printed. If only the reference id is used, the full in-text reference will be used. If something that is not one of the keys is placed in square brackets within the full refrence, for example a page number.
+
+The keys are as follows:
+
+. `author`. The authors or entities behind the work seperated by the word "and". Wrap entity names in curly brackets
+. `title`
+. `year` (optional)
+. `publisher` (optional)
+. `url` (optional)
+. `journaltitle`: Title of the book or journal the work is in (optional)
+. `issuetitle`: Issue title of the book or journal the work is in (optional, must be used with `journaltitle`)
+. `editor`: Author of the book or journal the work is in (optional, must be used with `journaltitle`)
+. `volume`: Volume of the book or journal the work is in (optional, must be used with `journaltitle`)
+
+[REFS]
+
+[lindsey2015]
+author: Marley-Vincent Lindsey
+editor: William Value
+title: The Politics of Pokémon. Socialized Gaming, Religious Themes and the Construction of Communal Narratives
+journaltitle: Heidelberg Journal of Religions on the Internet
+issuetitle: Religion in Digital Games Reloaded. Immersion into the Field
+year: 2015
+volume: 7th
diff --git a/schema.gn b/schema.gn
deleted file mode 100644 (file)
index 8937f37..0000000
--- a/schema.gn
+++ /dev/null
@@ -1,122 +0,0 @@
----
-title: The Goodnight Markdown Specification - Version 0.1.0
-author: Goodnight Publishing
-style: default
----
-
-[META]
-
-[TOC]
-
-# Features
-
-Along with the features below, some features where removed:
-
-- Line breaks have been removed.
-- Horizontal rules have been removed.
-
-I don't belive in line breaks and horizontal rules are the responsibility of the stylesheet.
-
-## Headings
-
-Headings are prefixed with the `#` hastag character as with standard markdown. There must not be a space before the `#` character. A space may or may not be put after the `#`. Inline formatting will be properly parsed. Inline references will be properly parsed.
-
-## Links
-
-Still to be implemented
-
-## Email addresses
-
-Still to be implemented
-
-## Paragraphs
-
-`text about paragraphs`
-
-## Images
-
-Still to be implemented
-
-## Inline code
-
-Inline code blocks can be used with the `\`` backtick character.
-
-## Code blocks
-
-Code blocks can be used with 3 backtick        characters: `\`\`\``. This sequence must start on the first character on a new line.
-
-## Block quotes
-
-Block quotes are started with the tab character `\t`. They can contain any other elements.
-
-       Here is an example of a blockquote.
-
-       - Here is a list element in a blockquote
-
-       Tabbed lines with a space between them are joined into one single blockquote. If two different blockquotes are needed next to each other, a backslash at the first character of a newline can split the two up.
-
-## Inline formatting
-
-- Bold styling will work only with a single asterisk character.
-- Italic styling will work only with a single underscore character.
-
-## Lists
-
-- Unordered lists will work only with the hyphen character.
-- Ordered lists will work only with the period character.
-
-## Macros
-
-To generate the title, table of contents and bibliography, the following macros can be used:
-
-- `[TITLE]` generates the title block
-- `[TOC]` generates the table of contents
-- `[META]` generates the bibliography
-
-## Metadata
-
-The metadata format used is the YAML format. To generate the title, the standard LaTeX keys of "title", "author" and "date", should be used. If "style" is used, the parser will introduce the stylesheet with the relevant extension. For example, `style: stylesheet` will introduce `stylesheet.css` for html and `stylesheet.sty` for LaTeX.
-
-```
----
-title: The Goodnight Markdown Specification
-author: William Greenwood
----
-```
-
-## Referencing
-
-All referencing in Goodnight is APA 7th [lindsey2015]. It can be done similar to referenced links with a syntax similar to YAML metadata:
-
-```
-"Placeholder quote" [1]
-
-\[1]
-title: Placeholder title
-author: Placeholder name
-```
-
-If one of the keys is placed in square brackets after the in-text reference, it will be printed. If only the reference id is used, the full in-text reference will be used. If something that is not one of the keys is placed in square brackets within the full refrence, for example a page number.
-
-The keys are as follows:
-
-. `author`. The authors or entities behind the work seperated by the word "and". Wrap entity names in curly brackets
-. `title`
-. `year` (optional)
-. `publisher` (optional)
-. `url` (optional)
-. `journaltitle`: Title of the book or journal the work is in (optional)
-. `issuetitle`: Issue title of the book or journal the work is in (optional, must be used with `journaltitle`)
-. `editor`: Author of the book or journal the work is in (optional, must be used with `journaltitle`)
-. `volume`: Volume of the book or journal the work is in (optional, must be used with `journaltitle`)
-
-[REFS]
-
-[lindsey2015]
-author: Marley-Vincent Lindsey
-editor: William Value
-title: The Politics of Pokémon. Socialized Gaming, Religious Themes and the Construction of Communal Narratives
-journaltitle: Heidelberg Journal of Religions on the Internet
-issuetitle: Religion in Digital Games Reloaded. Immersion into the Field
-year: 2015
-volume: 7th
index f253e798b7a7a8056b7656d0224365f483b8a76f..5091d05166382b784e98df43eb1a4d7d41deafa6 100644 (file)
@@ -91,7 +91,7 @@ pub fn parse_body (string: String, metadata: Metadata, reference_list: Option<Re
                                        }},
                                ('[', 'M', 'E', 'T', 'A', ']') => {document.push(Box::new(metadata.clone()))},
                                ('[', 'T', 'O', 'C', ']', ..) => {
-                                       document.push(Box::new(Contents{elements: None}))
+                                       document.push(Box::new(Contents{elements: parse_headings(string.clone(), reference_list.clone())}))
                                },
 
                                // match all block quotes
@@ -144,6 +144,63 @@ pub fn parse_body (string: String, metadata: Metadata, reference_list: Option<Re
        document
 }
 
+fn parse_headings(string: String, reference_list: Option<ReferenceList>) -> Option<Vec<Heading>> {
+       let mut elements: Vec<Heading> = Vec::new();
+
+       let mut lines = string.lines().peekable();
+       while let Some(line) = lines.next() {
+               if !(line == "") {
+                       let first = &(String::from(line) + "       ")[0..7].chars().collect::<Vec<_>>();
+                       match (first[0], first[1], first[2], first[3], first[4], first[5]) {
+                               // match all code blocks so that we dont match headings in a preformatted area
+                               ('`', '`', '`', ..) => {
+                                       while match lines.peek() {
+                                               Some(string) => {
+                                                       if string.starts_with("```") {false}
+                                                       else {
+                                                               lines.next();
+                                                               true
+                                                       }
+                                               },
+                                               None => false
+                                       } {()}
+                                       lines.next(); // consume the final ```
+                               }
+
+                               // match all heading patterns
+                               ('#', '#', '#', '#', '#', '#') => {
+                                       elements.push(Heading{text: parse_reference(&reference_list, line[6..].to_string()), level: 6})
+                               },
+                               ('#', '#', '#', '#', '#', ..) => {
+                                       elements.push(Heading{text: parse_reference(&reference_list, line[5..].to_string()), level: 5})
+                               },
+                               ('#', '#', '#', '#', ..) => {
+                                       elements.push(Heading{text: parse_reference(&reference_list, line[4..].to_string()), level: 4})
+                               },
+                               ('#', '#', '#', ..) => {
+                                       elements.push(Heading{text: parse_reference(&reference_list, line[3..].to_string()), level: 3})
+                               },
+                               ('#', '#', ..) => {
+                                       elements.push(Heading{text: parse_reference(&reference_list, line[2..].to_string()), level: 2})
+                               },
+                               ('#', ..) => {
+                                       elements.push(Heading{text: parse_reference(&reference_list, line[1..].to_string()), level: 1})
+                               },
+                               ('[', 'R', 'E', 'F', 'S', ']') => {
+                                       elements.push(Heading{text: String::from("Bibliography"), level: 1})
+                               },
+                               _ => {()}
+                       }
+               }
+       }
+
+       if elements.is_empty() {
+               None
+       } else {
+               Some(elements)
+       }
+}
+
 fn parse_reference(reference_list: &Option<ReferenceList>, text: String) -> String {
        match reference_list.clone() {
                Some(bibliography) => {
index 44af23c92265ca4ab433efd77dc54dfe32ac18d3..31c025a0d477bb89b5cb656253e9ae3854f22969 100644 (file)
@@ -10,17 +10,116 @@ impl Renderable for Contents {
        }
 
        fn render_html(&self) -> String {
-               String::from("\
-<h2>Table of Contents</h2>
-<p>This is where the table of contents will go</p>
-               ")
+               let mut text = String::from("<h2>Table of Contents</h2>\n<ol style='list-style-type: none;'>\n");
+               let mut counters = [0; 6];
+
+               match &self.elements {
+                       Some(elements) => {
+                               let mut last_level = 1;
+                               let mut depth = 1;
+
+                               for element in elements {
+                                       if element.level > last_level {
+                                               text.push_str(&"<ol style='list-style-type: none;'>".repeat(element.level - last_level));
+                                               depth += element.level - last_level;
+                                       }
+                                       else if element.level < last_level {
+                                               text.push_str(&"</ol>".repeat(last_level - element.level));
+                                               depth -= last_level - element.level;
+                                       }
+                                       last_level = element.level;
+
+                                       counters[element.level - 1] += 1;
+
+                                       let plaintext = element.render_plaintext();
+                                       let anchor: String = plaintext.to_lowercase().split_whitespace().map(|s| s.chars().chain(['-'])).flatten().collect();
+                                       let anchor = &anchor[..anchor.len()-1];
+                                       let counter_text = counters[..element.level]
+                                               .iter()
+                                               .map(|s| s .to_string()
+                                               .as_str()
+                                               .chars()
+                                               .chain(['.'])
+                                               .collect::<Vec<char>>())
+                                               .flatten()
+                                               .collect::<String>();
+                                       let counter_text = &counter_text[..counter_text.len()-1];
+
+                                       text.push_str(&match element.level {
+                                               1 => {
+                                                       counters[1..].fill(0);
+                                                       if &element.text == "Bibliography" {
+                                                               String::from("<li>\n\t<a href='#bibliography'><b>Bibliography</b></a></li>\n")
+                                                       } else {
+                                                               format!(
+                                                                       "<li>\n\t<a href='#{anchor}'>Section {section}</a> - <b>{plaintext}</b></li>\n",
+                                                                       section = counters[0]
+                                                               )
+                                                       }
+                                               },
+                                               2..=5 => {
+                                                       counters[element.level..].fill(0);
+                                                       format!("<li>\n\t<em><a href='#{anchor}'>({counter_text})</a></em> {plaintext}</li>\n")
+                                               },
+                                               _ => {
+                                                       format!("<li>\n\t<em><a href='#{anchor}'>({counter_text})</a></em> {plaintext}</li>\n")
+                                               }
+                                       })
+                               };
+                               text + &"</ol>".repeat(depth)
+                       }
+                       None => {String::new()}
+               }
        }
 
        fn render_gemtext(&self) -> String {
-               String::from("\
-## Table of Contents
+               let mut text = String::from("## Table of Contents\n\n");
+               let mut counters = [0; 6];
+
+               match &self.elements {
+                       Some(elements) => {
+                               for element in elements {
+                                       text.push_str(&match element.level {
+                                               1 => {
+                                                       counters[0] += 1;
+                                                       counters[1..].fill(0);
+                                                       if &element.text == "Bibliography" {
+                                                               format!("* {}", element.render_plaintext())
+                                                       } else {
+                                                               format!("* Section {} - {}", counters[0], element.render_plaintext())
+                                                       }
+                                               },
+                                               2 => {
+                                                       counters[1] += 1;
+                                                       counters[2..].fill(0);
+                                                       format!("* ({}.{}) {}", counters[0], counters[1], element.render_plaintext())
+                                               },
+                                               3 => {
+                                                       counters[2] += 1;
+                                                       counters[3..].fill(0);
+                                                       format!("* ({}.{}.{}) {}", counters[0], counters[1], counters[2], element.render_plaintext())
+                                               },
+                                               4 => {
+                                                       counters[3] += 1;
+                                                       counters[4..].fill(0);
+                                                       format!("* ({}.{}.{}.{}) {}", counters[0], counters[1], counters[2], counters[3], element.render_plaintext())
+                                               },
+                                               5 => {
+                                                       counters[4] += 1;
+                                                       counters[5..].fill(0);
+                                                       format!("* ({}.{}.{}.{}.{}) {}", counters[0], counters[1], counters[2], counters[3], counters[4], element.render_plaintext())
+                                               },
+                                               _ => {
+                                                       counters[5] += 1;
+                                                       format!("* ({}.{}.{}.{}.{}.{}) {}", counters[0], counters[1], counters[2], counters[3], counters[4], counters[5], element.render_plaintext())
+                                               }
+                                       })
+                               };
+                               text + "\n"
+                       }
+                       None => {String::new()}
+               }
 
-This is where the Table of Contents will go.\n\n")
        }
 
        fn render_plaintext(&self) -> String {
index 98f5bada59d937a903f63b7765f694be3e44ad07..3abb8c92c6575d471e13fc15e64af0cbda021583 100644 (file)
@@ -1,5 +1,3 @@
-use std::collections::HashMap;
-
 use crate::types::metadata::Metadata;
 use crate::types::reference_list::ReferenceList;
 use crate::types::Renderable;
index 39eabc4ddd2305f1e68f28a4e8cf7c9c56f6e290..6911ded84b92703c5ade99071ddb0270b3dbe9cd 100644 (file)
@@ -23,6 +23,8 @@ impl Renderable for Heading {
 
        fn render_html (&self) -> String {
                let text = parse_inline_html(self.text.trim());
+               let anchor: String = text.to_lowercase().split_whitespace().map(|s| s.chars().chain(['-'])).flatten().collect();
+               let anchor = &anchor[..anchor.len()-1];
                let section = match self.level {
                        1 => "h2",
                        2 => "h3",
@@ -30,7 +32,7 @@ impl Renderable for Heading {
                        4 => "h5",
                        _ => "h6"
                };
-               format!("<{section}>{text}</{section}>\n")
+               format!("<{section} id={anchor}>{text}</{section}>\n")
        }
 
        fn render_gemtext (&self) -> String {
@@ -43,8 +45,8 @@ impl Renderable for Heading {
        }
 
        fn render_plaintext (&self) -> String {
-               let text = parse_inline_remove(self.text.trim()).to_uppercase();
-               format!("{text}\n\n")
+               let text = parse_inline_remove(self.text.trim());
+               format!("{text}\n")
        }
 }
 
index a5c5fc0dde652e459aea72efc1d18f22de0de804..0d6a3e8c79f356fafb7418def900b2e2ebbc9383 100644 (file)
@@ -108,7 +108,7 @@ impl Renderable for ReferenceList {
        }
 
        fn render_html(&self) -> String {
-               let mut full_text = String::from("<section><h2>Bibliography</h2>");
+               let mut full_text = String::from("<section><h2 id='bibliography'>Bibliography</h2>");
                for reference in &self.list {
                        full_text.push_str(&(reference.render_html() + "\n\n"));
                }