From d454a1f8ea557729725fc0ba15b3be3b41b76170 Mon Sep 17 00:00:00 2001 From: jetstream0 <49297268+jetstream0@users.noreply.github.com> Date: Fri, 9 Jun 2023 23:15:52 -0700 Subject: [PATCH] web preview + finish-ish markdown ordered lists, superscript, tables and some fixes --- .gitignore | 4 +- index.html | 26 +++++++ index.ts | 23 +++++- makoto.ts | 191 +++++++++++++++++++++++++++++++++++++++++++--- styles/makoto.css | 34 +++++++++ styles/style.css | 58 ++++++++++++++ web.ts | 52 +++++++++++++ 7 files changed, 375 insertions(+), 13 deletions(-) create mode 100644 index.html create mode 100644 styles/makoto.css create mode 100644 styles/style.css create mode 100644 web.ts diff --git a/.gitignore b/.gitignore index 1838af2..87ffa6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.js -tsconfig.json \ No newline at end of file +tsconfig.json +tsconfig-node.json +tsconfig-web.json \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..fa8a089 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + Makoto Markdown Editor + + + + +
+
+ +
    +
  1. markdown goes here...
  2. +
+
+
+ +
+
+
+
+ + + diff --git a/index.ts b/index.ts index 68290da..c33093f 100644 --- a/index.ts +++ b/index.ts @@ -17,6 +17,12 @@ Quirks ``` - If a language for the code block is provided, the parser will add a css class "code-" 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 "1". 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**"), "

tes test_assert_equal(parse_md_to_html("****a*"), "

a*

", "bold italic test 2"); +test_assert_equal(parse_md_to_html("**burger** *b*ca*\n**with no fries**"), "

burger bca*

\n

with no fries

", "bold italic test 3"); + test_assert_equal(parse_md_to_html("---\n--\n----\n--a-\n---"), "
\n

--

\n
\n

--a-

\n
", "horizontal rule test"); test_assert_equal(parse_md_to_html("\\*\\*cheese\\*\\*\n*\\*cheese\\*\\*"), "

**cheese**

\n

*cheese*

", "backslash test"); @@ -63,12 +71,23 @@ test_assert_equal(parse_md_to_html("```\nif time == 420:\n weed()\n```"), "testing\n
\n# title
\n  i like **cheeseburgers** and `code`
\n
\n

`

", "code block test 2"); +test_assert_equal(parse_md_to_html("> ```\n> if\n> b\n> a\n> ```"), "
\n
\nif
\n b
\na
\n
\n
", "code block test 2") + test_assert_equal(parse_md_to_html("test\n> test\n> ## TEST\n> **beach**\n> `wee`\n> # dd"), "

test

\n
\n

test

\n

TEST

\n

beach

\n

wee

\n

dd

\n
", "block quote test 1"); test_assert_equal(parse_md_to_html("> ```\n> alert('e')\n> ```"), "
\n
\nalert('e')
\n
\n
", "block quote test 2"); -//todo: ordered lists, unordered lists, tables +test_assert_equal(parse_md_to_html("> a\n\n> b"), "
\n

a

\n
\n
\n

b

\n
", "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"), "
\n
    \n
  • burger
  • \n
\n

-winter melons

\n

abcdefg

\n
    \n
  • fries
  • \n
\n
\n
    \n
  • pizza
  • \n
\n

abacus

", "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"), "
    \n
  1. a
  2. \n
  3. b
  4. \n
  5. c
  6. \n
\n

5. should fail

\n
\n
    \n
  1. d
  2. \n
  3. e
  4. \n
\n
\n

3. should fail too

", "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"), "
    \n
  1. uno
  2. \n
  3. dos
  4. \n
  5. tres
  6. \n
  7. cuatro
  8. \n
  9. cinco
  10. \n
  11. seis
  12. \n
  13. siete
  14. \n
  15. ocho
  16. \n
  17. nueve
  18. \n
  19. diez
  20. \n
  21. once
  22. \n
", "ordered lists test 2"); + +test_assert_equal(parse_md_to_html("a ^ace^ base\n```js\n^sup^\n```\ne=mc^2\n^ea^"), "

a ace base

\n
\n^sup^
\n
\n

e=mc^2

\n

ea

", "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"), "\n\n\n\n\n\n\n\n\n\n\n
abc
def
\n

a

", "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`); diff --git a/makoto.ts b/makoto.ts index 1954e10..ccaa60b 100644 --- a/makoto.ts +++ b/makoto.ts @@ -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 += `${table_item}\n\n`; + } else { + html_line += `${table_item}\n\n`; + } + } //if previous character is also newline, there hasn't been opportunity to add a

, so add it! if (chars[i-1] === "\n") { html_line = "

"; @@ -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 += ""; + 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 and replace it with a * + let split: string[] = html_line.split(""); + 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 += ""; + } + } + } + } else if (asterisk_num === 2) { + //remove the last and replace it with a ** + let split: string[] = html_line.split(""); + 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 += ""; + } + } + } + } + asterisk_num = 0; + asterisk_out_num = 0; + in_asterisk = false; + } + if (in_superscript) { + //superscript never ended + //remove the last and replace it with a ^ + let split: string[] = html_line.split(""); + 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 += ""; + } + } + } + in_superscript = false; + } + //ending table row + if (in_table) { + in_table_header = false; + html_line += "\n"; + } html += html_line; if (html_line.startsWith("

")) { html += "

\n"; - } else if ((html_line.startsWith("
  • ") || html_line.startsWith("
      ")) && in_unordered_list) { + } else if ((html_line.startsWith("
    • ") || html_line.startsWith("
        ") || html_line.startsWith("
          ")) && (in_unordered_list || in_ordered_list)) { html += "\n"; - if (i === chars.length-1) { + if (in_ordered_list) { + ordered_list_num++; + } + if (i === chars.length-1 && in_unordered_list) { html += "
      "; + //set it to false, not that it matters + blockquote_list = false; + } else if (i === chars.length-1 && in_ordered_list) { + html += ""; + //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 += ""; + in_blockquote = false; + } else if (in_blockquote && char === "\n" && chars[i-1] === "\n") { + //two new lines in a row means blockquote ends + html += "\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 += "
      \n"; continue; } else if (in_blockquote && chars[i-1] === "\n" && (char !== ">" || chars[i+1] !== " ")) { - html_line = "
      \n"; + if (blockquote_list) { + //end list if list started in blockquote and blockquote ends + blockquote_list = false; + if (in_unordered_list) { + html += "
    \n\n"; + } else if (in_ordered_list) { + html += "\n\n"; + } + ordered_list_num = 0; + in_ordered_list = false; + in_unordered_list = false; + } else { + html += "\n"; + } in_blockquote = false; } else if (char === ">" && 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 += " "; - } else if (in_blockquote && ((char === " " && chars.slice(i-2, i) === "\n>") || (char === ">" && chars[i-1] === "\n" && chars[i+1] === " "))) { + if (in_blockquote && ((char === " " && chars.slice(i-2, i) === "\n>") || (char === ">" && chars[i-1] === "\n" && chars[i+1] === " "))) { //do not add the blockquote syntax thing "> " to the codeblock + } else if (char === " " && space_start) { + html_line += " "; } 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 = "
      \n
    • "; + blockquote_list = false; } else { html_line = "
    • "; } 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 += "
    \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 += "\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 = "
      \n
    1. "; + in_ordered_list = true; + } else { + html_line = "
    2. "; + } + 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 += "
    \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 += "\n\n"; + } else { + //just a new table row + html += "\n"; + } + continue; + } else if (in_table && char === "|") { + if (in_table_header) { + html_line += `\n`; + } else { + html_line += `\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_item}${table_item}
    \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 += ""; + continue; + } else { + in_superscript = true; + html_line += ""; + continue; + } + } // if (end_add_char) { html_line += char; diff --git a/styles/makoto.css b/styles/makoto.css new file mode 100644 index 0000000..34b2689 --- /dev/null +++ b/styles/makoto.css @@ -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; +} diff --git a/styles/style.css b/styles/style.css new file mode 100644 index 0000000..613e17a --- /dev/null +++ b/styles/style.css @@ -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; +} diff --git a/web.ts b/web.ts new file mode 100644 index 0000000..f6781fd --- /dev/null +++ b/web.ts @@ -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; +}