commit abd71d62633f096a5ac8c417260cfe2c211b70e9 Author: Daniel Bulant Date: Sat Mar 8 22:39:43 2025 +0100 initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..82b2b9e --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# the shebang is ignored, but nice for editors + +if type -P lorri &>/dev/null; then + eval "$(lorri direnv)" +else + echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]' + use nix +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5feb907 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +node_modules \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..61510ec --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "markdown" +version = "0.1.0" +edition = "2021" + +[dependencies] +markdown = { version = "1.0.0-alpha.21", features = ["serde"]} +itertools = "0.13" +serde_json = "1.0.133" +serde_yaml = "0.9.34" +serde = { version = "1.0.215", features = ["derive"] } +regex = "1.11.1" +clap = { version = "4.5.21", features = ["derive"] } +syntext = "5.0" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3df5ac --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# MDSvexRS + +A faster markdown preprocessor for svelte. Compiles `.md` files into `.svelte` with non-reactive blocks wrapped in direct HTML for faster compilation and rendering. + +Note that this, like the original MDSvex, trusts it's input and doesn't escape HTML or script files. diff --git a/highlighter/hanekawa.json b/highlighter/hanekawa.json new file mode 100644 index 0000000..a6defb5 --- /dev/null +++ b/highlighter/hanekawa.json @@ -0,0 +1,508 @@ +{ + "type": "dark", + "name": "Hanekawa", + "colors": { + "foreground": "#c3c3c3", + "editor.foreground": "#F8F8F2", + "dropdown.foreground": "#c3c3c3", + "panelTitle.activeForeground": "#c3c3c3", + "panelTitle.inactiveForeground": "#c3c3c3", + "gitDecoration.ignoredResourceForeground": "#6f6f6f", + "list.invalidItemForeground": "#6f6f6f", + "list.errorForeground": "#FF5555", + "editorError.foreground": "#FF5555", + "input.background": "#1b1820AA", + "peekViewResult.background": "#1b1820", + "peekViewResult.fileForeground": "#988ccb", + "peekViewTitleDescription.foreground": "#988ccb", + "list.focusBackground": "#5a3b5d", + "quickInput.list.focusBackground": "#5a3b5dAA", + "quickInput.foreground": "#c3c3c3", + "peekViewTitleLabel.foreground": "#c3c3c3", + "selection.background": "#5a3b5d", + "editorGutter.modifiedBackground": "#152A3D", + "editorGutter.addedBackground": "#142F14", + "editorGutter.deletedBackground": "#1D1D1D", + "editorUnnecessaryCode.border": "#72737A", + "editorGroup.dropBackground": "#262626AA", + "panelSection.dropBackground": "#262626AA", + "statusBarItem.prominentBackground": "#5a3b5dAA", + "statusBarItem.prominentForeground": "#ebd6fc", + "keybindingLabel.background": "#2e2939", + "keybindingLabel.foreground": "#c7c7c7", + "keybindingLabel.bottomBorder": "#211c23", + "settings.sashBorder": "#211c23", + "tab.hoverBackground": "#262129AA", + "list.activeSelectionBackground": "#5a3b5d", + "peekViewResult.selectionBackground": "#5a3b5d", + "list.hoverBackground": "#5a3b5d7a", + "editor.selectionHighlightBackground": "#262626", + "editor.linkedEditingBackground": "#262626", + "editor.selectionBackground": "#5a3b5dAA", + "minimap.selectionHighlight": "#5a3b5d", + "list.activeSelectionForeground": "#ebd6fc", + "list.focusForeground": "#ebd6fc", + "pickerGroup.foreground": "#988ccb", + "descriptionForeground": "#988ccb", + "gitlens.trailingLineForegroundColor": "#988ccb99", + "input.placeholderForeground": "#988ccb", + "list.deemphasizedForeground": "#988ccb", + "inputValidation.errorBackground": "#36151F", + "minimap.errorHighlight": "#FF5555AA", + "inputValidation.errorForeground": "#FF5555", + "inputValidation.warningBackground": "#191a18", + "list.inactiveSelectionBackground": "#36233899", + "list.inactiveSelectionForeground": "#c3c3c3", + "editor.background": "#100d11", + "errorForeground": "#FF5555", + "tab.activeBackground": "#262129", + "panelSectionHeader.background": "#0f0d10", + "focusBorder": "#211c23", + "pickerGroup.border": "#211c23", + "tab.border": "#211c23", + "tree.indentGuidesStroke": "#211c23", + "sideBar.border": "#211c23", + "editorIndentGuide.background": "#211c23", + "editorIndentGuide.activeBackground": "#211c23", + "inputOption.activeBorder": "#211c23", + "textBlockQuote.border": "#211c23", + "dropdown.border": "#211c23", + "input.border": "#211c23", + "activityBar.border": "#211c23", + "editorGroup.border": "#211c23", + "editorOverviewRuler.border": "#211c23", + "editorError.border": "#36151F", + "editorWarning.border": "#211c23", + "editorInfo.border": "#211c23", + "editorHint.border": "#211c23", + "editorHoverWidget.border": "#211c23", + "debugExceptionWidget.border": "#211c23", + "merge.border": "#211c23", + "panel.border": "#211c23", + "statusBar.border": "#211c23", + "titleBar.border": "#211c23", + "notificationCenter.border": "#211c23", + "notifications.border": "#211c23", + "editorCursor.background": "#100d11", + "terminalCursor.background": "#100d11", + "terminalCursor.foreground": "#7660A4", + "editorSuggestWidget.border": "#100d11", + "panel.background": "#100d11", + "sideBar.background": "#100d11", + "tab.inactiveBackground": "#100d11", + "editorGroupHeader.tabsBackground": "#100d11", + "dropdown.background": "#100d11", + "editorLineNumber.foreground": "#777495", + "gitlens.gutterForegroundColor": "#777495", + "checkbox.border": "#7e7997", + "checkbox.foreground": "#7660A4", + "toolbar.hoverBackground": "#262129", + "toolbar.hoverOutline": "#262129", + "toolbar.activeBackground": "#262129", + "textLink.foreground": "#7660A4AA", + "textLink.activeForeground": "#7660A4", + "settings.modifiedItemIndicator": "#7660A4", + "textBlockQuote.background": "#281f28", + "textCodeBlock.background": "#281f28", + "textSeparator.foreground": "#211c23", + "editorCursor.foreground": "#7660A4", + "editorWidget.border": "#7660A4", + "list.highlightForeground": "#8968dc", + "editorLineNumber.activeForeground": "#7660A4", + "debugIcon.breakpointForeground": "#7660A4", + "tab.activeBorder": "#7660A4", + "peekView.border": "#7660A4", + "statusBar.background": "#7660A4", + "statusBarItem.remoteBackground": "#5a3b5d", + "statusBarItem.remoteForeground": "#ebd6fc", + "statusBar.noFolderBackground": "#7660A4", + "panelTitle.activeBorder": "#7660A4", + "statusBar.debuggingBackground": "#7660A4", + "editorBracketMatch.border": "#7660A4", + "gitDecoration.modifiedResourceForeground": "#7660A4", + "activityBar.foreground": "#7660A4", + "activityBarBadge.background": "#7660A4", + "activityBarBadge.foreground": "#141115", + "badge.background": "#7660A4", + "badge.foreground": "#141115", + "progressBar.background": "#7660A4", + "scrollbarSlider.hoverBackground": "#7660A49A", + "notificationToast.border": "#7660A49A", + "scrollbarSlider.background": "#7660A45A", + "scrollbarSlider.activeBackground": "#7660A49A", + "activityBar.inactiveForeground": "#7660A4BB", + "button.background": "#2e2939", + "button.foreground": "#c7c7c7", + "button.secondaryForeground": "#c7c7c7BB", + "button.secondaryBackground": "#2e2939CC", + "editor.lineHighlightBackground": "#14111577", + "gitlens.trailingLineBackgroundColor": "#1e1d1f77", + "editor.lineHighlightBorder": "#14111500", + "sideBarSectionHeader.background": "#141115", + "activityBar.background": "#141115", + "editorGutter.background": "#00000000", + "symbolIcon.arrayForeground": "#86dbfd", + "symbolIcon.constantForeground": "#86dbfd", + "symbolIcon.enumeratorMemberForeground": "#86dbfd", + "symbolIcon.booleanForeground": "#d55fde", + "symbolIcon.classForeground": "#e39656", + "symbolIcon.enumeratorForeground": "#7ceec8", + "symbolIcon.fieldForeground": "#7ceec8", + "symbolIcon.constructorForeground": "#e39656", + "symbolIcon.functionForeground": "#9a97f4", + "symbolIcon.colorForeground": "#7660A4", + "symbolIcon.eventForeground": "#7660A4", + "symbolIcon.fileForeground": "#7e7997", + "symbolIcon.folderForeground": "#7e7997", + "symbolIcon.interfaceForeground": "#F6E3CC", + "editor.findMatchBackground": "#5a3b5d", + "editor.findMatchBorder": "#7660A4", + "editor.wordHighlightStrongBackground": "#262626AA", + "editor.snippetTabstopHighlightBackground": "#262626AA", + "editor.findMatchHighlightBackground": "#305D49AA", + "peekViewEditor.matchHighlightBackground": "#362338", + "editor.wordHighlightBackground": "#262626AA", + "editorOverviewRuler.findMatchForeground": "#362338", + "searchEditor.findMatchBackground": "#305D49", + "peekViewResult.matchHighlightBackground": "#362338", + "editorWidget.background": "#0f0d10", + "debugToolBar.background": "#0f0d10", + "debugView.stateLabelBackground": "#262129", + "debugTokenExpression.string": "#F6E3CC", + "debugTokenExpression.boolean": "#d55fde", + "debugTokenExpression.value": "#988ccb", + "debugTokenExpression.number": "#86dbfd", + "debugTokenExpression.error": "#FF5555", + "debugTokenExpression.name": "#e39656", + "breadcrumb.foreground": "#777495", + "breadcrumb.focusForeground": "#988ccb", + "breadcrumb.activeSelectionForeground": "#9a97f4", + "testing.iconFailed": "#FF5555", + "testing.iconErrored": "#FF5555", + "testing.runAction": "#7660A4", + "welcomePage.tileBackground": "#281f28AA", + "welcomePage.buttonBackground": "#281f28AA", + "welcomePage.tileHoverBackground": "#5a3b5dAA", + "welcomePage.buttonHoverBackground": "#5a3b5dAA", + "testing.message.hint.decorationForeground": "#988ccb", + "settings.focusedRowBackground": "#262129", + "notebook.focusedRowBorder": "#262129", + "notebook.rowHoverBackground": "#1e1d1f", + "debugView.valueChangedHighlight": "#5a3b5d99", + "editorSuggestWidget.background": "#0f0d10", + "notifications.background": "#0f0d1080", + "peekViewTitle.background": "#0f0d10", + "widget.shadow": "#1111115a", + "scrollbar.shadow": "#1111113a", + "titleBar.activeBackground": "#0f0d10", + "titleBar.inactiveBackground": "#0f0d10", + "gitlens.gutterBackgroundColor": "#281f28", + "peekViewEditor.background": "#281f28", + "peekViewEditorGutter.background": "#281f28", + "terminal.ansiRed": "#E356A7", + "terminal.ansiGreen": "#42E66C", + "terminal.ansiYellow": "#EFA554", + "terminal.ansiBlue": "#9B6BDF", + "terminal.ansiMagenta": "#E64747", + "terminal.ansiCyan": "#75D7EC", + "editor.stackFrameHighlightBackground": "#5a3b5d", + "debugIcon.breakpointCurrentStackframeForeground": "#5a3b5d", + "debugIcon.breakpointDisabledForeground": "#6f6f6f", + "debugIcon.breakpointUnverifiedForeground": "#72737A", + "debugIcon.startForeground": "#7660A4", + "debugIcon.continueForeground": "#7660A4", + "debugIcon.stepOverForeground": "#9B6BDF", + "debugIcon.stepIntoForeground": "#EFA554", + "debugIcon.stopForeground": "#FF5555", + "debugIcon.stepOutForeground": "#75D7EC", + "debugIcon.restartForeground": "#42E66C", + "debugConsole.infoForeground": "#988ccb", + "debugConsole.errorForeground": "#FF5555", + "debugConsole.sourceForeground": "#9a97f4", + "debugIcon.breakpointStackframeForeground": "#362338", + "editor.focusedStackFrameHighlightBackground": "#362338", + "diffEditor.removedTextBackground": "#1D1D1D50", + "diffEditor.insertedTextBackground": "#142F1450", + "gitlens.gutterUncommittedForegroundColor": "#152A3D", + "gitlens.lineHighlightBackgroundColor": "#142F1450", + "charts.red": "#E356A7", + "charts.blue": "#9B6BDF", + "charts.yellow": "#EFA554", + "charts.orange": "#9a97f4", + "charts.green": "#42E66C", + "charts.purple": "#7ceec8", + "editorBracketHighlight.foreground1": "#F8F8F2", + "editorBracketHighlight.foreground2": "#7ceec8", + "editorBracketHighlight.foreground3": "#e39656", + "editorBracketHighlight.foreground4": "#9a97f4", + "editorBracketHighlight.foreground5": "#7ceec8", + "editorBracketHighlight.foreground6": "#86dbfd", + "editorBracketHighlight.unexpectedBracket.foreground": "#FF5555" + }, + "semanticHighlighting": true, + "semanticTokenColors": { + "variable.readonly.local": "#F8F8F2", + "function.declaration": "#e39656" + }, + "tokenColors": [ + { + "scope": [ + "emphasis" + ], + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": [ + "strong" + ], + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": [ + "invalid" + ], + "settings": { + "foreground": "#FF5555", + "fontStyle": "underline strikethrough" + } + }, + { + "scope": [ + "invalid.deprecated" + ], + "settings": { + "foreground": "#988ccb", + "fontStyle": "underline italic" + } + }, + { + "name": "Underlined markup", + "scope": [ + "markup.underline" + ], + "settings": { + "fontStyle": "underline" + } + }, + { + "name": "Bold markup", + "scope": [ + "markup.bold" + ], + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup italic", + "scope": [ + "markup.italic" + ], + "settings": { + "fontStyle": "italic" + } + }, + { + "scope": [ + "support.type.property-name.json", + "meta.object-literal.key", + "variable.object.property", + "variable.other.readwrite.alias", + "variable.other.property", + "meta.brace" + ], + "settings": { + "foreground": "#F8F8F2", + "fontStyle": "bold" + } + }, + { + "scope": [ + "source", + "punctuation.definition.parameters", + "punctuation.definition.block", + "text.html.markdown", + "variable.other.constant", + "meta.definition.variable", + "variable.other.constant.js", + "variable.other.constant.ts" + ], + "settings": { + "foreground": "#F8F8F2" + } + }, + { + "scope": [ + "variable.parameter", + "meta.parameters", + "meta.function-call.arguments", + "markup.inline.raw.string.markdown", + "string" + ], + "settings": { + "foreground": "#F6E3CC" + } + }, + { + "scope": [ + "constant.language.json", + "keyword", + "storage.type", + "storage.modifier", + "keyword.operator.optional", + "keyword.operator.new", + "keyword.operator.instanceof", + "keyword.operator.expression.typeof", + "constant.language.boolean", + "variable.language.this", + "constant.language.undefined", + "constant.language.java", + "constant.language.python", + "support.type.primitive.ts", + "markup.heading.setext", + "punctuation.definition.raw.markdown" + ], + "settings": { + "foreground": "#d55fde" + } + }, + { + "scope": [ + "variable.other.constant", + "meta.definition.method", + "meta.definition.function", + "meta.embedded.line", + "markup.inline.raw.string", + "variable.css", + "meta.function", + "support.class", + "meta.method.identifier", + "variable.other.readwrite.alias" + ], + "settings": { + "foreground": "#e39656" + } + }, + { + "scope": [ + "keyword.operator", + "entity.name.tag", + "meta.template.expression", + "punctuation.definition.list.begin", + "support.class.component.svelte", + "punctuation.definition.link.restructuredtext", + "punctuation.definition.typeparameters" + ], + "settings": { + "foreground": "#ff8fec" + } + }, + { + "scope": [ + "entity.name.type.class", + "meta.type", + "variable.other.object", + "entity.name.type", + "markup.heading", + "string.other.link.title", + "storage.modifier.import", + "meta.import", + "storage.type.java", + "support.type.property-name", + "meta.property-name" + ], + "settings": { + "foreground": "#7ceec8" + } + }, + { + "scope": [ + "support.type.primitive", + "entity.name.type.interface", + "variable.parameter.function-call" + ], + "settings": { + "foreground": "#7ceec8", + "fontStyle": "italic" + } + }, + { + "scope": [ + "meta.function-call", + "meta.decorator", + "meta.link", + "meta.method-call", + "meta.function-call.ts", + "storage.type.annotation", + "meta.declaration.annotation", + "support.function", + "meta.assertion.look-ahead.regexp", + "entity.name.function" + ], + "settings": { + "foreground": "#9a97f4" + } + }, + { + "scope": [ + "entity.other.attribute-name" + ], + "settings": { + "foreground": "#9a97f4", + "fontStyle": "italic" + } + }, + { + "scope": [ + "constant.numeric", + "support.type.object.module", + "variable.other.enummember", + "meta.enum.declaration", + "constant.other.color", + "variable.language.special", + "support.constant", + "meta.type.parameters", + "fenced_code.block.language.markdown", + "entity.name.section.markdown" + ], + "settings": { + "foreground": "#86dbfd" + } + }, + { + "scope": [], + "settings": { + "foreground": "#86dbfd", + "fontStyle": "italic" + } + }, + { + "scope": [ + "comment", + "markup.quote" + ], + "settings": { + "foreground": "#79769f", + "fontStyle": "italic" + } + }, + { + "scope": [ + "markup.fenced_code.block" + ], + "settings": { + "fontStyle": "bold" + } + }, + { + "scope": [], + "settings": { + "foreground": "#988ccb" + } + } + ] +} \ No newline at end of file diff --git a/highlighter/index.js b/highlighter/index.js new file mode 100644 index 0000000..924be4b --- /dev/null +++ b/highlighter/index.js @@ -0,0 +1,95 @@ +import { createHighlighter } from 'shiki' +import { readFileSync } from 'fs' +import readline from "readline/promises" +import { + transformerNotationDiff, + transformerNotationHighlight, + transformerNotationWordHighlight, + transformerNotationErrorLevel, + transformerMetaHighlight, + transformerMetaWordHighlight, + transformerNotationFocus +} from "@shikijs/transformers" + +const theme = JSON.parse(readFileSync('hanekawa.json', 'utf-8')) + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +const highlighter = createHighlighter({ + langs: ["javascript", "rust", "c#", "c", "asm", "sh", "ts"], + themes: [theme] +}) + +const tokenMap = { + fn: "meta.declaration.annotation" +} + +let loadedLangs; +let sum = 0 + +function time(start) { + let time = performance.now() - start + sum += time + // console.error(sum) +} + +rl.on('line', async (line) => { + let start = performance.now(); + const data = JSON.parse(line) + let { lang, inline, code, meta } = data + if(lang[0] == ".") { + let scope = lang.slice(1) + let color + if(tokenMap[scope]) scope = tokenMap[scope] + for(let tokens of theme.tokenColors) { + if(tokens.scope?.includes(scope)) { + color = tokens.settings.foreground + break + } + } + let html + // shiki does this for us, but we need to do it manually here + code = simplehtmlentities(code) + if(color) { + html = `${code}` + } else { + html = code + } + time(start) + console.log(JSON.stringify({ html, elapsed: performance.now() - start, sum })) + return + } + let shiki = await highlighter + if(!loadedLangs) loadedLangs = shiki.getLoadedLanguages() + if(!loadedLangs.includes(lang)) { + await shiki.loadLanguage(lang) + loadedLangs = shiki.getLoadedLanguages() + } + let html = shiki.codeToHtml(code, { + lang, + structure: inline ? 'inline' : 'classic', + theme: "Hanekawa", + meta: { + "data-pretty-code": "", + __raw: meta + }, + transformers: [ + transformerNotationDiff(), + transformerNotationHighlight(), + transformerNotationWordHighlight(), + transformerNotationErrorLevel(), + transformerNotationFocus() + ] + }) + if(inline) html = `${html}` + time(start) + let out = JSON.stringify({ html, elapsed: performance.now() - start, sum }); + console.log(out) +}) + +function simplehtmlentities(str) { + return str.replace(/&/g, "&").replace(//g, ">") +} \ No newline at end of file diff --git a/highlighter/package.json b/highlighter/package.json new file mode 100644 index 0000000..05b07a4 --- /dev/null +++ b/highlighter/package.json @@ -0,0 +1,15 @@ +{ + "name": "highlighter", + "version": "1.0.0", + "description": "", + "public": false, + "main": "index.js", + "type": "module", + "scripts": {}, + "dependencies": { + "@shikijs/transformers": "^1.22.2", + "shiki": "^1.24.0" + }, + "keywords": [], + "author": "Daniel Bulant" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d07b689 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "markdown", + "scripts": { + "build": "cargo build --release" + }, + "dependencies": { + "highlighter": "workspace:*" + } +} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b8b9398 --- /dev/null +++ b/shell.nix @@ -0,0 +1,38 @@ +{ pkgs ? import {}, pkgsun ? import {} }: +let + fenix = import (fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz") { }; + rust-toolchain = + fenix.default.toolchain; +in +pkgs.mkShell rec { + buildInputs = with pkgs;[ + openssl + pkg-config + cmake + zlib + rust-toolchain + + libclang + llvmPackages.clang + openssl + clang + llvmPackages.libclang.lib stdenv.cc.libc + ]; + nativeBuildInputs = with pkgs; [ + pkg-config + fontconfig + rustPlatform.bindgenHook + ]; + + preConfigure = with pkgs;'' + export BINDGEN_EXTRA_CLANG_ARGS="-isystem ${clang}/resource-root/include $NIX_CFLAGS_COMPILE" + ''; + + LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}"; + OPENSSL_DIR="${pkgs.openssl.dev}"; + OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"; + RUST_SRC_PATH = "${pkgsun.rust.packages.stable.rustPlatform.rustLibSrc}"; + RUST_PATH="${rust-toolchain}"; + RUST_LOG="debug"; + LIBCLANG_PATH = "${pkgs.llvmPackages_14.libclang.lib}/lib"; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6c15efe --- /dev/null +++ b/src/main.rs @@ -0,0 +1,657 @@ +use std::{io::{stdin, BufRead, BufReader, Read, Write}, process::{Child, ChildStdout, Command, Stdio}, sync::LazyLock, time::{Duration, Instant}}; + +use itertools::Itertools; +use markdown::{mdast::{Blockquote, Break, Code, Definition, Delete, Emphasis, FootnoteDefinition, FootnoteReference, Heading, Html, Image, ImageReference, InlineCode, InlineMath, Link, LinkReference, List, ListItem, Math, MdxFlowExpression, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Node, Paragraph, Root, Strong, Table, TableCell, TableRow, Text, ThematicBreak, Toml, Yaml}, unist::Position, Constructs}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use clap::Parser; + +#[derive(Debug)] +struct ToHtmlResult { + html: String, + svelte: bool +} + +impl ToHtmlResult { + fn new(html: String, svelte: bool) -> Self { + Self { html, svelte } + } + + fn empty() -> Self { + Self::new("".to_string(), false) + } +} + +trait ToHtml { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult; + + fn visit(&self, _ctx: &mut Context) {} +} + +impl ToHtml for Vec where T: ToHtml { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + merge(&self.iter().map(|t| t.to_html(ctx)).collect::>()) + } + + fn visit(&self, ctx: &mut Context) { + for t in self { + t.visit(ctx); + } + } +} + +impl ToHtml for Root { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + self.children.to_html(ctx) + } + + fn visit(&self, ctx: &mut Context) { + self.children.visit(ctx); + } +} + +impl ToHtml for Blockquote { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("
{}
", children.html), children.svelte) + } +} + +impl ToHtml for FootnoteDefinition { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } + + fn visit(&self, _ctx: &mut Context) { + todo!() + } +} + +impl ToHtml for MdxJsxFlowElement { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for List { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + // todo!() + let litype = match self.ordered { + true => "ol", + false => "ul" + }; + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("<{}>{}", litype, children.html, litype), children.svelte) + } +} + +impl ToHtml for MdxjsEsm { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for Toml { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for Yaml { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::empty() + } + + fn visit(&self, ctx: &mut Context) { + let value = serde_yaml::from_str(&self.value).unwrap(); + ctx.yaml = Some(value); + } +} + +impl ToHtml for Break { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::new("
".to_string(), false) + } +} + +static LANG_HINT_REGEX: LazyLock = LazyLock::new(|| regex::Regex::new(r"\{:(?[\w.]+)\}$").unwrap()); + +impl ToHtml for InlineCode { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let value = &self.value; + // if value ends with {lang} then it's a language hint + let output = if let Some(caps) = LANG_HINT_REGEX.captures(value) { + let lang = &caps["lang"]; + let code = &value[..value.len() - lang.len() - 3]; + ctx.highlight(HighlightRequest { + lang: lang.to_string(), + inline: true, + code: code.to_string(), + meta: None + }) + } else if let Some(lang) = &ctx.default_lang { + ctx.highlight(HighlightRequest { + lang: lang.clone(), + inline: true, + code: value.clone(), + meta: None + }) + } else { + format!("{}", html_encode(&self.value)) + }; + ToHtmlResult::new(output, false) + } +} + +impl ToHtml for InlineMath { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Delete { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Emphasis { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}", children.html), children.svelte) + } +} + +impl ToHtml for MdxTextExpression { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for FootnoteReference { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Html { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let value = self.value.clone(); + if value.starts_with(" ToHtmlResult { + let alt = &self.alt; + let title = self.title.as_ref().map(|t| format!(" title=\"{}\"", t)).unwrap_or_default(); + let url = &self.url; + ToHtmlResult::new(format!("\"{}\"{}", url, alt, title), false) + } +} + +impl ToHtml for ImageReference { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for MdxJsxTextElement { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for Link { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + let title = self.title.as_ref().map(|t| format!(" title=\"{}\"", t)).unwrap_or_default(); + ToHtmlResult::new(format!("{}", self.url, title, children.html), children.svelte) + } +} + +impl ToHtml for LinkReference { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Strong { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}", children.html), children.svelte) + } +} + +impl ToHtml for Text { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::new(html_encode(&self.value), false) + } +} + +impl ToHtml for Code { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let value = &self.value; + let lang = self.lang.as_ref().or(ctx.default_lang.as_ref()); + let highlighted = if let Some(lang) = lang { + ctx.highlight(HighlightRequest { + code: value.clone(), + inline: false, + lang: lang.clone(), + meta: self.meta.clone() + }) + } else { + format!("
{}
", html_encode(&self.value)) + }; + ToHtmlResult::new(highlighted, false) + } +} + +impl ToHtml for Math { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for MdxFlowExpression { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +fn slug(str: &str) -> String { + str.to_lowercase().replace(" ", "-") +} + +impl ToHtml for Heading { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + let text = self.children.iter().filter_map(|c| { + match c { + Node::Text(t) => Some(t.value.clone()), + _ => None + } + }).join(""); + let mut slug = slug(&text); + if ctx.titles.iter().any(|t| t.id == slug) { + let mut i = 1; + while ctx.titles.iter().any(|t| t.id == format!("{}-{}", slug, i)) { + i += 1; + } + slug = format!("{}-{}", slug, i); + } + ctx.titles.push(Title { + level: self.depth, + text: text.clone(), + id: slug.clone(), + pos: self.position.clone() + }); + ToHtmlResult::new(format!("\n{}\n", self.depth, slug, children.html, self.depth), children.svelte) + } +} + +impl ToHtml for Table { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}
", children.html), children.svelte) + } +} + +impl ToHtml for ThematicBreak { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::new("\n
\n".to_string(), false) + } +} + +impl ToHtml for TableRow { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}", children.html), children.svelte) + } +} + +impl ToHtml for TableCell { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}", children.html), children.svelte) + } +} + +impl ToHtml for ListItem { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("
  • {}
  • ", children.html), children.svelte) + } +} + +impl ToHtml for Definition { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Paragraph { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("

    {}

    ", children.html), children.svelte) + } +} + +macro_rules! node_impl { + ($($node:ident($name:ident)),+) => { + impl ToHtml for Node { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + match self { + $(markdown::mdast::Node::$node($name) => $name.to_html(ctx)),+ + } + } + fn visit(&self, ctx: &mut Context) { + match self { + $(markdown::mdast::Node::$node($name) => $name.visit(ctx)),+ + } + } + } + } +} + +node_impl!( + Root(root), + Blockquote(blockquote), + FootnoteDefinition(footnote_definition), + MdxJsxFlowElement(mdx_jsx_flow_element), + List(list), + MdxjsEsm(mdxjs_esm), + Toml(toml), + Yaml(yaml), + Break(rbreak), + InlineCode(inline_code), + InlineMath(inline_math), + Delete(delete), + Emphasis(emphasis), + MdxTextExpression(mdx_text_expression), + FootnoteReference(footnote_reference), + Html(html), + Image(image), + ImageReference(image_reference), + MdxJsxTextElement(mdx_jsx_text_element), + Link(link), + LinkReference(link_reference), + Strong(strong), + Text(text), + Code(code), + Math(math), + MdxFlowExpression(mdx_flow_expression), + Heading(heading), + Table(table), + ThematicBreak(thematic_break), + TableRow(table_row), + TableCell(table_cell), + ListItem(list_item), + Definition(definition), + Paragraph(paragraph) +); + +fn svelte_html_encode(string: String) -> String { + String::from("{@html `") + &string.replace("\\", "\\\\").replace("`", "\\`") + "`}" +} + +fn merge(results: &[ToHtmlResult]) -> ToHtmlResult { + let chunked = results.iter().chunk_by(|r| r.svelte); + let mut html = chunked.into_iter().map(|(svelte, results)| { + let html = results.map(|r| r.html.clone()).join(""); + ToHtmlResult::new(html, svelte) + }); + let Some(first) = html.next() else { + return ToHtmlResult::new("".to_string(), false); + }; + if let Some(second) = html.next() { + ToHtmlResult::new( + [first, second].into_iter().chain(html).map(|r| { + if r.html.is_empty() { + return "".to_string(); + } + if r.svelte { + r.html + } else { + svelte_html_encode(r.html) + } + }).join(""), + true + ) + } else { + first + } +} + +// from markdown-rs +pub fn html_encode(value: &str) -> String { + let encode_html = true; // originally a param + // It’ll grow a bit bigger for each dangerous character. + let mut result = String::with_capacity(value.len()); + let bytes = value.as_bytes(); + let mut index = 0; + let mut start = 0; + + while index < bytes.len() { + let byte = bytes[index]; + if matches!(byte, b'\0') || (encode_html && matches!(byte, b'&' | b'"' | b'<' | b'>')) { + result.push_str(&value[start..index]); + result.push_str(match byte { + b'\0' => "�", + b'&' => "&", + b'"' => """, + b'<' => "<", + // `b'>'` + _ => ">", + }); + + start = index + 1; + } + + index += 1; + } + + result.push_str(&value[start..]); + + result +} + +fn finish(res: ToHtmlResult) -> String { + if res.svelte { + res.html + } else { + svelte_html_encode(res.html) + } +} + +#[derive(Serialize)] +struct Title { + level: u8, + text: String, + id: String, + pos: Option +} + +struct Context { + child: Child, + bufread: BufReader, + yaml: Option>, + default_lang: Option, + script: Option, + args: Args, + titles: Vec, + + highlight_times: Duration, + js_times: Duration, + js_sum: f64 +} + +#[derive(Serialize)] +struct HighlightRequest { + code: String, + inline: bool, + lang: String, + meta: Option<String> +} + +#[derive(Deserialize)] +struct HighlightResponse { + html: String, + + elapsed: f64, + sum: f64 +} + +impl Context { + fn new(args: Args) -> Self { + let mut child = Command::new("node") + .arg(".") + .current_dir("highlighter") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn().unwrap(); + let stdout = child.stdout.take().unwrap(); + let bufread = BufReader::new(stdout); + + Context { child, bufread, yaml: None, titles: Vec::new(), default_lang: None, script: None, args, highlight_times: Duration::ZERO, js_times: Duration::ZERO, js_sum: 0. } + } + + fn highlight(&mut self, code: HighlightRequest) -> String { + let start = Instant::now(); + + let stdin = self.child.stdin.as_mut().unwrap(); + let data = serde_json::to_string(&code).unwrap() + "\n"; + stdin.write_all(data.as_bytes()).unwrap(); + + let mut buf = String::new(); + let _line = self.bufread.read_line(&mut buf).unwrap(); + let res: HighlightResponse = serde_json::from_str(&buf).unwrap(); + + self.highlight_times += start.elapsed(); + self.js_times += Duration::from_nanos((res.elapsed * 1_000_000.) as u64); + self.js_sum = res.sum; + res.html + } + + fn resolve_layout(&self) -> &str { + &self.args.layout + // Path::new(&self.args.layout). + } +} + +#[derive(Parser)] +struct Args { + #[arg(short, long)] + layout: String, + #[arg(short, long)] + path: String, + #[arg(long)] + timings: bool +} + +/// Converts markdown to svelte code, MDSvex alternative. Expects trusted code! +fn main() { + let options = markdown::ParseOptions { + constructs: Constructs { + attention: true, + autolink: true, + block_quote: true, + character_escape: true, + character_reference: true, + code_indented: true, + code_fenced: true, + code_text: true, + definition: true, + frontmatter: true, + gfm_autolink_literal: true, + gfm_footnote_definition: true, + gfm_label_start_footnote: true, + gfm_strikethrough: true, + gfm_table: true, + gfm_task_list_item: true, + hard_break_escape: true, + hard_break_trailing: true, + heading_atx: true, + heading_setext: true, + html_flow: true, + html_text: true, + label_start_image: true, + label_start_link: true, + label_end: true, + list_item: true, + math_flow: true, + math_text: true, + mdx_esm: false, + mdx_expression_flow: false, + mdx_expression_text: false, + mdx_jsx_flow: false, + mdx_jsx_text: false, + thematic_break: true, + }, + math_text_single_dollar: true, + gfm_strikethrough_single_tilde: true, + ..Default::default() + }; + let args = Args::parse(); + let mut ctx = Context::new(args); + let mut input = String::new(); + + + let start = Instant::now(); + stdin().read_to_string(&mut input).unwrap(); + let stdin_read = start.elapsed(); + + let start = Instant::now(); + let ast = markdown::to_mdast(&input, &options).unwrap(); + let ast_parse = start.elapsed(); + + let start = Instant::now(); + ast.visit(&mut ctx); + let ast_visit = start.elapsed(); + + if let Some(yaml) = &ctx.yaml { + if let Some(val) = yaml.get("defaultLang") { + ctx.default_lang = Some(val.as_str().unwrap().to_string()); + } + } + + let start = Instant::now(); + let res = ast.to_html(&mut ctx); + let html = finish(res); + let html_convert = start.elapsed(); + + if ctx.args.timings { + dbg!(stdin_read, ast_parse, ast_visit, html_convert, ctx.highlight_times, ctx.js_times, ctx.js_sum); + return; + } + + if let Some(yaml) = &mut ctx.yaml { + yaml.insert("titles".to_string(), serde_json::to_value(&ctx.titles).unwrap()); + } + + let value = ctx.script.clone().unwrap_or_else(|| String::from("<script></script>")); + + let script = { + let end = value.find('>').expect("Unclosed script tag (found <script but not >). May be a bug with Markdown parser.") + 1; + let mut script = value[..end].to_string(); + let layout = ctx.resolve_layout(); + script += format!("import MDXLayout from \"{}\";", layout).as_str(); + script += &value[end..]; + script + }; + + let frontmatter = (|| { + serde_json::to_string(&ctx.yaml?).ok() + })().unwrap_or("{}".to_string()); + println!("<script context=\"module\">export const metadata = {}</script>", frontmatter); + + println!("{script}"); + println!("<MDXLayout {{...metadata}}>"); + println!("{}", html); + println!("</MDXLayout>"); +} \ No newline at end of file