web preview + finish-ish markdown

ordered lists, superscript, tables
and some fixes
This commit is contained in:
jetstream0
2023-06-09 23:15:52 -07:00
parent 75260e28a3
commit d454a1f8ea
7 changed files with 375 additions and 13 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
*.js
tsconfig.json
tsconfig-node.json
tsconfig-web.json

26
index.html Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Makoto Markdown Editor</title>
<link href="/styles/style.css" rel="stylesheet">
<link href="/styles/makoto.css" rel="stylesheet">
</head>
<body>
<div id="grid-container">
<div id="editor-container" class="section">
<!--<textarea id="editor" placeholder="Type in some markdown..."></textarea>-->
<ol id="editor" contenteditable>
<li>markdown goes here...</li>
</ol>
</div>
<div id="preview-container" class="section">
<label for="dark-theme-toggle">Dark Theme</label><input id="dark-theme-toggle" type="checkbox">
<div id="rendered-text">
</div>
</div>
</div>
<script src="/web.js" type="module"></script>
</body>
</html>

View File

@@ -17,6 +17,12 @@ Quirks
```
- If a language for the code block is provided, the parser will add a css class "code-<language name>" to the resulting code block div (which btw has class code-block)
- All elements should be able to be used in block quotes (ok, not really a quirk). In code blocks, the code in the code block must also start with "> " of course
- Superscripts are supported! "^1^" become "<sup>1</sup>". Use these for footnotes too, I'm not implementing those.
- Underlined headers are not supported, just use a header and a horizontal rule
- Nested blockquotes won't work
- The first row of a table will be assumed to be the header row, and don't bother with a row of dashes after it.
- Also, the pipe ("|") in tables is mandatory at the end of the row, otherwise weird things will happen.
- Bold, italic, and other elements are not supported in tables
-
*/
@@ -45,6 +51,8 @@ test_assert_equal(parse_md_to_html("**test abc** *a*\n## **ch*ch**"), "<p><b>tes
test_assert_equal(parse_md_to_html("****a*"), "<p><b></b>a*</p>", "bold italic test 2");
test_assert_equal(parse_md_to_html("**burger** *b*ca*\n**with no fries**"), "<p><b>burger</b> <i>b</i>ca*</p>\n<p><b>with no fries</b></p>", "bold italic test 3");
test_assert_equal(parse_md_to_html("---\n--\n----\n--a-\n---"), "<hr>\n<p>--</p>\n<hr>\n<p>--a-</p>\n<hr>", "horizontal rule test");
test_assert_equal(parse_md_to_html("\\*\\*cheese\\*\\*\n*\\*cheese\\*\\*"), "<p>**cheese**</p>\n<p><i>*cheese*</i></p>", "backslash test");
@@ -63,12 +71,23 @@ test_assert_equal(parse_md_to_html("```\nif time == 420:\n weed()\n```"), "<d
test_assert_equal(parse_md_to_html("## testing\n```markdown\n# title\n i like **cheeseburgers** and `code`\n```\n```"), "<h2 id=\"header-0\">testing</h2>\n<div class=\"code-block code-markdown\">\n# title<br>\n&nbsp;&nbsp;i like **cheeseburgers** and `code`<br>\n</div>\n<p><code></code>`</p>", "code block test 2");
test_assert_equal(parse_md_to_html("> ```\n> if\n> b\n> a\n> ```"), "<blockquote>\n<div class=\"code-block\">\nif<br>\n&nbsp;b<br>\na<br>\n</div>\n</blockquote>", "code block test 2")
test_assert_equal(parse_md_to_html("test\n> test\n> ## TEST\n> **beach**\n> `wee`\n> # dd"), "<p>test</p>\n<blockquote>\n<p>test</p>\n<h2 id=\"header-0\">TEST</h2>\n<p><b>beach</b></p>\n<p><code>wee</code></p>\n<h1 id=\"header-1\">dd</h1>\n</blockquote>", "block quote test 1");
test_assert_equal(parse_md_to_html("> ```\n> alert('e')\n> ```"), "<blockquote>\n<div class=\"code-block\">\nalert('e')<br>\n</div>\n</blockquote>", "block quote test 2");
//todo: ordered lists, unordered lists, tables
test_assert_equal(parse_md_to_html("> a\n\n> b"), "<blockquote>\n<p>a</p>\n</blockquote>\n<blockquote>\n<p>b</p>\n</blockquote>", "block quote test 3");
console.log(parse_md_to_html("- burger\n- fries\n- pizza"));
test_assert_equal(parse_md_to_html("> - burger\n> -winter melons\n> abcdefg\n> - fries\n- p**i**zza\nabacus"), "<blockquote>\n<ul>\n<li>burger</li>\n</ul>\n<p>-winter melons</p>\n<p>abcdefg</p>\n<ul>\n<li>fries</li>\n</ul>\n</blockquote>\n<ul>\n<li>p<b>i</b>zza</li>\n</ul>\n<p>abacus</p>", "unordered lists test");
test_assert_equal(parse_md_to_html("1. a\n2. b\n3. c\n5. should *fail*\n> 1. d\n> 2. e\n3. should fail too"), "<ol>\n<li>a</li>\n<li>b</li>\n<li>c</li>\n</ol>\n<p>5. should <i>fail</i></p>\n<blockquote>\n<ol>\n<li>d</li>\n<li>e</li>\n</ol>\n</blockquote>\n<p>3. should fail too</p>", "ordered lists test 1")
test_assert_equal(parse_md_to_html("1. uno\n2. dos\n3. tres\n4. cuatro\n5. cinco\n6. seis\n7. siete\n8. ocho\n9. nueve\n10. diez\n11. once"), "<ol>\n<li>uno</li>\n<li>dos</li>\n<li>tres</li>\n<li>cuatro</li>\n<li>cinco</li>\n<li>seis</li>\n<li>siete</li>\n<li>ocho</li>\n<li>nueve</li>\n<li>diez</li>\n<li>once</li>\n</ol>", "ordered lists test 2");
test_assert_equal(parse_md_to_html("a ^ace^ base\n```js\n^sup^\n```\ne=mc^2\n^ea^"), "<p>a <sup>ace</sup> base</p>\n<div class=\"code-block code-js\">\n^sup^<br>\n</div>\n<p>e=mc^2</p>\n<p><sup>ea</sup></p>", "superscript test");
//I don't care about table edgecases.
test_assert_equal(parse_md_to_html("|a|b|c|\n|d|e|f|\n# a"), "<table>\n<tr>\n<th>a</th>\n<th>b</th>\n<th>c</th>\n</tr>\n<tr>\n<td>d</td>\n<td>e</td>\n<td>f</td>\n</tr>\n</table>\n<h1 id=\"header-0\">a</h1>", "basic table test");
console.log(`Total Passed: \x1B[32m${passed_tests}/${total_tests}\x1B[m\nTotal Failed: \x1B[31m${failed_tests}/${total_tests}\x1B[m`);

191
makoto.ts
View File

@@ -37,7 +37,13 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
let space_start: boolean = false;
let in_blockquote: boolean = false;
let in_unordered_list: boolean = false;
let blockquote_list: boolean = false;
let in_ordered_list: boolean = false;
let ordered_list_num: number = 0;
let in_superscript: boolean = false;
let in_table: boolean = false;
let in_table_header: boolean = false;
let table_item: string = "";
//loop through characters
let chars: string = md;
@@ -165,6 +171,16 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
if ((was_image || was_link) && char === ")") {
add_char = false;
}
//handle table row ending thing?
if (in_table && char === "|") {
in_table = false;
add_char = false;
if (in_table_header) {
html_line += `<th>${table_item}</th>\n</tr>\n</table>`;
} else {
html_line += `<td>${table_item}</td>\n</tr>\n</table>`;
}
}
//if previous character is also newline, there hasn't been opportunity to add a <p>, so add it!
if (chars[i-1] === "\n") {
html_line = "<p>";
@@ -183,17 +199,89 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
add_char = false;
}
}
//ending superscript
if (in_superscript && char === "^") {
html_line += "</sup>";
in_superscript = false;
add_char = false;
}
if (add_char) {
html_line += char;
}
}
if (in_asterisk) {
//bold/italic never ended
if (asterisk_num === 1) {
//remove the last <i> and replace it with a *
let split: string[] = html_line.split("<i>");
html_line = "";
for (let ii=0; ii < split.length; ii++) {
html_line += split[ii];
if (ii !== split.length-1) {
if (ii === split.length-2) {
html_line += "*";
} else {
html_line += "<i>";
}
}
}
} else if (asterisk_num === 2) {
//remove the last <b> and replace it with a **
let split: string[] = html_line.split("<b>");
html_line = "";
for (let ii=0; ii < split.length; ii++) {
html_line += split[ii];
if (ii !== split.length-1) {
if (ii === split.length-2) {
html_line += "**";
} else {
html_line += "<b>";
}
}
}
}
asterisk_num = 0;
asterisk_out_num = 0;
in_asterisk = false;
}
if (in_superscript) {
//superscript never ended
//remove the last <sup> and replace it with a ^
let split: string[] = html_line.split("<sup>");
html_line = "";
for (let ii=0; ii < split.length; ii++) {
html_line += split[ii];
if (ii !== split.length-1) {
if (ii === split.length-2) {
html_line += "^";
} else {
html_line += "<sup>";
}
}
}
in_superscript = false;
}
//ending table row
if (in_table) {
in_table_header = false;
html_line += "</tr>\n";
}
html += html_line;
if (html_line.startsWith("<p>")) {
html += "</p>\n";
} else if ((html_line.startsWith("<li>") || html_line.startsWith("<ul>")) && in_unordered_list) {
} else if ((html_line.startsWith("<li>") || html_line.startsWith("<ul>") || html_line.startsWith("<ol>")) && (in_unordered_list || in_ordered_list)) {
html += "</li>\n";
if (i === chars.length-1) {
if (in_ordered_list) {
ordered_list_num++;
}
if (i === chars.length-1 && in_unordered_list) {
html += "</ul>";
//set it to false, not that it matters
blockquote_list = false;
} else if (i === chars.length-1 && in_ordered_list) {
html += "</ol>";
//set it to false, not that it matters
blockquote_list = false;
}
}
html_line = "";
@@ -229,6 +317,11 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
html += "\n";
}
html += "</blockquote>";
in_blockquote = false;
} else if (in_blockquote && char === "\n" && chars[i-1] === "\n") {
//two new lines in a row means blockquote ends
html += "</blockquote>\n";
in_blockquote = false;
}
heading_level = 0;
if (i === chars.length - 1) {
@@ -243,7 +336,20 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
html += "<blockquote>\n";
continue;
} else if (in_blockquote && chars[i-1] === "\n" && (char !== "&gt;" || chars[i+1] !== " ")) {
html_line = "</blockquote>\n";
if (blockquote_list) {
//end list if list started in blockquote and blockquote ends
blockquote_list = false;
if (in_unordered_list) {
html += "</ul>\n</blockquote>\n";
} else if (in_ordered_list) {
html += "</ol>\n</blockquote>\n";
}
ordered_list_num = 0;
in_ordered_list = false;
in_unordered_list = false;
} else {
html += "</blockquote>\n";
}
in_blockquote = false;
} else if (char === "&gt;" && chars[i+1] === " " && (chars[i-1] === "\n" || i === 0)) {
//do not add the '>' to the html
@@ -304,10 +410,10 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
} else if (in_code_block) {
//do not render markdown inside code blocks... obviously
//preserve spaces at the beginning of lines
if (char === " " && space_start) {
html_line += "&nbsp;";
} else if (in_blockquote && ((char === " " && chars.slice(i-2, i) === "\n>") || (char === "&gt;" && chars[i-1] === "\n" && chars[i+1] === " "))) {
if (in_blockquote && ((char === " " && chars.slice(i-2, i) === "\n>") || (char === "&gt;" && chars[i-1] === "\n" && chars[i+1] === " "))) {
//do not add the blockquote syntax thing "> " to the codeblock
} else if (char === " " && space_start) {
html_line += "&nbsp;";
} else {
space_start = false;
html_line += char;
@@ -315,18 +421,42 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
continue;
}
//handle unordered lists
if (char === " " && chars[i-1] === "-" && (chars[i-2] === "\n" || i === 1)) {
if (char === " " && chars[i-1] === "-" && (chars[i-2] === "\n" || i === 1 || (in_blockquote && chars.slice(i-3, i-1) === "> " && (chars[i-4] === "\n" || i === 3)))) {
//it's a unordered list bullet point!!
if (!in_unordered_list) {
if (!in_unordered_list || (!in_blockquote && blockquote_list)) {
html_line = "<ul>\n<li>";
blockquote_list = false;
} else {
html_line = "<li>";
}
in_unordered_list = true;
if (in_blockquote) {
blockquote_list = true;
}
continue;
} else if (in_unordered_list && ((chars[i-1] === "\n" && char !== "-") || (chars[i-2] === "\n" && char !== " "))) {
html_line += "</ul>\n";
} else if (in_unordered_list && ((((chars[i-1] === "\n" && char !== "-") || (chars[i-2] === "\n" && char !== " ")) && !blockquote_list) || (((chars[i-3] === "\n" && char !== "-") || (chars[i-4] === "\n" && char !== " ")) && blockquote_list))) {
html += "</ul>\n";
in_unordered_list = false;
blockquote_list = false;
}
//handle ordered lists
let ol_num_length: number = String(ordered_list_num+1).length;
if (char === " " && chars.slice(i-1-ol_num_length, i) === `${ordered_list_num+1}.` && (chars[i-ol_num_length-2] === "\n" || i === ol_num_length+1 || (in_blockquote && chars.slice(i-ol_num_length-3, i) === `> ${ordered_list_num+1}.` && (chars[i-ol_num_length-4] === "\n" || i === ol_num_length+3)))) {
if (ordered_list_num === 0) {
html_line = "<ol>\n<li>";
in_ordered_list = true;
} else {
html_line = "<li>";
}
if (in_blockquote) {
blockquote_list = true;
}
continue;
} else if (in_ordered_list && ((chars[i-ol_num_length-2] === "\n" && chars.slice(i-ol_num_length-1, i+1) !== `${ordered_list_num+1}. ` && !in_blockquote) || (in_blockquote && chars[i-ol_num_length-4] === "\n" && chars.slice(i-ol_num_length-3, i+1) !== `> ${ordered_list_num+1}. `))) {
html += "</ol>\n";
ordered_list_num = 0;
in_ordered_list = false;
blockquote_list = false;
}
//handle code
if (char === "`" && !in_code) {
@@ -371,6 +501,35 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
html_line += char;
continue;
}
//handle tables
if (char === "|" && (chars[i-1] === "\n" || i === 0 || (in_blockquote && chars.slice(i-2, i) === "> " && (chars[i-3] === "\n" || i === 3)))) {
if (!in_table) {
//start of table
in_table = true;
in_table_header = true;
html += "<table>\n<tr>\n";
} else {
//just a new table row
html += "<tr>\n";
}
continue;
} else if (in_table && char === "|") {
if (in_table_header) {
html_line += `<th>${table_item}</th>\n`;
} else {
html_line += `<td>${table_item}</td>\n`;
}
table_item = "";
continue;
} else if (in_table && ((chars[i-1] === "\n" && char !== "|") || (in_blockquote && chars[i-3] === "\n" && char !== "|"))) {
in_table = false;
table_item = "";
//table ends
html += "</table>\n";
} else if (in_table) {
table_item += char;
continue;
}
//handle heading levels
//ensure headings are continuous and have after it ("#a##" or "##abc" are not a valid headings), and are at the beginning of the line
//ensure headings are possible in block quotes
@@ -514,6 +673,18 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult {
} else if (char !== "*" && in_asterisk) {
asterisk_out_num = 0;
}
//handle superscripts
if (char === "^") {
if (in_superscript) {
in_superscript = false;
html_line += "</sup>";
continue;
} else {
in_superscript = true;
html_line += "<sup>";
continue;
}
}
//
if (end_add_char) {
html_line += char;

34
styles/makoto.css Normal file
View File

@@ -0,0 +1,34 @@
blockquote {
margin: 0;
margin: 8px 0px;
padding: 4px 8px 4px 0px;
border-left: 8px solid #727272;
padding-left: 8px;
}
p {
margin: 4px 0px;
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
margin-bottom: 5px;
}
.code-block {
font-family: 'Courier New', Courier, monospace;
background-color: #d8d8d8;
}
img {
max-width: 85%;
}
table {
border-collapse: collapse;
}
th, td {
padding: 2px 5px;
border: 1px solid black;
}

58
styles/style.css Normal file
View File

@@ -0,0 +1,58 @@
html, body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
}
#grid-container {
display: grid;
grid-template-columns: auto auto;
height: 100vh;
width: 100vw;
}
.section {
padding: 17px;
height: 100%;
width: 49vw;
box-sizing: border-box;
}
#editor-container {
overflow-y: auto;
}
#editor {
box-sizing: border-box;
width: 95%;
width: calc(100% - 15px);
height: 95%;
margin: 0;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
font-size: 0.9em;
padding: 3px;
padding-left: 35px;
margin-left: 15px;
overflow-y: auto;
}
#editor li::marker {
color: #727272;
}
#rendered-text {
padding: 5px;
word-break: break-all;
font-family: Arial, sans-serif;
}
#rendered-text.dark {
background-color: black;
color: white;
}
#rendered-text.dark .code-block {
color: #2d2d2d;
}

52
web.ts Normal file
View File

@@ -0,0 +1,52 @@
import { parse_md_to_html_with_warnings, ParseResult } from './makoto.js';
import type { Warning } from './endosulfan.js';
let editor: HTMLTextAreaElement = document.getElementById("editor")! as HTMLTextAreaElement;
let preview: HTMLElement = document.getElementById("rendered-text")!;
let dark_theme_toggle: HTMLInputElement = document.getElementById("dark-theme-toggle")! as HTMLInputElement;
let unedited: boolean = true;
function render_warnings(warnings: Warning[]) {
console.log(warnings)
warnings.forEach((warning: Warning) => {
if (warning.line_number) {
//
}
});
}
const refresh_html = () => {
let parsed: ParseResult = parse_md_to_html_with_warnings(editor.innerText);
render_warnings(parsed.warnings);
preview.innerHTML = parsed.html;
};
editor.addEventListener("keyup", refresh_html);
document.addEventListener("click", (e: MouseEvent) => {
if (e.target === editor && unedited) {
(editor.children[0] as HTMLElement).innerText = "";
unedited = false;
refresh_html();
} else if (e.target !== editor && editor.innerText.trim() === "") {
(editor.children[0] as HTMLElement).innerText = "markdown goes here...";
unedited = true;
}
});
const theme_change = () => {
if (dark_theme_toggle.checked) {
preview.classList.add("dark");
} else {
preview.classList.remove("dark");
}
};
dark_theme_toggle.addEventListener("change", theme_change);
refresh_html();
theme_change();
if (editor.innerText !== "markdown goes here...") {
unedited = false;
}