initial commit

This commit is contained in:
Daniel Bulant 2025-03-08 22:39:43 +01:00
commit abd71d6263
No known key found for this signature in database
10 changed files with 1352 additions and 0 deletions

9
.envrc Normal file
View file

@ -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

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
node_modules

14
Cargo.toml Normal file
View file

@ -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"

5
README.md Normal file
View file

@ -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.

508
highlighter/hanekawa.json Normal file
View file

@ -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"
}
}
]
}

95
highlighter/index.js Normal file
View file

@ -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 data-pretty-code-figure style="color: ${color}">${code}</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 = `<code data-pretty-code-figure>${html}</code>`
time(start)
let out = JSON.stringify({ html, elapsed: performance.now() - start, sum });
console.log(out)
})
function simplehtmlentities(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
}

15
highlighter/package.json Normal file
View file

@ -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"
}

9
package.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "markdown",
"scripts": {
"build": "cargo build --release"
},
"dependencies": {
"highlighter": "workspace:*"
}
}

38
shell.nix Normal file
View file

@ -0,0 +1,38 @@
{ pkgs ? import <nixpkgs> {}, pkgsun ? import <nixos-unstable> {} }:
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";
}

657
src/main.rs Normal file
View file

@ -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<T> ToHtml for Vec<T> where T: ToHtml {
fn to_html(&self, ctx: &mut Context) -> ToHtmlResult {
merge(&self.iter().map(|t| t.to_html(ctx)).collect::<Vec<_>>())
}
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!("<blockquote>{}</blockquote>", 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("<br>".to_string(), false)
}
}
static LANG_HINT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"\{:(?<lang>[\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!("<code>{}</code>", 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!("<em>{}</em>", 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("<script") {
ctx.script = Some(value);
return ToHtmlResult::empty();
}
ToHtmlResult::new(value, true)
}
}
impl ToHtml for Image {
fn to_html(&self, _ctx: &mut Context) -> 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!("<img src=\"{}\" alt=\"{}\"{}>", 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!("<a href=\"{}\"{}>{}</a>", 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!("<strong>{}</strong>", 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!("<pre><code>{}</code></pre>", 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<h{} id=\"{}\">{}</h{}>\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!("<table>{}</table>", children.html), children.svelte)
}
}
impl ToHtml for ThematicBreak {
fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult {
ToHtmlResult::new("\n<hr>\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!("<tr>{}</tr>", 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!("<td>{}</td>", 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!("<li>{}</li>", 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!("<p>{}</p>", 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
// Itll 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' => "<EFBFBD>",
b'&' => "&amp;",
b'"' => "&quot;",
b'<' => "&lt;",
// `b'>'`
_ => "&gt;",
});
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<Position>
}
struct Context {
child: Child,
bufread: BufReader<ChildStdout>,
yaml: Option<serde_json::Map<String, Value>>,
default_lang: Option<String>,
script: Option<String>,
args: Args,
titles: Vec<Title>,
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>");
}