diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d6dae9 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Makoto Markdown to HTML +The Makoto parser converts markdown to HTML, powered by several hundred lines of spaghetti code. Specifically, it is a state machine (I think) that processes the markdown character by character. + +Makoto uses the MIT license. + +## Web Editor +A web markdown editor (output in HTML, of course), is available at [makoto.prussia.dev](https://makoto.prussia.dev) or [makoto.pages.dev](https://makoto.pages.dev). + +### Query Params +The web editor features various query params. + +- `help`: if "true", will display a quick intro to Makoto-flavoured markdown +- `save`: if "true", will save the user inputted markdown to local storage, and will retrieve any existing saved text from local storage +- `ignore`: if "all", all warnings will be ignored. Else, it can be a CSV (comma separated values) of warning types to ignore + +Example: [https://makoto.prussia.dev/?help=true&ignore=unknown-language,empty-link](https://makoto.prussia.dev/?help=true&ignore=unknown-language,empty-link) + +## HTML Output +Some example CSS to style the HTML output can be found in `styles/makoto.css`. + +- Headings are given automatically generated ids ('header-0', 'header-1' etc) so url anchors (https://example.com/blog#header-1) are possible +- Newlines will be put only after headings, paragraphs, and horizontal rules, all others will not be in final output (exceptions: no newlines in the last line, and newlines in code blocks are preserved) +- Spaces at the beginning of the line are normally cut off in html, so spaces will be replaced with the html entity for spaces (  ) at the beginning of the line in code blocks +- Lines in code blocks will be split with
s +- 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`) + +## Notable Differences From Standard Markdown +- Only one newline between text is needed for a new paragraph, not two +- 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 and lists 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 + +## List of Warning Types +- `unknown-language` +- `image-incomplete` +- `link-incomplete` +- `italic-not-closed` +- `bold-not-closed` +- `superscript-not-closed` +- `blockquote-broken` +- `code-block-not-closed` +- `unordered-list-broken` +- `code-snippet-not-closed` +- `too-much-header` +- `heading-broken` +- `horizontal-rule-broken` +- `missing-image-alt` +- `empty-link` +- `weird-href` diff --git a/index.html b/index.html index fa8a089..ab762fe 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,14 @@
- +
+ +
+ +
+ See Docs +
+
diff --git a/makoto.ts b/makoto.ts index ae886d6..fe6bf6d 100644 --- a/makoto.ts +++ b/makoto.ts @@ -471,7 +471,7 @@ export function parse_md_to_html_with_warnings(md: string): ParseResult { blockquote_list = true; } continue; - } else if (char !== " " && chars[i-1] === "-" && (chars[i-2] === "\n" || i === 1)) { + } else if (char !== " " && char !== "-" && chars[i-1] === "-" && (chars[i-2] === "\n" || i === 1)) { warnings.push({ type: "unordered-list-broken", message: "Missing space after unordered list", diff --git a/styles/style.css b/styles/style.css index 87f1c62..7504a8c 100644 --- a/styles/style.css +++ b/styles/style.css @@ -26,7 +26,7 @@ html, body { #editor { box-sizing: border-box; width: 95%; - width: calc(100% - 58px); + width: 100%; height: 95%; margin: 0; word-break: break-all; @@ -56,15 +56,32 @@ html, body { color: #2d2d2d; } -.line-warning { +#preview-tools { + font-family: Arial, sans-serif; + float: left; + width: 100%; +} + +.line-warning::before { position: relative; display: inline-block; width: 0; left: -58px; margin-top: -1px; font-size: 0.9em; -} - -.line-warning::after { content: "⚠️"; } + +.mobile-only { + display: none; +} + +#see-docs { + float: right; +} + +@media screen and (max-width: 950px) { + .mobile-only { + display: block; + } +} diff --git a/web.ts b/web.ts index 2c27576..4136026 100644 --- a/web.ts +++ b/web.ts @@ -2,7 +2,6 @@ import { parse_md_to_html_with_warnings, ParseResult } from './makoto.js'; import type { Warning } from './endosulfan.js'; /*todo: -- FIX not being able to delete at start of line when there is warning on the line - show multiple warnings per line - show warnings that do not have line number - show warning type, ability to ignore warnings based on type @@ -12,18 +11,24 @@ 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 html_toggle: HTMLInputElement = document.getElementById("html-toggle")! as HTMLInputElement; +let show_html: boolean = false; let unedited: boolean = true; +let use_local_storage: boolean = false; + +let ignore_all_warnings: boolean = false; +let ignore_warnings: string[] = []; + function render_warnings(warnings: Warning[]) { - document.querySelectorAll(".line-warning").forEach((item: Element) => item.remove()); + document.querySelectorAll(".line-warning").forEach((item: Element) => item.classList.remove("line-warning")); warnings.forEach((warning: Warning) => { + if (ignore_warnings.includes(warning.type)) return; if (warning.line_number) { - let line: Element = editor.children[warning.line_number-1]; - let warning_span: HTMLElement = document.createElement("SPAN"); - warning_span.classList.add("line-warning"); - warning_span.title = warning.message; - line.insertBefore(warning_span, line.childNodes[0]); + let line: HTMLElement = editor.children[warning.line_number-1] as HTMLElement; + line.classList.add("line-warning"); + line.title = warning.message; } }); } @@ -31,9 +36,18 @@ function render_warnings(warnings: Warning[]) { const refresh_html = () => { //go through the lines and get the editor text let editor_text: string = Array.from(editor.children).map((item) => item.textContent).reduce((added, current) => added+"\n"+current); + if (use_local_storage) { + localStorage.setItem("markdown", editor_text); + } let parsed: ParseResult = parse_md_to_html_with_warnings(editor_text); - render_warnings(parsed.warnings); - preview.innerHTML = parsed.html; + if (!ignore_all_warnings) { + render_warnings(parsed.warnings); + } + if (show_html) { + preview.innerText = parsed.html; + } else { + preview.innerHTML = parsed.html; + } }; editor.addEventListener("keyup", () => { @@ -74,14 +88,24 @@ const theme_change = () => { dark_theme_toggle.addEventListener("change", theme_change); +const html_change = () => { + let changed: boolean = show_html !== html_toggle.checked; + if (changed) { + show_html = show_html ? false : true; + refresh_html(); + } +} + +html_toggle.addEventListener("change", html_change); + //applies if the browser remembers what the user typed in if (editor.innerText.trim() !== "markdown goes here...") { unedited = false; } -//give a quick intro to markdown let params: URLSearchParams = new URLSearchParams(window.location.search); if (params.get("help") === "true") { + //give a quick intro to markdown (editor.children[0] as HTMLElement).innerText = "# Makoto Markdown Parser"; let extra_lines: string[] = [ "This markdown parser is powered by spaghetti. You can have **bold text** or *italic text*, and even ^superscripts!^", @@ -108,8 +132,36 @@ if (params.get("help") === "true") { additional_line.innerHTML = extra_lines[i]; editor.appendChild(additional_line); } - // +} + +if (params.get("save") === "true") { + //save to local storage, and retrieve from it + if (localStorage) { + use_local_storage = true; + } + let stored_markdown: string = localStorage.getItem("markdown"); + if (stored_markdown) { + let extra_lines: string[] = stored_markdown.split("\n"); + (editor.children[0] as HTMLElement).innerText = extra_lines.shift(); + for (let i=0; i < extra_lines.length; i++) { + let additional_line: HTMLElement = document.createElement("LI"); + //needs to be .innerHTML so the html space entity can be parsed + additional_line.innerHTML = extra_lines[i]; + editor.appendChild(additional_line); + } + unedited = false; + } +} + +if (params.get("ignore")) { + //allow user to specify warnings to ignore + if (params.get("ignore") === "all") { + ignore_all_warnings = true; + } else { + ignore_warnings = params.get("ignore").split(","); + } } refresh_html(); theme_change(); +html_change();