feat: new markdown compiler & shiki syntax hightlighting & copy code button

This commit is contained in:
ZhymabekRoman 2024-11-18 20:13:01 +05:00
parent fdd20d1ccc
commit fff99b6522
8 changed files with 472 additions and 113 deletions

Binary file not shown.

View file

@ -36,13 +36,16 @@
"type": "module",
"dependencies": {
"@iconify/svelte": "^4.0.2",
"@iconify/tailwind": "^1.1.3",
"bits-ui": "^0.21.16",
"copy-to-clipboard": "^3.3.3",
"es-toolkit": "^1.26.1",
"mdsvex": "^0.12.3",
"medium-zoom": "^1.1.0",
"shiki": "^1.23.1",
"shiki-transformer-copy-button": "^0.0.3",
"svelte-bricks": "^0.2.1",
"svelte-lazy": "1.2.9",
"svelte-legos": "^0.2.5",
"svelte-markdown": "^0.4.1"
"svelte-legos": "^0.2.5"
}
}

View file

@ -85,3 +85,99 @@ html {
.medium-zoom-image--opened {
z-index: 999;
}
@layer base {
/**
* Shiki
*/
pre.shiki {
counter-reset: line-number;
}
pre.shiki code {
display: grid;
}
pre.shiki,
pre.shiki span {
color: var(--shiki-light) !important;
background-color: transparent;
}
/* html.dark pre.shiki,
html.dark pre.shiki span {
color: var(--shiki-dark) !important;
} */
pre.shiki .line {
counter-increment: line-number;
}
pre.shiki .line:not(:last-of-type)::before {
content: counter(line-number);
color: hsl(240 5.3% 26.1%);
display: inline-block;
text-align: right;
margin-right: 1em;
width: 2ch;
}
html.dark pre.shiki .line:not(:last-of-type)::before {
color: hsl(240 5% 64.9%);
}
pre.shiki .diff.add {
background-color: hsla(141.7 76.6% 73.1% / 0.5);
}
pre.shiki .diff.remove {
background-color: hsla(0 93.5% 81.8% / 0.7);
}
html.dark pre.shiki .diff.add {
background-color: hsla(143.8 61.2% 20.2% / 0.7);
}
html.dark pre.shiki .diff.remove {
background-color: hsla(0 62.8% 30.6% / 0.7);
}
}
pre:has(code) {
position: relative;
}
pre button.copy {
position: absolute;
right: 16px;
top: 16px;
height: 20px;
width: 20px;
padding: 0;
display: flex;
& span {
width: 100%;
aspect-ratio: 1 / 1;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
& .ready {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'><path fill='none' stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192q.56-.045 1.124-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48 48 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.25 2.25 0 0 0 15 2.25h-1.5a2.25 2.25 0 0 0-2.15 1.586m5.8 0q.099.316.1.664v.75h-6V4.5q.001-.348.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9'/></svg>");
}
& .success {
display: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 16 16'><g fill='white' fill-rule='evenodd' clip-rule='evenodd'><path d='M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2M10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4z'/><path d='M2 7a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm6.585 1.08a.75.75 0 0 1 .336 1.005l-1.75 3.5a.75.75 0 0 1-1.16.234l-1.75-1.5a.75.75 0 0 1 .977-1.139l1.02.875l1.321-2.64a.75.75 0 0 1 1.006-.336'/></g></svg>");
}
&.copied {
& .success {
display: block;
}
& .ready {
display: none;
}
}
}

View file

@ -1,8 +1,16 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
import { mode } from 'mode-watcher';
import { toast } from 'svelte-sonner';
import { browser } from '$app/environment';
type $$Props = SonnerProps;
if (browser) {
window.addEventListener('toast', (event) => {
toast.success(event.detail.message);
});
}
</script>
<Sonner
@ -10,11 +18,12 @@
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
}
}}
{...$$restProps}
/>

View file

@ -1,10 +1,10 @@
<script>
import Header from '$lib/elements/Header.svelte';
import SvelteMarkdown from 'svelte-markdown';
import { formatDate } from '$lib/utils/dateFormatter';
import { onMount } from 'svelte';
import Icon from '@iconify/svelte';
import ImageZoom from '$lib/elements/ImageZoom.svelte';
import Skeleton from '$lib/components/ui/skeleton/skeleton.svelte';
let data = {
title: 'UploadThing is 5x Faster',
@ -14,45 +14,7 @@
role: 'CEO @ Ping Labs',
avatar: 'https://picsum.photos/seed/post1/400/300'
},
postImage: 'https://picsum.photos/seed/postimage/1200/600', // This can now be undefined or null
content: `
## V7 Is Here!
This release has been an absurd amount of work. So proud of the team and what we've built. Huge thanks to [Julius](#) and [Mark](#) for making this happen.
It is so, so hard to not go straight into the nerdy details, but the whole point of UploadThing is that you don't need to know ANY of those details. With that in mind, here's what's relevant for most of y'all:
- UploadThing is now *way* faster
- Uploads can be paused and resumed seamlessly (huge for users on bad internet connections)
- More details...
## Revolutionary Features
We've completely overhauled our backend infrastructure to bring you unparalleled performance. Our new distributed processing system can handle millions of concurrent uploads without breaking a sweat.
### AI-Powered Optimization
UploadThing now leverages cutting-edge machine learning algorithms to optimize your uploads in real-time. Whether you're uploading images, videos, or documents, our AI will automatically adjust compression and encoding settings to give you the best possible quality at the smallest file size.
## Security Enhancements
We've implemented state-of-the-art encryption protocols to ensure your data remains safe and secure throughout the entire upload process. Our new zero-knowledge architecture means that even we can't access your files without your explicit permission.
### Compliance and Regulations
UploadThing is now fully compliant with GDPR, CCPA, and other major data protection regulations worldwide. We've also obtained ISO 27001 certification, demonstrating our commitment to information security management.
## Future Roadmap
We're not stopping here. Our team is already hard at work on the next big update. Here's a sneak peek of what's coming:
- Quantum-resistant encryption for future-proof security
- Integration with major cloud storage providers for seamless file management
- Advanced analytics dashboard for enterprise users
- Support for emerging file formats and codecs
Stay tuned for more exciting updates as we continue to revolutionize the world of file uploads!
`,
postImage: 'https://picsum.photos/seed/postimage/1200/600', // This can now be undefined or null,
tableOfContents: [
{ id: 'v7-is-here', title: 'V7 Is Here!' },
{ id: 'benchmarks', title: 'Benchmarks' },
@ -62,11 +24,17 @@ Stay tuned for more exciting updates as we continue to revolutionize the world o
]
};
let contentLoaded = false;
let compiledContent = '';
onMount(() => {
setTimeout(() => {
onMount(async () => {
try {
const transformed = await import('../test/blog01.md');
compiledContent = transformed.default;
contentLoaded = true;
}, 500);
} catch (error) {
console.error('Error compiling markdown:', error);
contentLoaded = true;
}
});
</script>
@ -89,76 +57,244 @@ Stay tuned for more exciting updates as we continue to revolutionize the world o
</nav>
<div class="lg:flex lg:space-x-8">
<article class="flex-grow overflow-hidden bg-white rounded-lg shadow-lg">
{#if data.postImage}
<ImageZoom
src={data.postImage}
alt="Post cover image"
class="object-cover w-full h-auto max-h-96"
/>
{/if}
<header class="p-6 bg-gray-50">
<p class="mb-2 text-gray-600">{formatDate(data.date)}</p>
<h1 class="mb-4 text-4xl font-bold text-gray-900">{data.title}</h1>
<div class="flex items-center">
<img src={data.author.avatar} alt="" class="w-12 h-12 mr-4 rounded-full" />
<div>
<p class="font-semibold text-gray-900">{data.author.name}</p>
<p class="text-gray-600">{data.author.role}</p>
<article class="flex-grow overflow-hidden bg-white rounded-lg shadow-lg dark:bg-zinc-900">
{#if !contentLoaded}
<Skeleton class="w-full h-96" />
<div class="p-6 bg-gray-50 dark:bg-zinc-800">
<Skeleton class="w-32 h-4 mb-2" />
<Skeleton class="w-full h-10 mb-4" />
<div class="flex items-center">
<Skeleton class="w-12 h-12 mr-4 rounded-full" />
<div class="space-y-2">
<Skeleton class="w-40 h-4" />
<Skeleton class="w-32 h-4" />
</div>
</div>
</div>
</header>
<div class="p-6 {data.postImage ? '' : 'pt-0'}">
<div class="prose max-w-none">
{#if contentLoaded}
<SvelteMarkdown source={data.content} />
{:else}
<p>Loading content...</p>
{/if}
<div class="p-6">
<div class="space-y-4">
<Skeleton class="w-full h-4" />
<Skeleton class="w-full h-4" />
<Skeleton class="w-3/4 h-4" />
</div>
</div>
</div>
{:else}
{#if data.postImage}
<ImageZoom
src={data.postImage}
alt="Post cover image"
class="object-cover w-full h-auto max-h-96"
/>
{/if}
<header class="p-6 bg-gray-50 dark:bg-zinc-800">
<p class="mb-2 text-gray-600 dark:text-gray-400">{formatDate(data.date)}</p>
<h1 class="mb-4 text-4xl font-bold text-gray-900 dark:text-white">{data.title}</h1>
<div class="flex items-center">
<img src={data.author.avatar} alt="" class="w-12 h-12 mr-4 rounded-full" />
<div>
<p class="font-semibold text-gray-900 dark:text-white">{data.author.name}</p>
<p class="text-gray-600 dark:text-gray-400">{data.author.role}</p>
</div>
</div>
</header>
<div class="p-6 {data.postImage ? '' : 'pt-0'} dark:text-gray-300">
<div class="prose max-w-none">
{#if contentLoaded}
{#if compiledContent}
<svelte:component this={compiledContent} />
{:else}
<p>Error loading content</p>
{/if}
{:else}
<p>Loading content...</p>
{/if}
</div>
</div>
{/if}
</article>
<aside class="order-first mt-7 lg:mt-0 lg:min-w-80 lg:order-none">
<nav
aria-labelledby="toc-heading"
class="w-full p-4 bg-white rounded-lg shadow-lg lg:sticky lg:top-36"
>
<h2 id="toc-heading" class="mb-4 text-xl font-semibold text-gray-900">Contents</h2>
{#if data.tableOfContents && data.tableOfContents.length > 0}
<ul class="space-y-2">
{#each data.tableOfContents as item}
<li>
<a href={`#${item.id}`} class="transition-colors text-zinc-800 hover:text-zinc-900">
{item.title}
</a>
</li>
{/each}
</ul>
{:else}
<p>No table of contents available</p>
{/if}
</nav>
{#if !contentLoaded}
<div class="w-full p-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900">
<Skeleton class="w-32 h-6 mb-4" />
<div class="space-y-2">
<Skeleton class="w-full h-4" />
<Skeleton class="w-full h-4" />
<Skeleton class="w-3/4 h-4" />
</div>
</div>
{:else}
<nav
aria-labelledby="toc-heading"
class="w-full p-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900 lg:sticky lg:top-36"
>
<h2 id="toc-heading" class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">
Contents
</h2>
{#if data.tableOfContents && data.tableOfContents.length > 0}
<ul class="space-y-2">
{#each data.tableOfContents as item}
<li>
<a
href={`#${item.id}`}
class="transition-colors text-zinc-800 hover:text-zinc-900 dark:text-gray-300 dark:hover:text-white"
>
{item.title}
</a>
</li>
{/each}
</ul>
{:else}
<p class="dark:text-gray-300">No table of contents available</p>
{/if}
</nav>
{/if}
</aside>
</div>
</main>
<style lang="postcss">
:global(h2) {
@apply font-bold font-sans break-normal text-gray-900 dark:text-gray-100 md:text-2xl;
/* Headings */
:global(.prose h1) {
@apply text-4xl font-bold text-gray-900 dark:text-gray-100 mt-12 mb-6;
}
:global(*:not(:first-child) + h2) {
@apply pt-12;
:global(.prose h2) {
@apply text-3xl font-bold text-gray-900 dark:text-gray-100 mt-12 mb-4;
}
:global(.prose h3) {
@apply text-2xl font-bold text-gray-900 dark:text-gray-100 mt-8 mb-3;
}
:global(.prose h4) {
@apply text-xl font-bold text-gray-900 dark:text-gray-100 mt-6 mb-2;
}
:global(.prose h5) {
@apply text-lg font-bold text-gray-900 dark:text-gray-100 mt-4 mb-2;
}
:global(.prose h6) {
@apply text-base font-bold text-gray-900 dark:text-gray-100 mt-4 mb-2;
}
/* Paragraphs and spacing */
:global(.prose p) {
@apply leading-8 mt-7;
@apply text-gray-700 dark:text-gray-300 leading-7 mt-4;
}
:global(.prose h3 + p),
:global(.prose h4 + p) {
@apply mt-3;
:global(.prose > *:first-child) {
@apply mt-0;
}
/* Lists */
:global(.prose ul) {
@apply list-disc list-outside ml-6 mt-4 space-y-2;
}
:global(.prose ol) {
@apply list-decimal list-outside ml-6 mt-4 space-y-2;
}
:global(.prose li) {
@apply text-gray-700 dark:text-gray-300 leading-7;
}
/* Blockquotes */
:global(.prose blockquote) {
@apply border-l-4 border-gray-200 dark:border-gray-700 pl-4 italic my-6;
}
/* Code blocks */
:global(.prose pre) {
@apply rounded-lg p-4 my-6 overflow-x-auto;
}
:global(.prose code) {
@apply px-1.5 py-0.5 rounded text-sm font-mono;
}
:global(.prose pre code) {
@apply bg-transparent p-0 text-sm leading-relaxed;
}
/* Tables */
:global(.prose table) {
@apply w-full border-collapse my-6;
}
:global(.prose th) {
@apply border border-gray-300 dark:border-gray-700 px-4 py-2 bg-gray-100 dark:bg-zinc-800 text-left;
}
:global(.prose td) {
@apply border border-gray-300 dark:border-gray-700 px-4 py-2;
}
/* Links */
:global(.prose a) {
@apply text-primary hover:text-primary/90 underline decoration-primary/30 hover:decoration-primary/50;
}
/* Horizontal rule */
:global(.prose hr) {
@apply border-gray-200 dark:border-gray-700 my-8;
}
/* Images */
:global(.prose img) {
@apply rounded-lg my-6 mx-auto;
}
/* Inline elements */
:global(.prose strong) {
@apply font-bold text-gray-900 dark:text-gray-100;
}
:global(.prose em) {
@apply italic;
}
:global(.prose del) {
@apply line-through;
}
/* Definition lists */
:global(.prose dl) {
@apply mt-4;
}
:global(.prose dt) {
@apply font-bold text-gray-900 dark:text-gray-100;
}
:global(.prose dd) {
@apply ml-4 mt-2;
}
/* Nested lists spacing */
:global(.prose li > ul),
:global(.prose li > ol) {
@apply mt-2 mb-2;
}
/* Code block filename */
:global(.prose .filename) {
@apply text-sm text-gray-500 dark:text-gray-400 -mb-2 mt-6;
}
/* Summary blocks */
:global(.prose summary) {
@apply text-gray-700 dark:text-gray-300 leading-7 cursor-pointer hover:text-gray-900 dark:hover:text-gray-100 select-none;
}
:global(.prose details) {
@apply my-4 p-4 rounded-lg bg-gray-50 dark:bg-zinc-800;
}
:global(.prose details[open] summary) {
@apply mb-3;
}
</style>

View file

@ -43,7 +43,7 @@
possible.
</p>
</Dialog.Description>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 pt-7">
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 pt-7">
<div class="flex flex-col items-start gap-2">
<Label.Root class="text-sm font-medium">Problem Type</Label.Root>
<RadioGroup.Root bind:value={problemType} class="space-y-2">
@ -111,7 +111,7 @@
</Drawer.Description>
</Drawer.Header>
<div class="px-4">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 pt-7">
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 pt-7">
<div class="flex flex-col items-start gap-2">
<Label.Root class="text-sm font-medium">Problem Type</Label.Root>
<RadioGroup.Root bind:value={problemType} class="space-y-2">

View file

@ -0,0 +1,67 @@
# V7 Is Here! 🚀
> "The best file upload solution just got even better!" - _Tech Weekly_
This release has been an absurd amount of work. So proud of the team and what we've built. Huge thanks to [Julius](https://github.com/julius) and [Mark](https://github.com/mark) for making this happen.
---
It is so, so hard to not go straight into the nerdy details, but the whole point of UploadThing is that you don't need to know ANY of those details. With that in mind, here's what's relevant for most y'all:
- UploadThing is now _way_ faster
- Uploads can be **paused** and _resumed_ seamlessly
- ~~Old limitations removed~~
- More details...
## Performance Comparison
| Feature | V6 | V7 |
| ------------------ | ------ | --------- |
| Upload Speed | 10MB/s | 50MB/s |
| Concurrent Uploads | 100 | Unlimited |
| Max File Size | 2GB | 10GB |
## Revolutionary Features
We've completely overhauled our backend infrastructure to bring you unparalleled performance. Our new distributed processing system can handle millions of concurrent uploads without breaking a sweat.
### Code Example
```typescript
import { createUploadthing } from 'uploadthing/next';
const f = createUploadthing();
export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: '4MB' } })
.middleware(async () => {
return { userId: 1234 };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log('Upload complete for userId:', metadata.userId);
})
};
```
![Upload Dashboard](https://example.com/dashboard.png)
### AI-Powered Optimization
UploadThing now leverages cutting-edge machine learning algorithms to optimize your uploads in real-time.
<details>
<summary>Technical Details</summary>
- Uses TensorFlow.js for client-side optimizations
- Implements WebAssembly for performance
- Leverages Web Workers for background processing
</details>
---
_For more information, visit our [documentation](https://docs.uploadthing.com)._
```js
console.log('Hello, world!');
```

View file

@ -1,16 +1,64 @@
import { mdsvex, escapeSvelte } from 'mdsvex';
import { createHighlighter } from 'shiki';
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { h } from 'hastscript';
export function addCopyButton(options = {}) {
const toggleMs = options.toggle || 3000;
return {
name: 'shiki-transformer-copy-button',
pre(node) {
const button = h(
'button',
{
class: 'copy',
'data-code': this.source,
onclick: `
navigator.clipboard.writeText(this.dataset.code);
this.classList.add('copied');
setTimeout(() => this.classList.remove('copied'), ${toggleMs});
window.dispatchEvent(new CustomEvent('toast', { detail: { message: 'Copied to clipboard' } }));
`
},
[h('span', { class: 'ready' }), h('span', { class: 'success' })]
);
node.children.push(button);
}
};
}
/** @type {import('mdsvex').MdsvexOptions} */
const mdsvexOptions = {
extensions: ['.md'],
highlight: {
highlighter: async (code, lang = 'text') => {
const highlighter = await createHighlighter({
themes: ['poimandres'],
langs: ['javascript', 'typescript']
});
await highlighter.loadLanguage('javascript', 'typescript');
const html = escapeSvelte(
highlighter.codeToHtml(code, {
lang,
theme: 'poimandres',
transformers: [addCopyButton({ toggle: 1200 })]
})
);
return `{@html \`${html}\` }`;
}
}
};
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
extensions: ['.svelte', '.md'],
preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};