Last active
September 21, 2024 13:37
-
-
Save learosema/046201e873d71b8c8cba321afcf98d53 to your computer and use it in GitHub Desktop.
Markdown in ~100 lines of JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const MAGIC = [ | |
[/^###### (.+)$/g, '<h6>$1</h6>'], | |
[/^##### (.+)$/g, '<h5>$1</h5>'], | |
[/^#### (.+)$/g, '<h4>$1</h4>'], | |
[/^### (.+)$/g, '<h3>$1</h3>'], | |
[/^## (.+)$/g, '<h2>$1</h2>'], | |
[/^# (.+)$/g, '<h1>$1</h1>'], | |
[/ $/g, '<br/>'], | |
[/_(.+)_/g, '<u>$1</u>'], | |
[/\*(.+)\*/g, '<em>$1</em>'], | |
[/\*\*(.+)\*\*/g, '<strong>$1</strong>'], | |
[/<(https?:\/\/.+)>/g, '<a href="$1">$1</a>'], | |
[ | |
/\!\[(.+)\]\((.+)\)/g, | |
'<img src="$2" alt="$1" />' | |
], | |
[ | |
/\[(.+)\]\((.+)\)/g, | |
'<a href="$2">$1</a>' | |
] | |
]; | |
const UL_PATTERN = /((\s*)(\-|(?:(?:\d+\.)+)) (.+)\n)+/ | |
const ULLI_PATTERN = /(\s*)(\-|(?:(?:\d+\.)+)) (.+)\n?/g | |
function inlines(str) { | |
let result = str | |
for (const [search, replace] of MAGIC) { | |
result = result.replace(search, replace); | |
} | |
return result | |
} | |
function renderList(list) { | |
let u = Number.isNaN(list.start) | |
let html = u ? '<ul>' : `<ol${list.start!==1?` start="${list.start}"`:''}>`; | |
for (const li of list.items) { | |
html += '<li>' + inlines(li.content); | |
if (li.childList) { | |
html+=renderList(li.childList); | |
} | |
html += '</li>'; | |
} | |
return html + (u?'</ul>':'</ol>'); | |
} | |
function parseList(block) { | |
const matches = block.matchAll(ULLI_PATTERN); | |
if (! matches) { | |
throw new Error('could not be parsed', block); | |
} | |
const m = Array.from(matches); | |
const listItems = m.map(match => ({ | |
indent: match[1].length, | |
prefix: match[2], | |
content: match[3], | |
})); | |
const parseStart = (str) => { | |
const idxPattern = str.match(/(\d+)\.$/); | |
return idxPattern ? parseInt(idxPattern[1]): NaN; | |
} | |
const list = {start: parseStart(listItems[0].prefix), items: []}; | |
let currentList = list; | |
let stack = []; | |
let last = null; | |
for (const li of listItems) { | |
if (last !== null && li.indent > last.indent) { | |
stack.push(currentList); | |
currentList = last.childList = { | |
start: parseStart(li.prefix), | |
items: [] | |
}; | |
} else | |
if (last && li.indent < last.indent && stack.length > 0) { | |
currentList = stack.pop(); | |
} | |
const item = {...li, childList: null}; | |
currentList.items.push(item); | |
last = item; | |
} | |
return renderList(list); | |
} | |
export function markdown(input) { | |
return input.replace(/\r\n/g,'\n').split('\n\n') | |
.map(block => block.trim()) | |
.map(block => inlines(block)) | |
.map(block => { | |
if (UL_PATTERN.test(block)) { | |
return parseList(block); | |
} | |
if (/^<\w+>/.test(block)) { | |
return block; | |
} | |
return `<p>${block}</p>` | |
}).join('\n').trim(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { describe, it } from 'node:test'; | |
import assert from 'node:assert/strict'; | |
import { markdown } from './markdown.js'; | |
const MD1 = `Lorem Ipsum | |
Dolor Sit | |
amet! | |
`; | |
const HTML1 = `<p>Lorem Ipsum</p> | |
<p>Dolor Sit</p> | |
<p>amet!</p>`; | |
const MD2 = `# Placeholder Text | |
Lorem Ipsum Dolor Sit amet consetetur adipiscing elit | |
## Headline 2 | |
### Headline 3 | |
#### Headline 4 | |
##### Headline 5 | |
###### Headline 6 | |
`; | |
const HTML2 = `<h1>Placeholder Text</h1> | |
<p>Lorem Ipsum Dolor Sit amet consetetur adipiscing elit</p> | |
<h2>Headline 2</h2> | |
<h3>Headline 3</h3> | |
<h4>Headline 4</h4> | |
<h5>Headline 5</h5> | |
<h6>Headline 6</h6>` | |
const MD_IMG = `` | |
const HTML_IMG = `<p><img src="lea.jpg" alt="Lea" /></p>` | |
const MD_LIST = `# Unordered List | |
- Eat | |
- Sleep | |
- Code | |
- Repeat | |
` | |
const HTML_LIST = `<h1>Unordered List</h1> | |
<ul><li>Eat</li><li>Sleep</li><li>Code</li><li>Repeat</li></ul>` | |
const MD_NESTED_LIST = `# Lea | |
- Pronouns | |
- she/her | |
- Likes | |
- Pasta | |
- Coding | |
- Accessibility | |
` | |
const HTML_NESTED_LIST = `<h1>Lea</h1> | |
<ul><li>Pronouns<ul><li>she/her</li></ul></li><li>Likes<ul><li>Pasta</li><li>Coding</li><li>Accessibility</li></ul></li></ul>` | |
const MD_ORDERED_LIST = `# Lea | |
1. Frontend Dev | |
2. Loves Accessibility | |
3. Hates Ordered Lists | |
` | |
const HTML_ORDERED_LIST = `<h1>Lea</h1> | |
<ol><li>Frontend Dev</li><li>Loves Accessibility</li><li>Hates Ordered Lists</li></ol>` | |
const MD_NESTED_OL = `# Table of contents | |
1. Accessibility Fundamentals | |
1.1. Disability Etiquette | |
2. Accessibility Myths debunked | |
2.1. Accessibility is expensive | |
2.2. Accessibility is ugly | |
3. Quiz | |
` | |
const HTML_NESTED_OL = `<h1>Table of contents</h1> | |
<ol><li>Accessibility Fundamentals<ol><li>Disability Etiquette</li></ol>` + | |
`</li><li>Accessibility Myths debunked<ol><li>Accessibility is expensive</li>` + | |
`<li>Accessibility is ugly</li></ol></li><li>Quiz</li></ol>` | |
describe('markdown transform', () => { | |
it('transforms chunks of text into paragraphs', () => { | |
assert.equal(markdown(MD1), HTML1); | |
}); | |
it('transforms headline correctly', () => { | |
assert.equal(markdown(MD2), HTML2); | |
}); | |
it('transforms links correctly', () => { | |
const MD_LINK = `This is a [link](https://lea.codes/)` | |
const HTML_LINK = `<p>This is a <a href="https://lea.codes/">link</a></p>` | |
const MD_LINK2 = `This is another link: <https://test.de>.` | |
const HTML_LINK2 = `<p>This is another link: <a href="https://test.de">https://test.de</a>.</p>` | |
assert.equal(markdown(MD_LINK), HTML_LINK); | |
assert.equal(markdown(MD_LINK2), HTML_LINK2); | |
}); | |
it('transforms images correctly', () => { | |
assert.equal(markdown(MD_IMG), HTML_IMG); | |
}) | |
it('transforms unordered lists correctly', () => { | |
assert.equal(markdown(MD_LIST), HTML_LIST); | |
}); | |
it('transforms unordered nested lists correctly', () => { | |
assert.equal(markdown(MD_NESTED_LIST), HTML_NESTED_LIST); | |
}); | |
it('transforms ordered lists correctly', () => { | |
assert.equal(markdown(MD_ORDERED_LIST), HTML_ORDERED_LIST); | |
}); | |
it('transforms ordered nested lists correctly', () => { | |
assert.equal(markdown(MD_NESTED_OL), HTML_NESTED_OL); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment