Skip to content

pedroth/nabladown.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nabladown.js

A parser and renderer for the Nabladown language.

NablaDown.js is a JS library able to parse: String -> Abstract Syntax Tree a pseudo/flavored Markdown language and render: Abstract Syntax Tree -> HTML it into HTML.

The purpose of this library is to render beautiful documents in HTML, using a simple language as Markdown, with the focus on rendering code,equations and html. It includes macros for extending the language features.

The library is written in a way, that is possible to create and compose multiple renderers together. This way is possible to add features on top of a basic renderer. More on that below (check the Advanced section).

Playground Usage:

Contents

  1. QuickStart
  2. Language cheat sheet
  3. Try it
  4. Advanced
  5. Develop nabladown.js
  6. Influences
  7. TODO

Quick Start

Nabladown.js provides two main functions:

  • parse: String -> AST
  • render: AST -> HTML

The parser will produce a Abstract Synatx Tree(AST) from a string, and render will create HTML nodes from the parsing tree.

Web

<!DOCTYPE html>
<html lang="en">

<head>
</head>

<body>

</body>
<script type="module">
    import { parse, render } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/index.js";

    // You can also import from local file e.g:
    // import { parse, render } from "./node_modules/nabladown.js/dist/web/index.js";

    const content = "#$\\nabla$ Nabladown`.js`\n <span style='background: blue'>Check it out</span> [here](https://www.github.com/pedroth/nabladown.js)\n";

    render(parse(content)).then(dom => document.body.appendChild(dom));
</script>
</html>

Install it using npm install nabladown.js

import { useEffect, useState } from 'react'
import { parse, render } from "nabladown.js/dist/web/index"
import './App.css'

const content = "#$\\nabla$ Nabladown`.js`\n <span style='background: blue'>Check it out</span> [here](https://www.github.com/pedroth/nabladown.js)\n";
function App() {
  const [dom, setDom] = useState("");
  useEffect(() => {
    render(parse(content)).then((nablaDom) => setDom(nablaDom));
  }, [])

  return (
    <div dangerouslySetInnerHTML={{ __html: dom.innerHTML }}>
    </div>
  )
}

export default App

Install it using npm install nabladown.js / bun add nabladown.js

import { parse, render } from "nabladown.js/dist/node/index.js";

const content = "#$\\nabla$ Nabladown`.js`\n <span style='background: blue'>Check it out</span> [here](https://www.github.com/pedroth/nabladown.js)\n";

(async () => {
    const domStr = await render(parse(content))
    console.log(domStr);
})();

With formatted string:

import { parse, renderToString } from "nabladown.js/dist/node/index.js";

const content = "#$\\nabla$ Nabladown`.js`\n <span style='background: blue'>Check it out</span> [here](https://www.github.com/pedroth/nabladown.js)\n";

(async () => {
    const domStr = await renderToString(parse(content), {isFormatted: true})
    console.log(domStr);
})();

NPM

Check npm page here, to check all nabladown.js versions.

Language cheat sheet

This language is similar markdown syntax but adds some extras like formulas, code, HTML, and macros.

Although similar to markdown, it has some minor differences

Headers

# H1
## H2
### H3
...
###### H6

Style

_italics_

*bold*

*_bold and italics_*

_*italics and bold*_

Paragraph

lorem ipsum lorem ipsum // paragraph

lorem ipsum lorem ipsum. lorem ipsum lorem ipsum. lorem ipsum lorem ipsum lorem ipsum lorem ipsum // paragraph

lorem ipsum lorem ipsum. lorem ipsum lorem ipsum. lorem ipsum lorem ipsum
lorem ipsum lorem ipsum. lorem ipsum lorem ipsum.
lorem ipsum lorem ipsum. // paragraph

Lists

Unordered

- Parent
 - Child
  - GrandChild
  - GrandChild
 - Child

Ordered

// numbers don't really matter,
// they just need to be numbers
1. Parent
 2. Child
  3. GrandChild
  3. GrandChild
 8. Child

Mixed type

1. Ordered Parent
 - Unordered Child
 - Unordered Child
 - Unordered Child
2. Ordered Parent
 - Unordered Child
 - Unordered Child

Indentation

Single spaces:

- A list
 - A sublist
  - A subsublist
 - A sublist
- A list

Single tabs:

- A list
  - A sublist
    - A subsublist
  - A sublist
- A list

For now, nabladown.js is not able to write paragraphs in lists. Like here. To be added in future. But there is an hack:

- A list
 - <div>
    !!!
    Write paragraph as usual
    !!!
   </div>
 - Another list item

Links

// simple link
[nabladown.js](https://pedroth.github.io/nabladown.js/)

// link using reference
[brave][ref]

Some optional text...

[ref]: https://search.brave.com/

It is possible to link to titles:

# A Title

[Go to title](#a-title)

You can also use bare links like this:

https://pedroth.github.io/nabladown.js/

Footnotes

Some optional text [^1]
blablabla [^foot] blablabla

...

[^1]: Text with *nabladown* syntax
[^foot]: You can use any identifier

For now, it's not possible to add paragraphs in footnotes, like here. But there is an hack:

A complex footnote[^complex] !!
---
[^complex]: <div> 
!!!
Write nabladown as usual!
!!!!
</div>

Images/Videos

// simple image
![Image legend](https://picsum.photos/200)

// image with title
![Image _legend_  with $\nabla$](https://picsum.photos/200)

// Image with link to it
[![Image reference + *link* + reference][link_variable]][link_variable]

[link_variable]: some link to image

// video
![Free *video*][open_video_var]

[open_video_var]: https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4

// youtube video with legend
![*Megaman* youtube video](https://www.youtube.com/watch?v=uVxshK09WvI)

// sound
![Free _sound_](https://www.bensound.com/bensound-music/bensound-ukulele.mp3)

Math

Use Tex syntax inside '$'.

// inline
Lorem ipsum $x^2+1 = 0$

// paragraph
$$e^{2\pi i} - 1 = 0$$

// paragraph
$$
\oint_{\partial\Omega} \alpha = \ int_\Omega \text{d} \alpha
$$

Code

Inline code

lorem ipsum `inline code here` lorem ipsum

Block code

```java
class Main {
  public static void main(String[] args) {
    System.out.println("Hello")
  }
}
```

In the abstract

```<language>

block code...

```

Syntax here. Name of the available languages according to highlight.js

Line Separation

lorem ipsum 

---

lorem ipsum

HTML

# Normal markdown with <span style="color: red"> red text </span> inline

A paragraph with html and nabladown inside:
<div>
<a href="https://pedroth.github.io/nabladown.js">
    $1 + 1 = 2$
</a>
<button onClick="alert('hello world')">
  hello *world*
</button>
</div>

Comments

Normal html comments:

<div class="quote">
  <a href="https://pedroth.github.io/nabladown.js">
    $$\sum_ {n=1}^\infty 1 / n^2 = \pi^2 / 6$$
  </a>
  <hr />
  <!-- A comment -->
  <div style="text-align: center">
    <button onClick="alert('hello world')">
      hello _*world*_!!
    </button>
  </div>
</div>
<!-- 
  Another comment

  With text in it!!
-->

Macros

Macros definitions:

::
  // Define a function in js, with form: 
  // f: (input: string, array: string[]) => string
  function addClass(input, args) {
    // this macro add a particular class to nabladown input
    const [className] = args;
    return `<div class="${className}">${input}</div>`
  }

  // export function in special way
  MACROS = {addClass}
::

Macros usage:

[addClass myClass]::
Normal $\nabla$nabladowns`.js`
::

Macros usage inline:

Hello [addClass red]::world::!!

Arguments are differentiated through the space character unless they have " quotes:

::
 function id(input, args) {
  // this macros adds an id to a particular nabladown input
  const [name] = args;
  return `<div id="${name}">${input}</div>`;
 }

 MACROS={id}
::

[id "hello world"]::
 *Hello world!!!*
::

A general usage of macros would be:

[alreadyDefinedMacroFunction arg1 arg2 ... argN]::

A nabladown.js string

::

As inline:

... [alreadyDefinedMacroFunction arg1 arg2 ... argN]::A nabladown.js string:: ...

It should be possible to import macros:

::
import "./path2macros.js";
import "./src/macros.js";
::

That is the only way to import files, for now. The file with defining macros should be something like this:

// macros.js
function macro1(input, args) {
 ... 
}

...

function macroN(input, args) {
  ...
}

MACROS = {macro1, ..., macroN}

Example: creating details section using a macro

::
function details(input, args) {
  const [title] = args;
  return `
  <details>
  <summary>${title}</summary>
    ${input}
  </details>`
}

MACROS = {details}
::
# A details example

[details "Factorial definition"]::

$$
  n! = \begin{cases} 
		1 & \text{if } n = 0, \\
		n \times (n-1)! & \text{if } n > 0.
 	  \end{cases}
$$
::

Try it

You can try nabladown.js language in two ways:

Advanced

Library exports

This library exports:

  • Parser.js
  • Render.js (vanilla render)
  • MathRender.js (vanilla + math)
  • CodeRender.js (vanilla + code)
  • NablaRender.js (vanilla + math + code)

Web import

import {parse} from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/Parser.js"
import {render as vanillaRender, Render} from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/Render.js"
import {render as mathRender, Render as MathRender} from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/MathRender.js"
import {render as codeRender, Render as CodeRender} from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/CodeRender/CodeRender.js"
import {render as codeRender, Render as NablaRender} from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/NabladownRender.js"

You can also import a particular version of nabladown.js from jsdelivr

You can also point to local nabladown.js

import {parse} from "<LOCAL_NABLADOWN.JS>/dist/web/Parser.js"
import {render as vanillaRender, Render} from "<LOCAL_NABLADOWN.JS>/dist/web/Render.js"
import {render as mathRender, Render as MathRender} from "<LOCAL_NABLADOWN.JS>/dist/web/MathRender.js"
import {render as codeRender, Render as CodeRender} from "<LOCAL_NABLADOWN.JS>/dist/web/CodeRender/CodeRender.js"
import {render as codeRender, Render as NablaRender} from "<LOCAL_NABLADOWN.JS>/dist/web/NabladownRender.js"

Node / Bun

import {parse} from "nabladown.js/dist/node/Parser.js"
import {render as vanillaRender, Render} from "nabladown.js/dist/node/Render.js"
import {render as mathRender, Render as MathRender} from "nabladown.js/dist/node/MathRender.js"
import {render as codeRender, Render as CodeRender} from "nabladown.js/dist/node/CodeRender/CodeRender.js"
import {render as codeRender, Render as NablaRender} from "nabladown.js/dist/node/NabladownRender.js"

Using all renders

<html>

<body></body>
<script type="module">
    import { parse } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/Parser.js"
    import { render as vanillaRender } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/Render.js"
    import { render as mathRender } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/MathRender.js"
    import { render as codeRender } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/CodeRender/CodeRender.js"
    import { render as nablaRender } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/NabladownRender.js"

    (async () => {
        // append basic rendering
        await vanillaRender(parse("# $\\nabla$ Nabladown`.js` \n")).then(dom => document.body.appendChild(dom));
        // append code rendering
        await codeRender(parse("# $\\nabla$ Nabladown`.js` \n")).then(dom => document.body.appendChild(dom));
        // append math rendering
        await mathRender(parse("# $\\nabla$ Nabladown`.js` \n")).then(dom => document.body.appendChild(dom));
        // append nabladown rendering
        await nablaRender(parse("# $\\nabla$ Nabladown`.js` \n")).then(dom => document.body.appendChild(dom));
    })()
</script>

</html>

Extending basic renderer

It is possible to extend the basic renderer, to build a custom one. There are a few ways of doing this:

  • Adding style to HTML components using regular CSS.
  • Extending Render class from Render.js

The CodeRender class is an example of extending the Render class, where code highlight was implemented.

The MathRender class is an example of extending the Render class, where katex rendering was added.

You can also combine multiple renderers together using composeRender function. Check NabladownRender class for an example of that.

Changing CSS

<html>
  <head>
    <style>
      body {
        background-color: #212121;
        color: white;
        font-family: sans-serif;
      }

      body h1 {
        text-decoration: underline;
        background-color: blue;
      }

      body code {
        border-style: solid;
        border-width: thin;
        border-radius: 6px;
        box-sizing: border-box;
        background-color: red;
        border: hidden;
        font-size: 85%;
        padding: 0.2em 0.4em;
        color: green;
      }
    </style>
  </head>
  <body></body>
  <script type="module">
    import {parse, render} from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/index.js"
    render(parse("# $ \\nabla $ Nabladown`.js` \n")).then(dom => document.body.appendChild(dom));
  </script>
</html>

Extending NabladownRender class

Change headers color based on their level

<html>

<body></body>
<script type="module">
    import { parse } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/index.js"
    import { Render } from "https://cdn.jsdelivr.net/npm/nabladown.js/dist/web/NabladownRender.js";
    class CustomRender extends Render {
        /**
         * (title, context) => DomBuilder
         */
        renderTitle(title, context) {
            const colors = ["red", "orange", "yellow", "green", "blue", "purple"];
            const { level } = title;
            const header = super.renderTitle(title, context);
            header.attr("style", `color:${colors[level - 1]}`);
            return header;
        }
    }

    const render = syntaxTree => new CustomRender().render(syntaxTree);
    const text = `# $ \\nabla$Nabladown.js \n#### $ \\nabla$Nabladown.js \n#####$ \\nabla$Nabladown.js \n`;
    // append custom rendering
    render(parse(text)).then(dom => document.body.appendChild(dom));
</script>

</html>

All render methods return a DOM abstraction object, described here.

For more details, you need to dig the source code :D

Develop Nabladown.js

Dependencies

nabladown.js is using bun@^1.1.21, nodejs@^22.3.0 and [email protected]

Building library

bun run build

Testing

Running unit tests: bun test.

Running playground index.html, just use bun serve.

Influences / Inspiration

TODO

  1. Optimize html generation

    • Remove unnecessary spans, divs, etc.
  2. Total compatibility between nodejs and browser rendering.

    • Copy button doesn't work when generating html as string
  3. Optimize fetching styles

  4. Make nabladown.js a totally offline lib

    • Use local katex style instead of online one
  5. Add paragraphs to lists as here and footnotes

  6. Add inline attributes to links, equations, custom... as Quatro and this or this

  7. Add easy tables, check AsciiDoc tables and Orgmode tables

  8. Think about escaping characters, like `, <, *, >, _

  9. Optimize Playground

    • Loading screen
    • Render by chunks
    • Show token info in playground
  10. Add dialog in images (expanding images in cell phone) - Check photoswipe, glightbox

  11. Multiple styles in code rendering

  12. Add metadata space such Quatro

  13. Change some recursions to linear recursions or just loops (?)

    • Apply parseAnyBut loop to parseDocument, parseExpressions, ...