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).
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.
<!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);
})();
Check npm
page here, to check all nabladown.js
versions.
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
# H1
## H2
### H3
...
###### H6
_italics_
*bold*
*_bold and italics_*
_*italics and bold*_
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
- Parent
- Child
- GrandChild
- GrandChild
- Child
// numbers don't really matter,
// they just need to be numbers
1. Parent
2. Child
3. GrandChild
3. GrandChild
8. Child
1. Ordered Parent
- Unordered Child
- Unordered Child
- Unordered Child
2. Ordered Parent
- Unordered Child
- Unordered Child
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
// 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/
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>
// 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)
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
$$
lorem ipsum `inline code here` lorem ipsum
```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
lorem ipsum
---
lorem ipsum
# 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>
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 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}
::
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}
$$
::
You can try nabladown.js
language in two ways:
- Using playground
- Using the nabladown-server
This library exports:
- Parser.js
- Render.js (vanilla render)
- MathRender.js (vanilla + math)
- CodeRender.js (vanilla + code)
- NablaRender.js (vanilla + math + code)
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"
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"
<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>
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.
<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>
<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
nabladown.js
is using bun@^1.1.21
, nodejs@^22.3.0
and npm@10.8.0
bun run build
Running unit tests: bun test
.
Running playground index.html
, just use bun serve
.
-
Optimize html generation
- Remove unnecessary spans, divs, etc.
-
Total compatibility between nodejs and browser rendering.
- Copy button doesn't work when generating html as string
-
Optimize fetching styles
-
Make nabladown.js a totally offline lib
- Use local katex style instead of online one
-
Add paragraphs to lists as here and footnotes
-
Add inline attributes to links, equations, custom... as Quatro and this or this
-
Add easy tables, check AsciiDoc tables and Orgmode tables
-
Think about escaping characters, like `, <, *, >, _
-
Optimize Playground
- Loading screen
- Render by chunks
- Show token info in playground
-
Add dialog in images (expanding images in cell phone) - Check photoswipe, glightbox
-
Multiple styles in code rendering
-
Add metadata space such Quatro
-
Change some recursions to linear recursions or just loops (?)
- Apply parseAnyBut loop to parseDocument, parseExpressions, ...