mirror of
https://github.com/danbulant/mdsvexrs
synced 2026-05-19 04:08:47 +00:00
initial publish version
This commit is contained in:
parent
30dbf37ada
commit
7ab4b42cf2
7 changed files with 221 additions and 69 deletions
58
packages/vite/README.md
Normal file
58
packages/vite/README.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# MDSvexRs
|
||||
|
||||
Vite/sveltekit plugin that uses `mdsvexrs-wasm` to convert markdown to svelte files in an optimized way.
|
||||
This plugin generates raw html (using `{@html}` tags) that result in faster build times (less js generated, less js for esbuild to process), faster changes (setting innerHtml is faster than using JSDOM) and slightly faster loading times.
|
||||
|
||||
Main use case was allowing ~2k LoC markdown with embedded svelte to build with less than 20GB of RAM (yes, that's how much esbuild used).
|
||||
|
||||
## Use
|
||||
|
||||
Install this package
|
||||
|
||||
```
|
||||
pnpm install mdsvexrs
|
||||
```
|
||||
|
||||
And add it to preprocess under `svelte.config.js`:
|
||||
|
||||
```js
|
||||
import { mdsvexrs } from 'mdsvexrs'
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: ...,
|
||||
extensions: ['.svelte', '.md'],
|
||||
|
||||
preprocess: [
|
||||
mdsvexrs({ layout: "$lib/layout.svelte" })
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
Note that a layout *is* required and requires a static path - use `$lib` prefix and put your layout under `src/lib`. Layout is a svelte file
|
||||
that accepts route data and markdown frontmatter as inputs.
|
||||
|
||||
As a minimal layout, just render children:
|
||||
|
||||
```svelte
|
||||
<!-- src/lib/layout.svelte -->
|
||||
<slot />
|
||||
```
|
||||
|
||||
Frontmatter is passed as props, and props passed to markdown component (such as sveltekit `data`) are also passed as props to layout.
|
||||
|
||||
Note that markdown scripts are assumed to be 'old' svelte and not runes. They use `$$props` to pass props to layout, which won't work in runes mode (i.e. if you use `$state` or similar in .md `<script>`).
|
||||
|
||||
## Added features
|
||||
|
||||
Inline code highlighting - use it either via appending `{:lang}` inside inline code or by setting `defaultLang` in frontmatter.
|
||||
|
||||
## Differences from MDSvex
|
||||
|
||||
- Layout is required
|
||||
- Most svelte syntax is not supported - this library uses HTML oriented markdown parser, which is then passed unescaped to svelte.
|
||||
- some easy common fixes are simply quoting argument values if they contain spaces, or moving template logic into separate components and just referencing them
|
||||
- custom html tags need to be enumerated in config (`customTags: ['a']` in `mdsvexrs({ ... })`). They are still imported from layout.
|
||||
- they are uppercased during import and used as such, so the above will result in `<A href...></A>`.
|
||||
- note that overusing custom tags does come with a performance penalty, especially with very common tags like `p` or `code`.
|
||||
- multiple layouts are not supported
|
||||
1
packages/vite/index.d.ts
vendored
1
packages/vite/index.d.ts
vendored
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
export interface Options {
|
||||
layout: string;
|
||||
customTags?: string[];
|
||||
}
|
||||
|
||||
interface Plugin {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,26 @@
|
|||
import * as wasm from "mdsvexrs-wasm"
|
||||
|
||||
/**
|
||||
* @param {import("./").Options} options
|
||||
* @returns {import("./").Plugin}
|
||||
*/
|
||||
export function mdsvexrs(options) {
|
||||
let opts = wasm.get_default_options()
|
||||
opts.layout = options.layout
|
||||
if (options.customTags) {
|
||||
options.customTags.forEach(tag => {
|
||||
opts.add_custom_tag(tag)
|
||||
})
|
||||
}
|
||||
return {
|
||||
name: 'mdsvexrs',
|
||||
markup: ({ content, filename }) => {
|
||||
if(!filename || !filename.endsWith('.md')) return
|
||||
|
||||
const code = wasm.render(content, opts)
|
||||
|
||||
return {
|
||||
code: wasm.render(content, options.layout)
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
packages/wasm/README.md
Normal file
3
packages/wasm/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# MDSvexRs-wasm
|
||||
|
||||
A library to render markdown into svelte components. Internally used by `mdsvexrs`.
|
||||
|
|
@ -5,36 +5,44 @@ use mdsvexrs::Context;
|
|||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
// #[wasm_bindgen]
|
||||
// pub struct Options {
|
||||
// layout: String,
|
||||
// // path: String,
|
||||
// }
|
||||
|
||||
// #[wasm_bindgen]
|
||||
// impl Options {
|
||||
// #[wasm_bindgen(getter)]
|
||||
// pub fn layout(&self) -> String {
|
||||
// self.layout.clone()
|
||||
// }
|
||||
|
||||
// #[wasm_bindgen(setter)]
|
||||
// pub fn set_layout(&mut self, layout: String) {
|
||||
// self.layout = layout;
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[wasm_bindgen]
|
||||
// pub fn get_default_options() -> Options {
|
||||
// Options {
|
||||
// layout: String::new(),
|
||||
// }
|
||||
// }
|
||||
#[wasm_bindgen]
|
||||
pub struct Options {
|
||||
layout: String,
|
||||
custom_tags: Vec<String>,
|
||||
// path: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn render(contents: &str, layout: &str) -> String {
|
||||
impl Options {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn layout(&self) -> String {
|
||||
self.layout.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_layout(&mut self, layout: String) {
|
||||
self.layout = layout;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn add_custom_tag(&mut self, tag: String) {
|
||||
self.custom_tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn get_default_options() -> Options {
|
||||
Options {
|
||||
layout: String::new(),
|
||||
custom_tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn render(contents: &str, opts: &Options) -> String {
|
||||
Context::new(mdsvexrs::MdsvexrsOptions {
|
||||
layout: layout.to_string(),
|
||||
layout: opts.layout.to_string(),
|
||||
custom_tags: opts.custom_tags.to_vec(),
|
||||
// path: options.path,
|
||||
})
|
||||
.convert(contents)
|
||||
|
|
|
|||
148
src/lib.rs
148
src/lib.rs
|
|
@ -32,6 +32,13 @@ impl ToHtmlResult {
|
|||
Self { html, svelte }
|
||||
}
|
||||
|
||||
fn from_wrapped(html: (String, bool), svelte: bool) -> Self {
|
||||
Self {
|
||||
html: html.0,
|
||||
svelte: html.1 || svelte,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self::new("".to_string(), false)
|
||||
}
|
||||
|
|
@ -71,8 +78,8 @@ impl ToHtml for Root {
|
|||
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),
|
||||
ToHtmlResult::from_wrapped(
|
||||
ctx.wrap_in_tag("blockquote", "", children.html),
|
||||
children.svelte,
|
||||
)
|
||||
}
|
||||
|
|
@ -102,8 +109,8 @@ impl ToHtml for List {
|
|||
false => "ul",
|
||||
};
|
||||
let children = self.children.to_html(ctx);
|
||||
ToHtmlResult::new(
|
||||
format!("<{}>{}</{}>", litype, children.html, litype),
|
||||
ToHtmlResult::from_wrapped(
|
||||
ctx.wrap_in_tag(litype, "", children.html),
|
||||
children.svelte,
|
||||
)
|
||||
}
|
||||
|
|
@ -133,8 +140,9 @@ impl ToHtml for Yaml {
|
|||
}
|
||||
|
||||
impl ToHtml for Break {
|
||||
fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult {
|
||||
ToHtmlResult::new("<br>".to_string(), false)
|
||||
fn to_html(&self, ctx: &mut Context) -> ToHtmlResult {
|
||||
let (tag, changed) = ctx.resolve_tag("br");
|
||||
ToHtmlResult::new(format!("<{tag} />"), changed)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,9 +170,9 @@ impl ToHtml for InlineCode {
|
|||
meta: None,
|
||||
})
|
||||
} else {
|
||||
format!("<code>{}</code>", html_encode(&self.value))
|
||||
ctx.wrap_in_tag("code", "", html_encode(&self.value))
|
||||
};
|
||||
ToHtmlResult::new(output, false)
|
||||
ToHtmlResult::from_wrapped(output, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +191,7 @@ impl ToHtml for Delete {
|
|||
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)
|
||||
ToHtmlResult::from_wrapped(ctx.wrap_in_tag("em", "", children.html), children.svelte)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +220,7 @@ impl ToHtml for Html {
|
|||
}
|
||||
|
||||
impl ToHtml for Image {
|
||||
fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult {
|
||||
fn to_html(&self, ctx: &mut Context) -> ToHtmlResult {
|
||||
let alt = &self.alt;
|
||||
let title = self
|
||||
.title
|
||||
|
|
@ -220,9 +228,10 @@ impl ToHtml for Image {
|
|||
.map(|t| format!(" title=\"{}\"", t))
|
||||
.unwrap_or_default();
|
||||
let url = &self.url;
|
||||
let (tag, changed) = ctx.resolve_tag("img");
|
||||
ToHtmlResult::new(
|
||||
format!("<img src=\"{}\" alt=\"{}\"{}>", url, alt, title),
|
||||
false,
|
||||
format!("<{tag} src=\"{url}\" alt=\"{alt}\"{title}>"),
|
||||
changed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -247,8 +256,8 @@ impl ToHtml for Link {
|
|||
.as_ref()
|
||||
.map(|t| format!(" title=\"{}\"", t))
|
||||
.unwrap_or_default();
|
||||
ToHtmlResult::new(
|
||||
format!("<a href=\"{}\"{}>{}</a>", self.url, title, children.html),
|
||||
ToHtmlResult::from_wrapped(
|
||||
ctx.wrap_in_tag("a", &format!("href=\"{}\"{}", self.url, title), children.html),
|
||||
children.svelte,
|
||||
)
|
||||
}
|
||||
|
|
@ -263,8 +272,8 @@ impl ToHtml for LinkReference {
|
|||
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),
|
||||
ToHtmlResult::from_wrapped(
|
||||
ctx.wrap_in_tag("strong", "", children.html),
|
||||
children.svelte,
|
||||
)
|
||||
}
|
||||
|
|
@ -288,9 +297,9 @@ impl ToHtml for Code {
|
|||
meta: self.meta.clone(),
|
||||
})
|
||||
} else {
|
||||
format!("<pre><code>{}</code></pre>", html_encode(&self.value))
|
||||
ctx.wrap_in_tag("pre", "", ctx.wrap_in_tag("code", "", html_encode(&self.value)))
|
||||
};
|
||||
ToHtmlResult::new(highlighted, false)
|
||||
ToHtmlResult::from_wrapped(highlighted, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,12 +344,13 @@ impl ToHtml for Heading {
|
|||
id: slug.clone(),
|
||||
pos: self.position.clone(),
|
||||
});
|
||||
let (tag, changed) = ctx.resolve_tag(&format!("h{}", self.depth));
|
||||
ToHtmlResult::new(
|
||||
format!(
|
||||
"\n<h{} id=\"{}\">{}</h{}>\n",
|
||||
self.depth, slug, children.html, self.depth
|
||||
"\n<{tag} id=\"{slug}\">{}</{tag}>\n",
|
||||
children.html
|
||||
),
|
||||
children.svelte,
|
||||
children.svelte || changed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -348,34 +358,35 @@ impl ToHtml for Heading {
|
|||
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)
|
||||
ToHtmlResult::from_wrapped(ctx.wrap_in_tag("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)
|
||||
fn to_html(&self, ctx: &mut Context) -> ToHtmlResult {
|
||||
let (tag, changed) = ctx.resolve_tag("hr");
|
||||
ToHtmlResult::new(format!("\n<{tag}>\n"), changed)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
ToHtmlResult::from_wrapped(ctx.wrap_in_tag("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)
|
||||
ToHtmlResult::from_wrapped(ctx.wrap_in_tag("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)
|
||||
ToHtmlResult::from_wrapped(ctx.wrap_in_tag("li", "", children.html), children.svelte)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -388,7 +399,7 @@ impl ToHtml for Definition {
|
|||
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)
|
||||
ToHtmlResult::from_wrapped(ctx.wrap_in_tag("p", "", children.html), children.svelte)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -557,6 +568,7 @@ struct HighlightRequest {
|
|||
|
||||
pub struct MdsvexrsOptions {
|
||||
pub layout: String,
|
||||
pub custom_tags: Vec<String>,
|
||||
// pub path: String,
|
||||
}
|
||||
|
||||
|
|
@ -583,7 +595,7 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
fn highlight(&mut self, code: HighlightRequest) -> String {
|
||||
fn highlight(&mut self, code: HighlightRequest) -> (String, bool) {
|
||||
let theme = &self.theme_set.themes["base16-ocean.dark"];
|
||||
// #[cfg(not(target_arch = "wasm32"))]
|
||||
// let start = Instant::now();
|
||||
|
|
@ -593,36 +605,40 @@ impl Context {
|
|||
if let Some(default) = &self.default_lang {
|
||||
lang = default;
|
||||
} else {
|
||||
return format!("<pre><code>{}</code></pre>", html_encode(&code.code));
|
||||
return self.wrap_in_tag("pre", "", self.wrap_in_tag("code", "", html_encode(&code.code)));
|
||||
}
|
||||
}
|
||||
let syntax = self.syntax_set.find_syntax_by_token(lang);
|
||||
let syntax = match syntax {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return format!("<pre><code lang=\"{}\">{}</code></pre>", html_encode(lang), html_encode(&code.code));
|
||||
return self.wrap_in_tag("pre", "", self.wrap_in_tag("code", &format!(" lang=\"{}\"", html_encode(lang)), html_encode(&code.code)));
|
||||
}
|
||||
};
|
||||
let mut highlighter = HighlightLines::new(syntax, theme);
|
||||
|
||||
let mut string = String::new();
|
||||
|
||||
let (code_tag, code_changed) = self.resolve_tag("code");
|
||||
let (pre_tag, pre_changed) = self.resolve_tag("pre");
|
||||
|
||||
match code.inline {
|
||||
true => {
|
||||
let regions = highlighter
|
||||
.highlight_line(&code.code, &self.syntax_set)
|
||||
.unwrap();
|
||||
string += &format!("<code lang=\"{}\">", html_encode(lang));
|
||||
string += &format!("<{code_tag} lang=\"{}\">", html_encode(lang));
|
||||
append_highlighted_html_for_styled_line(
|
||||
®ions[..],
|
||||
IncludeBackground::No,
|
||||
&mut string,
|
||||
)
|
||||
.unwrap();
|
||||
string += "</code>";
|
||||
string += &format!("</{code_tag}>");
|
||||
(string, code_changed)
|
||||
}
|
||||
false => {
|
||||
string += &format!("<pre><code lang=\"{}\">", html_encode(lang));
|
||||
string += &format!("<{pre_tag}><{code_tag} lang=\"{}\">", html_encode(lang));
|
||||
for line in LinesWithEndings::from(&code.code) {
|
||||
let regions = highlighter.highlight_line(line, &self.syntax_set).unwrap();
|
||||
append_highlighted_html_for_styled_line(
|
||||
|
|
@ -632,14 +648,24 @@ impl Context {
|
|||
)
|
||||
.unwrap();
|
||||
}
|
||||
string += "</code></pre>\n";
|
||||
string += &format!("</{code_tag}></{pre_tag}>\n");
|
||||
(string, code_changed || pre_changed)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(not(target_arch = "wasm32"))] {
|
||||
// self.highlight_times += start.elapsed();
|
||||
// }
|
||||
string
|
||||
fn resolve_tag(&self, tag: &str) -> (String, bool) {
|
||||
if self.options.custom_tags.contains(&tag.to_string()) {
|
||||
(tag.to_ascii_uppercase(), true)
|
||||
} else {
|
||||
(tag.to_string(), false)
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_in_tag(&self, tag: &str, opts: &str, content: impl Into<Wrappable>) -> (String, bool) {
|
||||
let content: Wrappable = content.into();
|
||||
let (tag, changed) = self.resolve_tag(tag);
|
||||
(format!("<{tag} {opts}>{}</{tag}>", content.html), changed || content.svelte)
|
||||
}
|
||||
|
||||
fn resolve_layout(&self) -> &str {
|
||||
|
|
@ -692,7 +718,15 @@ impl Context {
|
|||
) + 1;
|
||||
let mut script = value[..end].to_string();
|
||||
let layout = self.resolve_layout();
|
||||
script += format!("import MDXLayout from \"{}\";", layout).as_str();
|
||||
if self.options.custom_tags.is_empty() {
|
||||
script += format!("import MDXLayout from \"{}\";", layout).as_str();
|
||||
} else {
|
||||
let custom_tags = self.options.custom_tags
|
||||
.iter()
|
||||
.map(|tag| format!("{} as {}", tag, tag.to_ascii_uppercase()))
|
||||
.join(", ");
|
||||
script += format!("import MDXLayout, {{ {} }} from \"{}\";", custom_tags, layout).as_str();
|
||||
}
|
||||
script += &value[end..];
|
||||
script
|
||||
};
|
||||
|
|
@ -722,6 +756,38 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
struct Wrappable {
|
||||
html: String,
|
||||
svelte: bool,
|
||||
}
|
||||
|
||||
impl From<ToHtmlResult> for Wrappable {
|
||||
fn from(value: ToHtmlResult) -> Self {
|
||||
Self {
|
||||
html: value.html,
|
||||
svelte: value.svelte,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Wrappable {
|
||||
fn from(value: String) -> Self {
|
||||
Self {
|
||||
html: value,
|
||||
svelte: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(String, bool)> for Wrappable {
|
||||
fn from(value: (String, bool)) -> Self {
|
||||
Self {
|
||||
html: value.0,
|
||||
svelte: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const DEFAULT_MD_OPTIONS: markdown::ParseOptions = markdown::ParseOptions {
|
||||
constructs: Constructs {
|
||||
attention: true,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ use mdsvexrs::Context;
|
|||
struct Args {
|
||||
#[arg(short, long)]
|
||||
layout: String,
|
||||
#[arg(short, long, default_value = "")]
|
||||
custom_tags: Vec<String>,
|
||||
// #[arg(short, long)]
|
||||
// path: String,
|
||||
#[arg(long)]
|
||||
|
|
@ -20,6 +22,7 @@ fn main() {
|
|||
|
||||
let mut ctx = Context::new(mdsvexrs::MdsvexrsOptions {
|
||||
layout: args.layout,
|
||||
custom_tags: args.custom_tags,
|
||||
// path: args.path,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue