Compare commits
No commits in common. "master" and "carta-md-v3.6.1" have entirely different histories.
master
...
carta-md-v
101 changed files with 1297 additions and 5359 deletions
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
@ -5,15 +5,11 @@
|
||||||
"coldark",
|
"coldark",
|
||||||
"dompurify",
|
"dompurify",
|
||||||
"flexsearch",
|
"flexsearch",
|
||||||
"Gemoji",
|
|
||||||
"gruvbox",
|
"gruvbox",
|
||||||
"iconify",
|
|
||||||
"Katex",
|
"Katex",
|
||||||
"mdsvex",
|
"mdsvex",
|
||||||
"oldschool",
|
"oldschool",
|
||||||
"rehype",
|
"rehype",
|
||||||
"shiki",
|
|
||||||
"shikijs",
|
|
||||||
"tikz",
|
"tikz",
|
||||||
"tikzjax",
|
"tikzjax",
|
||||||
"typeof"
|
"typeof"
|
||||||
|
@ -21,6 +17,5 @@
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
"[svelte]": {
|
"[svelte]": {
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||||
},
|
}
|
||||||
"css.customData": [".vscode/tailwind.json"]
|
|
||||||
}
|
}
|
||||||
|
|
55
.vscode/tailwind.json
vendored
55
.vscode/tailwind.json
vendored
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
"version": 1.1,
|
|
||||||
"atDirectives": [
|
|
||||||
{
|
|
||||||
"name": "@tailwind",
|
|
||||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"name": "Tailwind Documentation",
|
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@apply",
|
|
||||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"name": "Tailwind Documentation",
|
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@responsive",
|
|
||||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"name": "Tailwind Documentation",
|
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@screen",
|
|
||||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"name": "Tailwind Documentation",
|
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "@variants",
|
|
||||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"name": "Tailwind Documentation",
|
|
||||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
66
README.md
66
README.md
|
@ -1,25 +1,31 @@
|
||||||
<div align="right">
|
<div align="right">
|
||||||
<a href="https://www.npmjs.com/package/carta-md">
|
<a href="https://www.npmjs.com/package/carta-md">
|
||||||
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
|
<img src="https://img.shields.io/npm/v/carta-md?color=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bundlephobia.com/package/carta-md">
|
<a href="https://bundlephobia.com/package/carta-md">
|
||||||
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
|
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
|
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
|
||||||
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&labelColor=171d27&logo=git&logoColor=white" alt="license">
|
<img src="https://img.shields.io/npm/l/carta-md?color=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
|
||||||
</a>
|
</a>
|
||||||
<a href="http://beartocode.github.io/carta/">
|
<a href="http://beartocode.github.io/carta/">
|
||||||
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs">
|
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://beartocode.github.io/carta/)
|
<div align="center">
|
||||||
|
<a href="https://beartocode.github.io/carta/">
|
||||||
|
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 align="center"><strong>Carta</strong></h1>
|
<br>
|
||||||
<div align="center">Modern, lightweight, powerful Markdown Editor.</div>
|
|
||||||
|
<div align="center"><strong>Carta</strong></div>
|
||||||
|
<div align="center">Swiftly edit and render Markdown, with no overhead.</div>
|
||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://beartocode.github.io/carta/">📚 Documentation</a>
|
<a href="https://beartocode.github.io/carta/">Documentation</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://github.com/BearToCode/carta">GitHub</a>
|
<a href="https://github.com/BearToCode/carta">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,30 +34,24 @@
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
> [!NOTE]
|
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer, based on [Marked](https://github.com/markedjs/marked). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
|
||||||
> Carta has recently been updated to `v4`, which features numerous major changes.
|
Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
|
||||||
>
|
|
||||||
> Follow the [Migration Guide](http://beartocode.github.io/carta/migration) to update your project.
|
|
||||||
|
|
||||||
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer. It is powered by [unified](https://github.com/unifiedjs/unified), [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
|
|
||||||
Differently from most editors, Carta does not include a code editor, but it is _just_ a textarea with syntax highlighting, shortcuts and more.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
|
- Keyboard **shortcuts** (extensible);
|
||||||
- 🛠️ Toolbar (extensible);
|
- Toolbar (extensible);
|
||||||
- ⌨️ Keyboard **shortcuts** (extensible);
|
- Markdown syntax highlighting;
|
||||||
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark;
|
- Scroll sync;
|
||||||
- 🔀 Scroll sync;
|
- Accessibility friendly;
|
||||||
- ✅ Accessibility friendly;
|
- **SSR** compatible;
|
||||||
- 🖥️ **SSR** compatible;
|
- **Katex** support (plugin);
|
||||||
- ⚗️ **KaTeX** support (plugin);
|
- **Slash** commands (plugin);
|
||||||
- 🔨 **Slash** commands (plugin);
|
- **Emojis**, with included search (plugin);
|
||||||
- 😄 **Emojis**, with included search (plugin);
|
- **Tikz** support (plugin);
|
||||||
- ✏️ **TikZ** support (plugin);
|
- **Attachment** support (plugin);
|
||||||
- 📂 **Attachment** support (plugin);
|
- **Anchor** links in headings;
|
||||||
- ⚓ **Anchor** links in headings (plugin);
|
- Code blocks **syntax highlighting** (plugin).
|
||||||
- 🌈 Code blocks **syntax highlighting** (plugin).
|
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
|
@ -80,7 +80,6 @@ Differently from most editors, Carta does not include a code editor, but it is _
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Sanitization is not dealt with by Carta. You need to provide a `sanitizer` in the options.
|
> Sanitization is not dealt with by Carta. You need to provide a `sanitizer` in the options.
|
||||||
> Common sanitizers are [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) (suggested) and [sanitize-html](https://www.npmjs.com/package/sanitize-html).
|
> Common sanitizers are [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) (suggested) and [sanitize-html](https://www.npmjs.com/package/sanitize-html).
|
||||||
> Checkout the documentation for an example.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -100,9 +99,11 @@ npm i @cartamd/plugin-name
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
// Component default theme
|
// Component default theme
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
|
// Markdown input theme (Speed Highlight)
|
||||||
|
import 'carta-md/light.css';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
// Remember to use a sanitizer to prevent XSS attacks
|
// Remember to use a sanitizer to prevent XSS attacks
|
||||||
|
@ -110,14 +111,13 @@ npm i @cartamd/plugin-name
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Or in global stylesheet */
|
/* Or in global stylesheet */
|
||||||
/* Set your custom monospace font */
|
/* Set your custom monospace font */
|
||||||
:global(.carta-font-code) {
|
:global(.carta-font-code) {
|
||||||
font-family: '...', monospace;
|
font-family: '...', monospace;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
```
|
```
|
||||||
|
|
|
@ -47,8 +47,8 @@
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk-sv": "^0.0.6",
|
"cmdk-sv": "^0.0.6",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"iconify-icon": "^2.0.0",
|
"katex": "^0.16.7",
|
||||||
"katex": "^0.16.10",
|
"radix-icons-svelte": "^1.2.1",
|
||||||
"tailwind-merge": "^2.0.0"
|
"tailwind-merge": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Copy } from 'radix-icons-svelte';
|
||||||
|
|
||||||
let elem: HTMLElement;
|
let elem: HTMLElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -11,10 +13,10 @@
|
||||||
navigator.clipboard.writeText(elem.innerText);
|
navigator.clipboard.writeText(elem.innerText);
|
||||||
}}
|
}}
|
||||||
class="
|
class="
|
||||||
absolute right-4 top-[min(50%_,_32px)] aspect-square -translate-y-1/2 transform
|
absolute right-4 top-[min(50%_,_32px)] -translate-y-1/2 transform
|
||||||
rounded hover:bg-neutral-800 hover:text-neutral-300 active:text-sky-300
|
rounded p-2 hover:bg-neutral-800 hover:text-neutral-300 active:text-sky-300
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<iconify-icon icon="octicon:copy-16" class="p-2 text-lg"></iconify-icon>
|
<Copy class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onNavigate } from '$app/navigation';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { debounce, throttle } from '$lib/utils';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
const PADDING = 80;
|
const PADDING = 80;
|
||||||
|
|
||||||
|
@ -9,58 +7,49 @@
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
let headers: HTMLElement[] = [];
|
let headers: HTMLElement[] = [];
|
||||||
let selectedHeaderIndex = 0;
|
let scrollY = 0;
|
||||||
|
|
||||||
function retrieveHeaders() {
|
function retrieveHeaders() {
|
||||||
headers = Array.from(
|
const markdownContainer = document.querySelector('.markdown');
|
||||||
document.querySelectorAll('.markdown > h1, .markdown > h2, .markdown > h3')
|
if (!markdownContainer) return;
|
||||||
) as HTMLElement[];
|
headers = Array.from(markdownContainer.querySelectorAll('h1, h2, h3')) as HTMLElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightHeader = () => {
|
function highlightHeader(header: HTMLElement, nextHeader: HTMLElement | null, index: number) {
|
||||||
for (let index = headers.length - 1; index >= 0; index--) {
|
const headerHasReachedTop = header.getBoundingClientRect().top <= PADDING || index == 0;
|
||||||
const header = headers[index];
|
const nextHeaderReachedTop = nextHeader && nextHeader.getBoundingClientRect().top <= PADDING;
|
||||||
const rect = header.getBoundingClientRect();
|
return !nextHeaderReachedTop && headerHasReachedTop;
|
||||||
if (rect.top < PADDING) {
|
}
|
||||||
selectedHeaderIndex = index;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedHeaderIndex = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [throttledHighlightHeader] = throttle(highlightHeader, 100);
|
let observer: MutationObserver;
|
||||||
const debouncedHighlightHeader = debounce(highlightHeader, 100);
|
|
||||||
|
|
||||||
onNavigate(() => {
|
onMount(() => {
|
||||||
setTimeout(() => {
|
observer = new MutationObserver(retrieveHeaders);
|
||||||
retrieveHeaders();
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
highlightHeader();
|
retrieveHeaders();
|
||||||
}, 300);
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
observer?.disconnect();
|
||||||
});
|
});
|
||||||
onMount(retrieveHeaders);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window bind:scrollY />
|
||||||
on:scroll={() => {
|
|
||||||
throttledHighlightHeader();
|
|
||||||
debouncedHighlightHeader(); // So it is called at the end of the scroll event
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="h-full space-y-3 {className}">
|
<div class="h-full space-y-3 {className}">
|
||||||
{#each headers as header, i}
|
{#each headers as header, i}
|
||||||
{@const margin = Number(header.tagName.split('')[1]) - 1}
|
{@const margin = Number(header.tagName.split('')[1]) - 1}
|
||||||
{#key selectedHeaderIndex}
|
{@const nextHeader = headers[i + 1]}
|
||||||
{#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href}
|
{#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href}
|
||||||
|
{#key scrollY}
|
||||||
<a
|
<a
|
||||||
style="margin-left: {margin * 0.75}rem;"
|
style="margin-left: {margin * 0.75}rem;"
|
||||||
class="block text-sm {selectedHeaderIndex === i
|
class="block text-sm {highlightHeader(header, nextHeader, i)
|
||||||
? 'font-medium text-sky-300'
|
? 'font-medium text-sky-300'
|
||||||
: 'text-neutral-400'}"
|
: 'text-neutral-400'}"
|
||||||
href={header.children[0].href}>{header.innerText}</a
|
href={header.children[0].href}>{header.innerText}</a
|
||||||
>
|
>
|
||||||
{/if}
|
{/key}
|
||||||
{/key}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Archive, GithubLogo } from 'radix-icons-svelte';
|
||||||
|
|
||||||
export let npmLink: string;
|
export let npmLink: string;
|
||||||
export let githubLink: string;
|
export let githubLink: string;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="plugin-link mb-2 mt-6 flex items-end space-x-3">
|
<div class="plugin-link mb-2 mt-6 inline-flex items-center space-x-3">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<a href={githubLink} class="flex aspect-square">
|
<a href={githubLink}>
|
||||||
<iconify-icon icon="mdi:github" class="text-3xl text-white hover:text-sky-300"></iconify-icon>
|
<GithubLogo class="h-6 w-6 text-white hover:text-sky-300" />
|
||||||
</a>
|
</a>
|
||||||
<a href={npmLink} class="flex aspect-square">
|
<a href={npmLink}>
|
||||||
<iconify-icon icon="gg:npm" class="text-3xl text-white hover:text-sky-300"></iconify-icon>
|
<Archive class="h-6 w-6 text-white hover:text-sky-300" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Cross1, HamburgerMenu } from 'radix-icons-svelte';
|
||||||
import Sidebar from '../sidebar/Sidebar.svelte';
|
import Sidebar from '../sidebar/Sidebar.svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
|
|
||||||
<div class="container mb-4 w-full border-b border-neutral-800 px-4 pb-1 sm:px-6 {className}">
|
<div class="container mb-4 w-full border-b border-neutral-800 px-4 pb-1 sm:px-6 {className}">
|
||||||
<button on:click={() => (enabled = !enabled)} class="text-neutral-500 hover:text-neutral-200">
|
<button on:click={() => (enabled = !enabled)} class="text-neutral-500 hover:text-neutral-200">
|
||||||
<iconify-icon icon="ci:hamburger-lg" class="text-3xl"></iconify-icon>
|
<HamburgerMenu class="h-7 w-7" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@
|
||||||
on:click={() => (enabled = false)}
|
on:click={() => (enabled = false)}
|
||||||
class="absolute right-4 top-4 text-neutral-500 hover:text-neutral-200"
|
class="absolute right-4 top-4 text-neutral-500 hover:text-neutral-200"
|
||||||
>
|
>
|
||||||
<iconify-icon icon="charm:cross" class="text-2xl"></iconify-icon>
|
<Cross1 class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { GithubLogo, Star } from 'radix-icons-svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
@ -18,15 +19,15 @@
|
||||||
href="https://github.com/BearToCode/carta"
|
href="https://github.com/BearToCode/carta"
|
||||||
class="flex h-12 items-center space-x-2 p-2 {className}"
|
class="flex h-12 items-center space-x-2 p-2 {className}"
|
||||||
>
|
>
|
||||||
<iconify-icon icon="mdi:github" class="text-2xl"></iconify-icon>
|
<GithubLogo class="h-5 w-5" />
|
||||||
<div class="hidden h-min flex-col justify-center space-y-1 md:flex">
|
<div class="hidden h-min flex-col justify-center space-y-1 md:flex">
|
||||||
<p class="text-[0.9rem] font-semibold leading-3">BearToCode/carta</p>
|
<p class="text-[0.9rem] font-semibold leading-3">BearToCode/carta</p>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="pulse my-1.5 h-3 w-[80px] rounded-full bg-neutral-800" />
|
<div class="pulse my-1.5 h-3 w-[80px] rounded-full bg-neutral-800" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="inline-flex items-center space-x-1">
|
<div class="inline-flex items-center space-x-1">
|
||||||
<iconify-icon icon="ic:round-star" class="h-3 w-3"></iconify-icon>
|
<Star class="h-3 w-3" />
|
||||||
<span class="mt-1 text-[0.8rem] leading-3">{stars}</span>
|
<span class="text-[0.8rem] leading-3">{stars}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Command from '$lib/components/ui/command';
|
import * as Command from '$lib/components/ui/command';
|
||||||
import type { Document } from 'flexsearch';
|
import type { Document } from 'flexsearch';
|
||||||
|
import { Enter, MagnifyingGlass } from 'radix-icons-svelte';
|
||||||
import {
|
import {
|
||||||
enrichResult,
|
enrichResult,
|
||||||
initializeSearch,
|
initializeSearch,
|
||||||
|
@ -53,8 +54,8 @@
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
<button on:click={() => (open = !open)} class="mr-2 block aspect-square md:hidden">
|
<button on:click={() => (open = !open)} class="mr-2 block md:hidden">
|
||||||
<iconify-icon icon="ion:search" class="text-2xl text-neutral-200"></iconify-icon>
|
<MagnifyingGlass class="h-7 w-7 text-neutral-200" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -62,7 +63,7 @@
|
||||||
on:click={() => (open = !open)}
|
on:click={() => (open = !open)}
|
||||||
>
|
>
|
||||||
<div class="inline-flex items-center space-x-2">
|
<div class="inline-flex items-center space-x-2">
|
||||||
<iconify-icon icon="ion:search" class="text-xl text-neutral-500"></iconify-icon>
|
<MagnifyingGlass class="h-5 w-5 text-neutral-500" />
|
||||||
<span class="text-neutral-500">Search...</span>
|
<span class="text-neutral-500">Search...</span>
|
||||||
</div>
|
</div>
|
||||||
<kbd
|
<kbd
|
||||||
|
@ -101,7 +102,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="absolute right-2 top-1/2 hidden -translate-y-1/2 group-aria-selected:block">
|
<div class="absolute right-2 top-1/2 hidden -translate-y-1/2 group-aria-selected:block">
|
||||||
<iconify-icon icon="mi:enter" class="text-xl text-neutral-400"></iconify-icon>
|
<Enter class="h-5 w-5 text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,4 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Code,
|
||||||
|
CodesandboxLogo,
|
||||||
|
Cube,
|
||||||
|
Dashboard,
|
||||||
|
Download,
|
||||||
|
Face,
|
||||||
|
FontFamily,
|
||||||
|
Link2,
|
||||||
|
Slash,
|
||||||
|
File,
|
||||||
|
FontStyle,
|
||||||
|
Stack
|
||||||
|
} from 'radix-icons-svelte';
|
||||||
import SidebarLink from './SidebarLink.svelte';
|
import SidebarLink from './SidebarLink.svelte';
|
||||||
|
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
@ -11,87 +25,75 @@
|
||||||
|
|
||||||
<!-- Introduction -->
|
<!-- Introduction -->
|
||||||
<SidebarLink href="/introduction">
|
<SidebarLink href="/introduction">
|
||||||
<iconify-icon icon="radix-icons:dashboard" class="text-xl"></iconify-icon>
|
<Dashboard class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Introduction</span>
|
<span class="text-[0.95rem]">Introduction</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Examples -->
|
<!-- Examples -->
|
||||||
<SidebarLink href="/examples">
|
<SidebarLink href="/examples">
|
||||||
<iconify-icon icon="ph:codesandbox-logo" class="text-xl"></iconify-icon>
|
<CodesandboxLogo class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Examples</span>
|
<span class="text-[0.95rem]">Examples</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Getting Started -->
|
<!-- Getting Started -->
|
||||||
<SidebarLink href="/getting-started">
|
<SidebarLink href="/getting-started">
|
||||||
<iconify-icon icon="ic:round-download" class="text-xl"></iconify-icon>
|
<Download class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Getting Started</span>
|
<span class="text-[0.95rem]">Getting Started</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Editing Styles -->
|
<!-- Editing Styles -->
|
||||||
<SidebarLink href="/editing-styles">
|
<SidebarLink href="/editing-styles">
|
||||||
<iconify-icon icon="lucide:palette" class="text-xl"></iconify-icon>
|
<FontStyle class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Editing Styles</span>
|
<span class="text-[0.95rem]">Editing Styles</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Migration -->
|
|
||||||
<SidebarLink href="/migration">
|
|
||||||
<iconify-icon icon="material-symbols:upgrade" class="text-xl"></iconify-icon>
|
|
||||||
<span class="text-[0.95rem]">Migration</span>
|
|
||||||
</SidebarLink>
|
|
||||||
|
|
||||||
<!-- Community Plugins -->
|
<!-- Community Plugins -->
|
||||||
<SidebarLink href="/community-plugins">
|
<SidebarLink href="/community-plugins">
|
||||||
<iconify-icon icon="ph:stack-fill" class="text-xl"></iconify-icon>
|
<Stack class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Community Plugins</span>
|
<span class="text-[0.95rem]">Community Plugins</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Using Svelte Components -->
|
|
||||||
<SidebarLink href="/using-components">
|
|
||||||
<iconify-icon icon="ri:svelte-fill" class="text-xl"></iconify-icon>
|
|
||||||
<span class="text-[0.95rem]">Using Components</span>
|
|
||||||
</SidebarLink>
|
|
||||||
|
|
||||||
<h3 class="mb-3 ml-4 mt-6 text-sm font-medium first:mt-0 last:mb-0">Plugins</h3>
|
<h3 class="mb-3 ml-4 mt-6 text-sm font-medium first:mt-0 last:mb-0">Plugins</h3>
|
||||||
|
|
||||||
<!-- Math -->
|
<!-- Math -->
|
||||||
<SidebarLink href="/plugins/math">
|
<SidebarLink href="/plugins/math">
|
||||||
<iconify-icon icon="tabler:math" class="text-xl"></iconify-icon>
|
<FontFamily class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Math</span>
|
<span class="text-[0.95rem]">Math</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Code -->
|
<!-- Code -->
|
||||||
<SidebarLink href="/plugins/code">
|
<SidebarLink href="/plugins/code">
|
||||||
<iconify-icon icon="fluent:code-16-filled" class="text-xl"></iconify-icon>
|
<Code class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Code</span>
|
<span class="text-[0.95rem]">Code</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Emoji -->
|
<!-- Emoji -->
|
||||||
<SidebarLink href="/plugins/emoji">
|
<SidebarLink href="/plugins/emoji">
|
||||||
<iconify-icon icon="mingcute:emoji-line" class="text-xl"></iconify-icon>
|
<Face class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Emoji</span>
|
<span class="text-[0.95rem]">Emoji</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Slash -->
|
<!-- Slash -->
|
||||||
<SidebarLink href="/plugins/slash">
|
<SidebarLink href="/plugins/slash">
|
||||||
<iconify-icon icon="tabler:slash" class="text-xl"></iconify-icon>
|
<Slash class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Slash</span>
|
<span class="text-[0.95rem]">Slash</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- TikZ -->
|
<!-- TikZ -->
|
||||||
<SidebarLink href="/plugins/tikz">
|
<SidebarLink href="/plugins/tikz">
|
||||||
<iconify-icon icon="mdi:draw-pen" class="text-xl"></iconify-icon>
|
<Cube class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">TikZ</span>
|
<span class="text-[0.95rem]">TikZ</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Attachment -->
|
<!-- Attachment -->
|
||||||
<SidebarLink href="/plugins/attachment">
|
<SidebarLink href="/plugins/attachment">
|
||||||
<iconify-icon icon="tdesign:attach" class="text-xl"></iconify-icon>
|
<File class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Attachment</span>
|
<span class="text-[0.95rem]">Attachment</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Anchor -->
|
<!-- Anchor -->
|
||||||
<SidebarLink href="/plugins/anchor">
|
<SidebarLink href="/plugins/anchor">
|
||||||
<iconify-icon icon="mingcute:link-fill" class="text-xl"></iconify-icon>
|
<Link2 class="h-5 w-5" />
|
||||||
<span class="text-[0.95rem]">Anchor</span>
|
<span class="text-[0.95rem]">Anchor</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from 'cmdk-sv';
|
import { Command as CommandPrimitive } from 'cmdk-sv';
|
||||||
|
import { MagnifyingGlass } from 'radix-icons-svelte';
|
||||||
import { cn } from '$lib/utils';
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
// type $$Props = CommandPrimitive.InputProps;
|
// type $$Props = CommandPrimitive.InputProps;
|
||||||
|
@ -10,7 +11,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center border-b px-3" data-cmdk-input-wrapper="">
|
<div class="flex items-center border-b px-3" data-cmdk-input-wrapper="">
|
||||||
<iconify-icon icon="ion:search" class="mr-2 shrink-0 text-xl opacity-50"></iconify-icon>
|
<MagnifyingGlass class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
bind:value
|
bind:value
|
||||||
class={cn(
|
class={cn(
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
import * as Dialog from '.';
|
import * as Dialog from '.';
|
||||||
import { cn, flyAndScale } from '$lib/utils';
|
import { cn, flyAndScale } from '$lib/utils';
|
||||||
|
import { Cross2 } from 'radix-icons-svelte';
|
||||||
|
|
||||||
type $$Props = DialogPrimitive.ContentProps;
|
type $$Props = DialogPrimitive.ContentProps;
|
||||||
|
|
||||||
|
@ -26,9 +27,9 @@
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-3 my-auto aspect-square rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
||||||
>
|
>
|
||||||
<iconify-icon icon="basil:cross-solid" class="text-2xl"></iconify-icon>
|
<Cross2 class="h-4 w-4" />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
|
import 'carta-md/dark.css';
|
||||||
|
import '$lib/styles/discord.scss';
|
||||||
import { emoji } from '@cartamd/plugin-emoji';
|
import { emoji } from '@cartamd/plugin-emoji';
|
||||||
import { code } from '@cartamd/plugin-code';
|
import { code } from '@cartamd/plugin-code';
|
||||||
import PlusCircled from './assets/PlusIcon.svelte';
|
import { PlusCircled } from 'radix-icons-svelte';
|
||||||
|
|
||||||
import '$lib/styles/discord.scss';
|
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
sanitizer: false,
|
|
||||||
disableIcons: true,
|
disableIcons: true,
|
||||||
extensions: [
|
extensions: [
|
||||||
emoji(),
|
emoji(),
|
||||||
|
@ -16,8 +15,8 @@
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
component: PlusCircled,
|
component: PlusCircled,
|
||||||
parent: 'input',
|
props: { class: 'discord-plus-icon' },
|
||||||
props: {}
|
parent: 'input'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -25,4 +24,5 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor placeholder="Send a message to @someone" mode="tabs" theme="discord" {carta} />
|
<CartaEditor placeholder="Send a message to @someone" mode="tabs" theme="discord" {carta}
|
||||||
|
></CartaEditor>
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { attachment } from '@cartamd/plugin-attachment';
|
import { attachment } from '@cartamd/plugin-attachment';
|
||||||
import { emoji } from '@cartamd/plugin-emoji';
|
import { emoji } from '@cartamd/plugin-emoji';
|
||||||
import { slash } from '@cartamd/plugin-slash';
|
import { slash } from '@cartamd/plugin-slash';
|
||||||
import { code } from '@cartamd/plugin-code';
|
import { code } from '@cartamd/plugin-code';
|
||||||
|
import 'carta-md/dark.css';
|
||||||
import '$lib/styles/github.scss';
|
import '$lib/styles/github.scss';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
sanitizer: false,
|
|
||||||
extensions: [
|
extensions: [
|
||||||
attachment({
|
attachment({
|
||||||
async upload() {
|
async upload() {
|
||||||
|
@ -21,10 +20,7 @@
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export let value = `This is an example inspired by [GitHub](https://github.com)
|
export let value = 'This is an example inspired by [GitHub](https://github.com)';
|
||||||
\`\`\`js
|
|
||||||
console.log('Hello, World!');
|
|
||||||
\`\`\``;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor bind:value mode="tabs" theme="github" {carta} />
|
<CartaEditor bind:value mode="tabs" theme="github" {carta} />
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor, Markdown } from 'carta-md';
|
import { Carta, CartaEditor, CartaViewer } from 'carta-md';
|
||||||
import placeholder from './math-stack-exchange-placeholder.tex?raw';
|
import placeholder from './math-stack-exchange-placeholder.tex?raw';
|
||||||
import { math } from '@cartamd/plugin-math';
|
import { math } from '@cartamd/plugin-math';
|
||||||
import { tikz } from '@cartamd/plugin-tikz';
|
import { tikz } from '@cartamd/plugin-tikz';
|
||||||
|
import 'carta-md/dark.css';
|
||||||
import '$lib/styles/math-stack-exchange.scss';
|
import '$lib/styles/math-stack-exchange.scss';
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
sanitizer: false,
|
|
||||||
extensions: [
|
extensions: [
|
||||||
math(),
|
math(),
|
||||||
tikz({
|
tikz({
|
||||||
|
@ -31,9 +30,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="math-stack-exchange-container">
|
<div class="math-stack-exchange-container">
|
||||||
<MarkdownEditor bind:value mode="tabs" theme="math-stack-exchange" {carta} />
|
<CartaEditor bind:value mode="tabs" theme="math-stack-exchange" {carta} />
|
||||||
|
|
||||||
{#key value}
|
{#key value}
|
||||||
<Markdown theme="math-stack-exchange" {value} {carta} />
|
<CartaViewer theme="math-stack-exchange" {value} {carta} />
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="discord-plus-icon">
|
|
||||||
<iconify-icon icon="radix-icons:plus-circled" class="text-2xl"></iconify-icon>
|
|
||||||
</div>
|
|
|
@ -27,7 +27,6 @@
|
||||||
.carta-font-code {
|
.carta-font-code {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
caret-color: white;
|
caret-color: white;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-toolbar {
|
.carta-toolbar {
|
||||||
|
@ -42,6 +41,10 @@
|
||||||
height: 1.75rem;
|
height: 1.75rem;
|
||||||
transform: translateX(-50%) translateY(-50%);
|
transform: translateX(-50%) translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[class*='shj-lang-'] {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin emoji
|
// Plugin emoji
|
||||||
|
@ -76,8 +79,3 @@
|
||||||
background: $background-contrast;
|
background: $background-contrast;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .shiki,
|
|
||||||
html.dark .shiki span {
|
|
||||||
color: var(--shiki-dark) !important;
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
.carta-font-code {
|
.carta-font-code {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
caret-color: white;
|
caret-color: white;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-toolbar {
|
.carta-toolbar {
|
||||||
|
@ -116,6 +115,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[class*='shj-lang-'] {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin emoji
|
// Plugin emoji
|
||||||
|
@ -196,8 +199,3 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .shiki,
|
|
||||||
html.dark .shiki span {
|
|
||||||
color: var(--shiki-dark) !important;
|
|
||||||
}
|
|
||||||
|
|
|
@ -43,21 +43,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@apply rounded bg-neutral-800 px-1 text-neutral-50;
|
@apply rounded;
|
||||||
}
|
&:not([class*='language-']) {
|
||||||
|
@apply bg-neutral-800 px-1 text-neutral-100;
|
||||||
p,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
code {
|
|
||||||
@apply px-1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-editor code {
|
|
||||||
font-family: 'Fira Code', monospace;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
.carta-font-code {
|
.carta-font-code {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
caret-color: white;
|
caret-color: white;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-toolbar {
|
.carta-toolbar {
|
||||||
|
@ -65,6 +64,10 @@
|
||||||
.carta-toolbar-right {
|
.carta-toolbar-right {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[class*='shj-lang-'] {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-icons-menu {
|
.carta-icons-menu {
|
||||||
|
@ -101,8 +104,3 @@
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .shiki,
|
|
||||||
html.dark .shiki span {
|
|
||||||
color: var(--shiki-dark) !important;
|
|
||||||
}
|
|
||||||
|
|
|
@ -54,41 +54,3 @@ export const flyAndScale = (
|
||||||
easing: cubicOut
|
easing: cubicOut
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const throttle = <R, A extends unknown[]>(
|
|
||||||
fn: (...args: A) => R,
|
|
||||||
delay: number
|
|
||||||
): [(...args: A) => R | undefined, () => void] => {
|
|
||||||
let wait = false;
|
|
||||||
let timeout: undefined | number;
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
return [
|
|
||||||
(...args: A) => {
|
|
||||||
if (cancelled) return undefined;
|
|
||||||
if (wait) return undefined;
|
|
||||||
|
|
||||||
const val = fn(...args);
|
|
||||||
|
|
||||||
wait = true;
|
|
||||||
|
|
||||||
timeout = window.setTimeout(() => {
|
|
||||||
wait = false;
|
|
||||||
}, delay);
|
|
||||||
|
|
||||||
return val;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
cancelled = true;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function debounce<T extends unknown[]>(cb: (...args: T) => unknown, wait = 1000) {
|
|
||||||
let timeout: NodeJS.Timeout;
|
|
||||||
return (...args: T) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => cb(...args), wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,15 +17,9 @@ new Carta({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### `gfmOptions`
|
|
||||||
|
|
||||||
Type: `GfmOptions`
|
|
||||||
|
|
||||||
GitHub Flavored Markdown options.
|
|
||||||
|
|
||||||
### `extensions`
|
### `extensions`
|
||||||
|
|
||||||
Type: `Extension[]`
|
Type: `CartaExtension[]`
|
||||||
|
|
||||||
List of extensions(plugins) to use.
|
List of extensions(plugins) to use.
|
||||||
|
|
||||||
|
@ -78,21 +72,9 @@ Type: `(html: string) => void`
|
||||||
|
|
||||||
HTML sanitizer. See [here]({base}/getting-started#sanitization) for more details.
|
HTML sanitizer. See [here]({base}/getting-started#sanitization) for more details.
|
||||||
|
|
||||||
### `shikiOptions`
|
# `CartaEditor` options
|
||||||
|
|
||||||
Type: `ShikiOptions`
|
List of options that can be used in the `<CartaEditor>` component.
|
||||||
|
|
||||||
Highlighter(Shiki) options.
|
|
||||||
|
|
||||||
### `theme`
|
|
||||||
|
|
||||||
Type: `Theme | DualTheme`
|
|
||||||
|
|
||||||
Shiki theme to use to highlight Markdown.
|
|
||||||
|
|
||||||
# `MarkdownEditor` options
|
|
||||||
|
|
||||||
List of options that can be used in the `<MarkdownEditor>` component.
|
|
||||||
|
|
||||||
### `carta`
|
### `carta`
|
||||||
|
|
||||||
|
@ -146,13 +128,13 @@ instead.
|
||||||
|
|
||||||
### `labels`
|
### `labels`
|
||||||
|
|
||||||
Type: `Partial<Labels>`
|
Type: `Partial<CartaLabels>`
|
||||||
|
|
||||||
Can be used to provide custom text for labels in the editor.
|
Can be used to provide custom text for labels in the editor.
|
||||||
|
|
||||||
# `Markdown` options
|
# `CartaViewer` options
|
||||||
|
|
||||||
List of options that can be used in the `<Markdown>` component.
|
List of options that can be used in the `<CartaViewer>` component.
|
||||||
|
|
||||||
### `carta`
|
### `carta`
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,14 @@ title: Extension
|
||||||
import Code from '$lib/components/code/Code.svelte';
|
import Code from '$lib/components/code/Code.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
# `Plugin` properties
|
# `CartaExtension` properties
|
||||||
|
|
||||||
You can easily extend Carta by creating custom plugins.
|
You can easily extend Carta by creating custom plugins.
|
||||||
|
|
||||||
<Code>
|
<Code>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const ext: Plugin = {
|
const ext: CartaExtension = {
|
||||||
// ...
|
// ...
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,47 +25,11 @@ const carta = new Carta({
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
Here are all the `Plugin` properties:
|
Here are all the `CartaExtension` properties:
|
||||||
|
|
||||||
### `transformers`
|
### `markedExtensions`
|
||||||
|
|
||||||
Type: `UnifiedTransformer`
|
List of marked extensions. For more information check out [Marked docs](https://marked.js.org/using_pro).
|
||||||
|
|
||||||
Remark or Rehype transformers.
|
|
||||||
|
|
||||||
#### `UnifiedTransformer.execution`
|
|
||||||
|
|
||||||
Type: `'sync' | 'async'`
|
|
||||||
|
|
||||||
If you specify async, this transformer won't be available for SSR.
|
|
||||||
|
|
||||||
#### `UnifiedTransformer.type`
|
|
||||||
|
|
||||||
Type: `'remark' | 'rehype'`
|
|
||||||
|
|
||||||
This determines at which step the transformer will operate, whether on Remark, on a Markdown-based syntax tree, or Rehype, on a HTML-based one.
|
|
||||||
|
|
||||||
#### `UnifiedTransformer.transform`
|
|
||||||
|
|
||||||
Type: `({ processor, carta }) => void`
|
|
||||||
|
|
||||||
The actual processor, can be async if the execution is specified as such.
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
execution: 'sync',
|
|
||||||
type: 'rehype',
|
|
||||||
transform({ processor }) {
|
|
||||||
processor
|
|
||||||
.use(rehypeSlug)
|
|
||||||
.use(rehypeAutolinkHeadings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
### `shortcuts`
|
### `shortcuts`
|
||||||
|
|
||||||
|
@ -99,7 +63,7 @@ Set of keys, corresponding to the `e.key` of `KeyboardEvent`s, but lowercase.
|
||||||
|
|
||||||
#### `KeyboardShortcut.action`
|
#### `KeyboardShortcut.action`
|
||||||
|
|
||||||
Type: `(input: InputEnhancer) => void`
|
Type: `(input: CartaInput) => void`
|
||||||
|
|
||||||
Shortcut callback.
|
Shortcut callback.
|
||||||
|
|
||||||
|
@ -109,14 +73,14 @@ Prevent saving the current state in history.
|
||||||
|
|
||||||
### `icons`
|
### `icons`
|
||||||
|
|
||||||
Type: `Icon[]`
|
Type: `CartaIcon[]`
|
||||||
|
|
||||||
Additional toolbar icons. For example:
|
Additional toolbar icons. For example:
|
||||||
|
|
||||||
<Code>
|
<Code>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const icon: Icon = {
|
const icon: CartaIcon = {
|
||||||
id: 'heading',
|
id: 'heading',
|
||||||
action: (input) => input.toggleLinePrefix('###'),
|
action: (input) => input.toggleLinePrefix('###'),
|
||||||
component: HeadingIcon
|
component: HeadingIcon
|
||||||
|
@ -125,19 +89,19 @@ const icon: Icon = {
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
#### `Icon.id`
|
#### `CartaIcon.id`
|
||||||
|
|
||||||
Type: `string`
|
Type: `string`
|
||||||
|
|
||||||
Id of the icon.
|
Id of the icon.
|
||||||
|
|
||||||
#### `Icon.action`
|
#### `CartaIcon.action`
|
||||||
|
|
||||||
Type: `(input: InputEnhancer) => void`
|
Type: `(input: CartaInput) => void`
|
||||||
|
|
||||||
Click callback.
|
Click callback.
|
||||||
|
|
||||||
#### `Icon.component`
|
#### `CartaIcon.component`
|
||||||
|
|
||||||
Type: `ComponentType` (SvelteComponent)
|
Type: `ComponentType` (SvelteComponent)
|
||||||
|
|
||||||
|
@ -198,15 +162,15 @@ const prefix: Prefix = {
|
||||||
|
|
||||||
### `listeners`
|
### `listeners`
|
||||||
|
|
||||||
Type: `Listener[]`
|
Type: `CartaListener[]`
|
||||||
|
|
||||||
Textarea event listeners. Has an additional `carta-render` and `carta-render-ssr` events keys.
|
Textarea event listeners. Has an additional `carta-render` and `carta-render-ssr` events keys.
|
||||||
|
|
||||||
<Code>
|
<Code>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const click: Listener = ['click', () => console.log('I was clicked!')];
|
const click: CartaListener = ['click', () => console.log('I was clicked!')];
|
||||||
const render: Listener = [
|
const render: CartaListener = [
|
||||||
'carta-render',
|
'carta-render',
|
||||||
(e) => {
|
(e) => {
|
||||||
const carta = e.detail.carta;
|
const carta = e.detail.carta;
|
||||||
|
@ -222,39 +186,33 @@ const render: Listener = [
|
||||||
|
|
||||||
### `components`
|
### `components`
|
||||||
|
|
||||||
Type: `ExtensionComponent[]`
|
Type: `CartaExtensionComponent[]`
|
||||||
|
|
||||||
Additional components to be added to the editor or viewer.
|
Additional components to be added to the editor or viewer.
|
||||||
|
|
||||||
#### `ExtensionComponent<T>.component`
|
#### `CartaExtensionComponent<T>.component`
|
||||||
|
|
||||||
Type: `typeof SvelteComponentTyped<T & { carta: Carta }>`
|
Type: `typeof SvelteComponentTyped<T & { carta: Carta }>`
|
||||||
|
|
||||||
Svelte components that exports `carta: Carta` and all the other properties specified as the generic parameter and in `props`.
|
Svelte components that exports `carta: Carta` and all the other properties specified as the generic parameter and in `props`.
|
||||||
|
|
||||||
#### `ExtensionComponent<T>.props`
|
#### `CartaExtensionComponent<T>.props`
|
||||||
|
|
||||||
Type: `T`
|
Type: `T`
|
||||||
|
|
||||||
Properties that will be handed to the component.
|
Properties that will be handed to the component.
|
||||||
|
|
||||||
#### `ExtensionComponent<T>.parent`
|
#### `CartaExtensionComponent<T>.parent`
|
||||||
|
|
||||||
Type: `MaybeArray<'editor' | 'input' | 'renderer' | 'preview'>`
|
Type: `MaybeArray<'editor' | 'input' | 'renderer' | 'preview'>`
|
||||||
|
|
||||||
Where the element will be placed.
|
Where the element will be placed.
|
||||||
|
|
||||||
### `grammarRules`
|
|
||||||
|
|
||||||
Type: `GrammarRule[]`
|
|
||||||
|
|
||||||
Custom Markdown TextMate grammar rules for Shiki. They will be injected into the language.
|
|
||||||
|
|
||||||
### `highlightRules`
|
### `highlightRules`
|
||||||
|
|
||||||
Type: `HighlightingRule[]`
|
Type: `ShjLanguageDefinition`
|
||||||
|
|
||||||
Custom highlighting rules for ShiKi. They will be injected into the selected theme.
|
Custom markdown highlighting rules. See [Speed-Highlight Wiki](https://github.com/speed-highlight/core/wiki/Create-or-suggest-new-languages) for more info.
|
||||||
|
|
||||||
### `onLoad`
|
### `onLoad`
|
||||||
|
|
||||||
|
|
|
@ -40,52 +40,3 @@ Svelte action that allows you to bind a specific element to the caret position.
|
||||||
<!-- ... -->
|
<!-- ... -->
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
## `Carta.highlighter`
|
|
||||||
|
|
||||||
Get the Shiki highlighter.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const highlighter = await carta.highlighter();
|
|
||||||
const userTheme = carta.theme;
|
|
||||||
```
|
|
||||||
|
|
||||||
Here are some other highlight related utilities:
|
|
||||||
|
|
||||||
### `isBundleLanguage`
|
|
||||||
|
|
||||||
Checks if a language is a bundled language.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const isBundleLanguage = (lang: string): lang is BundledLanguage;
|
|
||||||
```
|
|
||||||
|
|
||||||
### `isBundleTheme`
|
|
||||||
|
|
||||||
Checks if a theme is a bundled theme.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const isBundleTheme = (theme: string): theme is BundledTheme;
|
|
||||||
```
|
|
||||||
|
|
||||||
### `isDualTheme`
|
|
||||||
|
|
||||||
Checks if a theme is a dual theme.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme;
|
|
||||||
```
|
|
||||||
|
|
||||||
### `isSingleTheme`
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme;
|
|
||||||
```
|
|
||||||
|
|
||||||
### `isThemeRegistration`
|
|
||||||
|
|
||||||
Checks if a theme is a theme registration.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const isThemeRegistration = (theme: Theme): theme is ThemeRegistration;
|
|
||||||
```
|
|
||||||
|
|
|
@ -43,68 +43,11 @@ While the core styles are embedded in the Svelte components, the others can be s
|
||||||
|
|
||||||
### Using multiple themes
|
### Using multiple themes
|
||||||
|
|
||||||
By using the `theme` property in `<MarkdownEditor>` you can differentiate the themes of multiple editors.
|
By using the `theme` property in the editor you can differentiate the themes of multiple editors.
|
||||||
|
|
||||||
## Dark mode
|
## Changing Markdown color theme
|
||||||
|
|
||||||
When using dark mode, there are two different themes that have to be changed: the editor theme and the one used for syntax highlighting:
|
Carta uses [Speed Highlight JS](https://github.com/speed-highlight/core) for syntax highlighting. Two default themes are included in the core package, `light.css` and `dark.css`, and others can be found on the Speed Highlight [GitHub](https://github.com/speed-highlight/core/tree/main/src/themes), but you can also easily create your own.
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Editor dark mode */
|
|
||||||
/* Only if you are using the default theme */
|
|
||||||
html.dark .carta-theme__default {
|
|
||||||
--border-color: var(--border-color-dark);
|
|
||||||
--selection-color: var(--selection-color-dark);
|
|
||||||
--focus-outline: var(--focus-outline-dark);
|
|
||||||
--hover-color: var(--hover-color-dark);
|
|
||||||
--caret-color: var(--caret-color-dark);
|
|
||||||
--text-color: var(--text-color-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code dark mode */
|
|
||||||
/* Only if you didn't specify a custom code theme */
|
|
||||||
html.dark .shiki,
|
|
||||||
html.dark .shiki span {
|
|
||||||
color: var(--shiki-dark) !important;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
## Changing Markdown input color theme
|
|
||||||
|
|
||||||
Carta uses [Shiki](https://shiki.matsu.io/) for syntax highlighting. Two default themes are included in the core package, which are set as a [dual theme](https://shiki.matsu.io/guide/dual-themes) to support light and dark mode. If you plan to use a custom one with light/dark modes, make sure to use a dual theme as well.
|
|
||||||
|
|
||||||
You can change theme in the options:
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const carta = new Carta({
|
|
||||||
// ...
|
|
||||||
theme: 'github-dark'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
If you use a [custom theme](https://shiki.matsu.io/guide/load-theme)(or a custom language), you need to provide it inside the options, so that it gets loaded into the highlighter:
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const carta = new Carta({
|
|
||||||
// ...
|
|
||||||
shikiOptions: {
|
|
||||||
langs: // ...
|
|
||||||
themes: // ...
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
## Markdown stylesheets
|
## Markdown stylesheets
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,9 @@ Setup a basic editor:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import 'carta-md/default.css'; /* Default theme */
|
import 'carta-md/default.css'; /* Default theme */
|
||||||
|
import 'carta-md/light.css'; /* Markdown input theme */
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
// Remember to use a sanitizer to prevent XSS attacks!
|
// Remember to use a sanitizer to prevent XSS attacks!
|
||||||
|
@ -49,13 +50,12 @@ Setup a basic editor:
|
||||||
let value = '';
|
let value = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} bind:value />
|
<CartaEditor {carta} bind:value />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Set your custom monospace font */
|
/* Set your custom monospace font */
|
||||||
:global(.carta-font-code) {
|
:global(.carta-font-code) {
|
||||||
font-family: '...', monospace;
|
font-family: '...', monospace;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
```
|
```
|
||||||
|
@ -68,7 +68,7 @@ Or, if you just want to render content:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, Markdown } from 'carta-md';
|
import { Carta, CartaViewer } from 'carta-md';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
/* ... */
|
/* ... */
|
||||||
|
@ -77,7 +77,7 @@ Or, if you just want to render content:
|
||||||
let value = '...';
|
let value = '...';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Markdown {carta} {value} />
|
<CartaViewer {carta} {value} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
@ -102,7 +102,7 @@ Since Carta operates both on the server and the client, you'd need a sanitizer a
|
||||||
let value = '';
|
let value = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} bind:value />
|
<CartaEditor {carta} bind:value />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
|
@ -5,21 +5,21 @@ section: Overview
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as Card from "$lib/components/ui/card";
|
import * as Card from "$lib/components/ui/card";
|
||||||
|
import * as Icon from "radix-icons-svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
> Modern, lightweight, powerful Markdown Editor.
|
> Swiftly edit and render Markdown, with no overhead.
|
||||||
|
|
||||||
Carta is a lightweight, fast and extensible Svelte Markdown editor and viewer, designed for flexibility. It works natively in SvelteKit, and supports Server Side Rendering.
|
Carta is a lightweight, fast and extensible Svelte Markdown editor and viewer, designed for flexibility. It works natively in SvelteKit, and supports Server Side Rendering.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
|
- **Lightweight**: no code editor is included, just a textarea with syntax highlighting, with Markdown related utilities.
|
||||||
- 🛠️ Toolbar (extensible);
|
- **SSR compatible**: works great with SvelteKit.
|
||||||
- ⌨️ Keyboard **shortcuts** (extensible);
|
- **Keyboard shortcuts**: extensible and configurable.
|
||||||
- 📦 Supports **[+150 plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark.
|
- **Toolbar**: add or remove buttons according to your needs.
|
||||||
- 🔀 Scroll sync;
|
- **Plugins friendly**: easily create your own extension.
|
||||||
- ✅ Accessibility friendly;
|
- **Accessibility**: includes ARIA roles, arrow keys navigation and labels.
|
||||||
- 🖥️ **SSR** compatible;
|
|
||||||
|
|
||||||
## Official Plugins
|
## Official Plugins
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/plugins/math">
|
<Card.Root href="/plugins/math">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="tabler:math" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.FontFamily class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Math</Card.Title>
|
<Card.Title>Math</Card.Title>
|
||||||
<Card.Description>Support for KaTex expressions.</Card.Description>
|
<Card.Description>Support for KaTex expressions.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -39,7 +39,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/plugins/code">
|
<Card.Root href="/plugins/code">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="fluent:code-16-filled" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.Code class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Code</Card.Title>
|
<Card.Title>Code</Card.Title>
|
||||||
<Card.Description>Code blocks syntax highlighting.</Card.Description>
|
<Card.Description>Code blocks syntax highlighting.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -47,7 +47,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/plugins/emoji">
|
<Card.Root href="/plugins/emoji">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="mingcute:emoji-line" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.Face class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Emoji</Card.Title>
|
<Card.Title>Emoji</Card.Title>
|
||||||
<Card.Description>Embed emojis in Markdown.</Card.Description>
|
<Card.Description>Embed emojis in Markdown.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -55,7 +55,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/plugins/slash">
|
<Card.Root href="/plugins/slash">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="tabler:slash" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.Slash class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Slash</Card.Title>
|
<Card.Title>Slash</Card.Title>
|
||||||
<Card.Description>Support for slash commands.</Card.Description>
|
<Card.Description>Support for slash commands.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -63,7 +63,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/plugins/tikz">
|
<Card.Root href="/plugins/tikz">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="mdi:draw-pen" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.Cube class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>TikZ</Card.Title>
|
<Card.Title>TikZ</Card.Title>
|
||||||
<Card.Description>Support for TikZ/PgfPlots diagrams.</Card.Description>
|
<Card.Description>Support for TikZ/PgfPlots diagrams.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -71,7 +71,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/plugins/attachment">
|
<Card.Root href="/plugins/attachment">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="tdesign:attach" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.File class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Attachment</Card.Title>
|
<Card.Title>Attachment</Card.Title>
|
||||||
<Card.Description>Handle text attachments.</Card.Description>
|
<Card.Description>Handle text attachments.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -79,7 +79,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/plugins/anchor">
|
<Card.Root href="/plugins/anchor">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="mingcute:link-fill" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.Link2 class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Anchor</Card.Title>
|
<Card.Title>Anchor</Card.Title>
|
||||||
<Card.Description>Add anchor links to headings.</Card.Description>
|
<Card.Description>Add anchor links to headings.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -87,7 +87,7 @@ Carta comes with a set of official plugins for the most common use cases.
|
||||||
|
|
||||||
<Card.Root href="/community-plugins">
|
<Card.Root href="/community-plugins">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="ph:stack-fill" class="text-3xl text-sky-300"></iconify-icon>
|
<Icon.Stack class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Community Plugins</Card.Title>
|
<Card.Title>Community Plugins</Card.Title>
|
||||||
<Card.Description>Explore plugins from the community.</Card.Description>
|
<Card.Description>Explore plugins from the community.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -105,7 +105,7 @@ A list of examples inspired by popular platforms.
|
||||||
|
|
||||||
<Card.Root href="/examples#github">
|
<Card.Root href="/examples#github">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="mdi:github" class="text-3xl text-sky-300" ></iconify-icon>
|
<Icon.GithubLogo class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>GitHub</Card.Title>
|
<Card.Title>GitHub</Card.Title>
|
||||||
<Card.Description>Inspired by GitHub.</Card.Description>
|
<Card.Description>Inspired by GitHub.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -113,7 +113,7 @@ A list of examples inspired by popular platforms.
|
||||||
|
|
||||||
<Card.Root href="/examples#discord">
|
<Card.Root href="/examples#discord">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="ic:baseline-discord" class="text-3xl text-sky-300" ></iconify-icon>
|
<Icon.DiscordLogo class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Discord</Card.Title>
|
<Card.Title>Discord</Card.Title>
|
||||||
<Card.Description>Inspired by Discord.</Card.Description>
|
<Card.Description>Inspired by Discord.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
@ -121,7 +121,7 @@ A list of examples inspired by popular platforms.
|
||||||
|
|
||||||
<Card.Root href="/examples#math-stack-exchange">
|
<Card.Root href="/examples#math-stack-exchange">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<iconify-icon icon="fluent:math-formula-16-filled" class="text-3xl text-sky-300" ></iconify-icon>
|
<Icon.Cube class="w-8 h-8 text-sky-300" />
|
||||||
<Card.Title>Math Stack Exchange</Card.Title>
|
<Card.Title>Math Stack Exchange</Card.Title>
|
||||||
<Card.Description>Inspired by Math Stack Exchange.</Card.Description>
|
<Card.Description>Inspired by Math Stack Exchange.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
---
|
|
||||||
title: Migration Guide
|
|
||||||
section: Overview
|
|
||||||
---
|
|
||||||
|
|
||||||
# Major Changes
|
|
||||||
|
|
||||||
## Removal of Marked
|
|
||||||
|
|
||||||
Marked has been replaced with a combination of Unified, Remark and Rehype. If you previously used a custom plugin with it, you'll have to update it manually. Otherwise, all builtin plugins have already been updated. Make sure to **update** them!
|
|
||||||
|
|
||||||
Some plugins now have a different implementation and their options have changed. Those plugins are [plugin-math](https://beartocode.github.io/carta/plugins/math) and [plugin-anchor](https://beartocode.github.io/carta/plugins/anchor).
|
|
||||||
|
|
||||||
## Syntax highlighter update
|
|
||||||
|
|
||||||
SpeedHighlight has been replaced with [Shiki](https://shiki.matsu.io/). It now offers support for more languages, themes, and extensibility.
|
|
||||||
|
|
||||||
Make sure to remove previous themes imports, as Shiki uses JS based ones.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import 'carta-md/light.css'; // 👈 To be removed!
|
|
||||||
```
|
|
||||||
|
|
||||||
And also update the default theme. Previous based selectors should be removed:
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* 👇 To be removed! */
|
|
||||||
[class*='shj-lang-'] {
|
|
||||||
/* ... */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Removed verbose prefixes
|
|
||||||
|
|
||||||
Many exports have been renamed to make them less verbose:
|
|
||||||
|
|
||||||
- `CartaEditor` -> `MarkdownEditor` (old one still supported);
|
|
||||||
- `CartaRenderer` -> `Markdown` (old one still supported);
|
|
||||||
- `CartaEvent` -> `Event`;
|
|
||||||
- `CartaEventType` -> `EventType`;
|
|
||||||
- `CartaExtension` -> `Plugin`;
|
|
||||||
- `CartaExtensionComponent` -> `ExtensionComponent`;
|
|
||||||
- `CartaOptions` -> `Options`;
|
|
||||||
- `CartaHistory` -> `TextAreaHistory`;
|
|
||||||
- `CartaHistoryOptions` -> `TextAreaHistoryOptions`;
|
|
||||||
- `CartaIcon` -> `Icon`;
|
|
||||||
- `CartaListener` -> `Listener`;
|
|
||||||
- `CartaInput` -> `InputEnhancer`;
|
|
||||||
- `CartaRenderer` -> `Renderer`;
|
|
||||||
- `CartaLabels` -> `Labels`;
|
|
||||||
|
|
||||||
# Minor Changes
|
|
||||||
|
|
||||||
- If you don't use a sanitizer, you need to explicitly set it to `false`;
|
|
||||||
- Removed deprecated option `cartaRef` and `shjRef` for extensions;
|
|
||||||
- Removed deprecated options `postProcess` for `plugin-tikz`;
|
|
||||||
- `Carta.options` are no longer available.
|
|
|
@ -39,7 +39,7 @@ import '@cartamd/plugin-anchor/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { anchor } from '@cartamd/plugin-anchor';
|
import { anchor } from '@cartamd/plugin-anchor';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -47,7 +47,7 @@ import '@cartamd/plugin-anchor/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
@ -59,12 +59,8 @@ Here are the options you can pass to `anchor()`:
|
||||||
```ts
|
```ts
|
||||||
export interface AnchorExtensionOptions {
|
export interface AnchorExtensionOptions {
|
||||||
/**
|
/**
|
||||||
* rehype-slug options.
|
* Maximum depth of headers to generate anchors for. Defaults to 6.
|
||||||
*/
|
*/
|
||||||
slug?: SlugOptions;
|
maxDepth?: number;
|
||||||
/**
|
|
||||||
* rehype-autolink-headings options.
|
|
||||||
*/
|
|
||||||
autolink?: AutolinkOptions;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -35,7 +35,7 @@ import '@cartamd/plugin-attachment/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { attachment } from '@cartamd/plugin-attachment';
|
import { attachment } from '@cartamd/plugin-attachment';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -49,7 +49,7 @@ import '@cartamd/plugin-attachment/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
|
@ -7,7 +7,8 @@ title: Code
|
||||||
import Code from '$lib/components/code/Code.svelte';
|
import Code from '$lib/components/code/Code.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
|
This plugin adds support for code blocks **syntax highlighting**.
|
||||||
|
This is done using [Speed-highlight JS](https://github.com/speed-highlight/core), which supports dynamic imports. This way, languages definitions are only imported at the moment of need.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -35,34 +36,34 @@ import '@cartamd/plugin-code/default.css';
|
||||||
|
|
||||||
### Using the default highlighter
|
### Using the default highlighter
|
||||||
|
|
||||||
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options.
|
Carta comes with a default highlighter that matches the one used to highlight Markdown in the editor and is used by default.
|
||||||
|
The theme is the same as the one used in the main Carta package (`carta-md/light.css` or `carta-md/dark.css`).
|
||||||
|
[Here](https://github.com/speed-highlight/core/tree/main/src/themes) you can find other themes.
|
||||||
|
|
||||||
|
### Using a custom highlighter
|
||||||
|
|
||||||
|
You can also provide a custom highlighter, that can be either sync or async.
|
||||||
|
|
||||||
<Code>
|
<Code>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const carta = new Carta({
|
code({
|
||||||
// ...
|
customHighlight: {
|
||||||
extensions: [
|
highlighter: (code, lang) => myCustomHighlighter(code, lang),
|
||||||
code({
|
langPrefix: 'my-highlighter-'
|
||||||
theme: 'ayu-light'
|
}
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
### Using a custom highlighter
|
|
||||||
|
|
||||||
It is no longer possible to specify a custom highlighter in this plugin. However, there are many different [Remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) that provide syntax highlighting.
|
|
||||||
|
|
||||||
### Extension
|
### Extension
|
||||||
|
|
||||||
<Code>
|
<Code>
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { code } from '@cartamd/plugin-code';
|
import { code } from '@cartamd/plugin-code';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -70,11 +71,46 @@ It is no longer possible to specify a custom highlighter in this plugin. However
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
The options you can pass to `code()` extend the ones provided by [Shiki](https://shiki.matsu.io/guide/transformers).
|
Here are the options you can pass to `code()`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CodeExtensionOptions {
|
||||||
|
/**
|
||||||
|
* Default language when none is provided.
|
||||||
|
*/
|
||||||
|
defaultLanguage?: string;
|
||||||
|
/**
|
||||||
|
* Whether to autodetect a language when none is provided.
|
||||||
|
* Overwritten by `defaultLanguage`.
|
||||||
|
*/
|
||||||
|
autoDetect?: string;
|
||||||
|
/**
|
||||||
|
* Line numbering.
|
||||||
|
* @defaults false.
|
||||||
|
*/
|
||||||
|
lineNumbering?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for custom syntax highlighting.
|
||||||
|
*/
|
||||||
|
customHighlight?: {
|
||||||
|
/**
|
||||||
|
* Custom highlight function. Beware that you'll have to provide your own styles.
|
||||||
|
* This function needs to convert a string of code into html.
|
||||||
|
*/
|
||||||
|
highlighter: (code: string, lang: string) => string | Promise<string>;
|
||||||
|
/**
|
||||||
|
* The language tag found immediately after the code block opening marker is
|
||||||
|
* appended to this to form the class attribute added to the `<code>` element.
|
||||||
|
*/
|
||||||
|
langPrefix: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -39,7 +39,7 @@ import '@cartamd/plugin-emoji/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { emoji } from '@cartamd/plugin-emoji';
|
import { emoji } from '@cartamd/plugin-emoji';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -47,7 +47,7 @@ import '@cartamd/plugin-emoji/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
|
@ -5,7 +5,7 @@ title: Math
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Code from '$lib/components/code/Code.svelte';
|
import Code from '$lib/components/code/Code.svelte';
|
||||||
import { Markdown, Carta } from 'carta-md';
|
import { CartaViewer, Carta } from 'carta-md';
|
||||||
import { math } from '@cartamd/plugin-math';
|
import { math } from '@cartamd/plugin-math';
|
||||||
import 'katex/dist/katex.css';
|
import 'katex/dist/katex.css';
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ or by using a content delivery network:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { math } from '@cartamd/plugin-math';
|
import { math } from '@cartamd/plugin-math';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -86,7 +86,7 @@ or by using a content delivery network:
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
@ -103,7 +103,7 @@ Pythagorean theorem: $a^2+b^2=c^2$
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
<Markdown {carta} value={inline} />
|
<CartaViewer {carta} value={inline} />
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ $$
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
<Markdown {carta} value={block} />
|
<CartaViewer {carta} value={block} />
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
@ -131,6 +131,7 @@ interface MathExtensionOptions {
|
||||||
* Options for inline katex, eg: $a^2+b^2=c^2$
|
* Options for inline katex, eg: $a^2+b^2=c^2$
|
||||||
*/
|
*/
|
||||||
inline?: {
|
inline?: {
|
||||||
|
katexOptions?: KatexOptions;
|
||||||
/**
|
/**
|
||||||
* @default control+m
|
* @default control+m
|
||||||
*/
|
*/
|
||||||
|
@ -143,18 +144,23 @@ interface MathExtensionOptions {
|
||||||
* $$
|
* $$
|
||||||
*/
|
*/
|
||||||
block?: {
|
block?: {
|
||||||
|
/**
|
||||||
|
* Tag the generated katex will be put into. Must have `display: block`.
|
||||||
|
*/
|
||||||
|
tag?: string;
|
||||||
|
/**
|
||||||
|
* Whether to center the generated expression.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
center?: boolean;
|
||||||
|
/**
|
||||||
|
* Class for generated katex.
|
||||||
|
*/
|
||||||
|
class?: string;
|
||||||
/**
|
/**
|
||||||
* @default ctrl+shift+m
|
* @default ctrl+shift+m
|
||||||
*/
|
*/
|
||||||
shortcut?: Set<string>;
|
shortcut?: Set<string>;
|
||||||
|
katexOptions?: KatexOptions;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Options for remark-math
|
|
||||||
*/
|
|
||||||
remarkMath?: RemarkMathOptions;
|
|
||||||
/**
|
|
||||||
* Options for rehype-katex
|
|
||||||
*/
|
|
||||||
rehypeKatex?: RehypeKatexOptions;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -39,7 +39,7 @@ import '@cartamd/plugin-slash/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { slash } from '@cartamd/plugin-slash';
|
import { slash } from '@cartamd/plugin-slash';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -47,7 +47,7 @@ import '@cartamd/plugin-slash/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
|
@ -20,7 +20,7 @@ npm i @cartamd/plugin-tikz
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
1. This plugin requires the import of a **heavy** library (~7Mb), which is dynamically imported at runtime;
|
1. This plugin requires the import of a **heavy** library (~7Mb), which is dynamically imported at runtime;
|
||||||
2. Generated images are **not SSR compatible**, as they are rendered in the browser;
|
2. Generated images are **not ssr compatible**, as they are rendered in the browser;
|
||||||
3. You need to update your sanitizer to allow the specific tag: `<div type="text/tikz">`.
|
3. You need to update your sanitizer to allow the specific tag: `<div type="text/tikz">`.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
@ -29,7 +29,7 @@ npm i @cartamd/plugin-tikz
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { tikz } from '@cartamd/plugin-tikz';
|
import { tikz } from '@cartamd/plugin-tikz';
|
||||||
import '@cartamd/plugin-tikz/fonts.css';
|
import '@cartamd/plugin-tikz/fonts.css';
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ npm i @cartamd/plugin-tikz
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
|
@ -1,217 +0,0 @@
|
||||||
---
|
|
||||||
title: Using Svelte Components
|
|
||||||
section: Overview
|
|
||||||
---
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Code from '$lib/components/code/Code.svelte';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
Svelte components can be embedded into the rendered HTML to make certain elements interactive. However, they require a bit more work, as Remark is configured to only render static HTML. To get around this, the idea is to do the following:
|
|
||||||
|
|
||||||
1. Create a Unified plugin to isolate the targeted element;
|
|
||||||
2. Replace all the elements with the component, after every render.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
Let's say we want to replace all hashtags, such as `#something`, with a custom component. Here is as example of how that could be achieved.
|
|
||||||
|
|
||||||
### Parsing the hashtags
|
|
||||||
|
|
||||||
First things first: we need to tell the parser that we want to parse hashtags as custom elements. To do this, it's useful to first install the following packages:
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm i unist-util-visit
|
|
||||||
# Types
|
|
||||||
npm i -D unified hast
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
Let's create a Unified plugin. The basic structure of a plugin is the following:
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { Plugin as UnifiedPlugin } from 'unified'
|
|
||||||
import { SKIP, visit } from 'unist-util-visit'
|
|
||||||
|
|
||||||
const unifiedPlugin: UnifiedPlugin<[], hast.Root> = () => {
|
|
||||||
return function (tree) {
|
|
||||||
// Visit every node in the syntax tree
|
|
||||||
visit(tree, (node, index, parent) => {
|
|
||||||
// Do something with the node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
We now want to parse text nodes, so that words such as `#pizza` and `#123` are separated from the rest. This is a possible implementation:
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const unifiedPlugin: UnifiedPlugin<[], hast.Root> = () => {
|
|
||||||
return function (tree) {
|
|
||||||
visit(tree, (node, index, parent) => {
|
|
||||||
// Skip code blocks and their children
|
|
||||||
if (node.type === 'element' && node.tagName === 'pre') return [SKIP];
|
|
||||||
// Skip non-text nodes
|
|
||||||
if (node.type !== 'text') return;
|
|
||||||
const text = node as hast.Text;
|
|
||||||
|
|
||||||
// Parse the text node and replace hashtags with spans
|
|
||||||
const regex = /#(\w+)/g;
|
|
||||||
const children: (hast.Element | hast.Text)[] = [];
|
|
||||||
let lastIndex = 0;
|
|
||||||
let match;
|
|
||||||
while ((match = regex.exec(text.value))) {
|
|
||||||
const before = text.value.slice(lastIndex, match.index);
|
|
||||||
if (before) {
|
|
||||||
children.push({ type: 'text', value: before });
|
|
||||||
}
|
|
||||||
children.push({
|
|
||||||
type: 'element',
|
|
||||||
tagName: 'span',
|
|
||||||
properties: { type: 'hashtag', value: match[1] },
|
|
||||||
children: [{ type: 'text', value: match[0] }]
|
|
||||||
});
|
|
||||||
lastIndex = regex.lastIndex;
|
|
||||||
}
|
|
||||||
if (lastIndex < text.value.length) {
|
|
||||||
children.push({ type: 'text', value: text.value.slice(lastIndex) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the text node with all the children
|
|
||||||
parent!.children.splice(index!, 1, ...children);
|
|
||||||
|
|
||||||
// Skip the children
|
|
||||||
return [SKIP, index! + children.length];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
If you want a more in-depth guide on writing Unified plugins, you can check out the official [documentation](https://unifiedjs.com/learn/guide/create-a-plugin/).
|
|
||||||
|
|
||||||
Notice that hashtags are now replaced with the following:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<span type="hashtag" value="pizza"> #pizza </span>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuring the transformer
|
|
||||||
|
|
||||||
Unified plugins need to be wrapped inside a `UnifiedTransformer` type, to be able to be used in Carta.
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { UnifiedTransformer } from 'carta-md';
|
|
||||||
|
|
||||||
const hashtagTransformer: UnifiedTransformer<'sync'> = {
|
|
||||||
execution: 'sync', // Sync, since the plugin is synchronous
|
|
||||||
type: 'rehype', // Rehype, since it operates on HTML
|
|
||||||
transform({ processor }) {
|
|
||||||
processor.use(unifiedPlugin);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
### Mounting the components
|
|
||||||
|
|
||||||
We now want to replace the generated hashtag placeholders with the following element:
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```svelte
|
|
||||||
<!-- Hashtag.svelte -->
|
|
||||||
<script>
|
|
||||||
export let value;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
on:click={() => {
|
|
||||||
console.log('Hashtag clicked!');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
#{value}
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
To do that, we create a listener that:
|
|
||||||
|
|
||||||
1. Finds all the previous placeholders;
|
|
||||||
2. Mounts the component next to them;
|
|
||||||
3. Removes the placeholders.
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { Listener } from 'carta-md';
|
|
||||||
import Hashtag from './Hashtag.svelte';
|
|
||||||
|
|
||||||
const convertHashtags: Listener<'carta-render'> = [
|
|
||||||
'carta-render',
|
|
||||||
function onRender({ detail: { carta } }) {
|
|
||||||
const rendererContainer = carta.renderer?.container;
|
|
||||||
if (!rendererContainer) return;
|
|
||||||
|
|
||||||
// Find all hashtag spans and replace them with Svelte components
|
|
||||||
const hashtagSpans = rendererContainer.querySelectorAll('span[type="hashtag"]');
|
|
||||||
for (const span of hashtagSpans) {
|
|
||||||
const hashtag = span.getAttribute('value') ?? '';
|
|
||||||
|
|
||||||
new Hashtag({
|
|
||||||
target: span.parentElement!,
|
|
||||||
anchor: span,
|
|
||||||
props: { value: hashtag }
|
|
||||||
});
|
|
||||||
|
|
||||||
span.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
### Using the plugin
|
|
||||||
|
|
||||||
Let's now create a Plugin with the transformer and the listener:
|
|
||||||
|
|
||||||
<Code>
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { Plugin } from 'carta-md';
|
|
||||||
|
|
||||||
export const hashtag = (): Plugin => ({
|
|
||||||
transformers: [hashtagTransformer],
|
|
||||||
listeners: [convertHashtags]
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
</Code>
|
|
||||||
|
|
||||||
We can now use the plugin with the following:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Carta } from 'carta-md';
|
|
||||||
|
|
||||||
const carta = new Carta({
|
|
||||||
// ...
|
|
||||||
extensions: [hashtag()]
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
You can find the example source code [here](https://github.com/BearToCode/svelte-in-carta-example).
|
|
|
@ -4,8 +4,8 @@
|
||||||
import Navbar from '$lib/components/navbar/Navbar.svelte';
|
import Navbar from '$lib/components/navbar/Navbar.svelte';
|
||||||
import Sidebar from '$lib/components/sidebar/Sidebar.svelte';
|
import Sidebar from '$lib/components/sidebar/Sidebar.svelte';
|
||||||
import Footer from '$lib/components/footer/Footer.svelte';
|
import Footer from '$lib/components/footer/Footer.svelte';
|
||||||
import { base } from '$app/paths';
|
|
||||||
import '../app.postcss';
|
import '../app.postcss';
|
||||||
|
import { base } from '$app/paths';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
@ -24,6 +24,6 @@
|
||||||
<slot />
|
<slot />
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
<HeaderTracker class="sticky top-24 hidden w-[15rem] flex-shrink-0 xl:block" />
|
<HeaderTracker class="sticky top-24 hidden w-[30rem] xl:block" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
import 'iconify-icon'; // Register iconify web components
|
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { onMount, type SvelteComponent } from 'svelte';
|
import { onMount, type SvelteComponent } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { base } from '$app/paths';
|
|
||||||
|
|
||||||
import '$lib/styles/markdown.scss';
|
import '$lib/styles/markdown.scss';
|
||||||
import '$lib/styles/coldark.scss';
|
import '$lib/styles/coldark.scss';
|
||||||
|
import { base } from '$app/paths';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,31 @@
|
||||||
<div align="right">
|
<div align="right">
|
||||||
<a href="https://www.npmjs.com/package/carta-md">
|
<a href="https://www.npmjs.com/package/carta-md">
|
||||||
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
|
<img src="https://img.shields.io/npm/v/carta-md?color=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bundlephobia.com/package/carta-md">
|
<a href="https://bundlephobia.com/package/carta-md">
|
||||||
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
|
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
|
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
|
||||||
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&labelColor=171d27&logo=git&logoColor=white" alt="license">
|
<img src="https://img.shields.io/npm/l/carta-md?color=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
|
||||||
</a>
|
</a>
|
||||||
<a href="http://beartocode.github.io/carta/">
|
<a href="http://beartocode.github.io/carta/">
|
||||||
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs">
|
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[](https://beartocode.github.io/carta/)
|
<div align="center">
|
||||||
|
<a href="https://beartocode.github.io/carta/">
|
||||||
|
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 align="center"><strong>Carta</strong></h1>
|
<br>
|
||||||
<div align="center">Modern, lightweight, powerful Markdown Editor.</div>
|
|
||||||
|
<div align="center"><strong>Carta</strong></div>
|
||||||
|
<div align="center">Swiftly edit and render Markdown, with no overhead.</div>
|
||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://beartocode.github.io/carta/">📚 Documentation</a>
|
<a href="https://beartocode.github.io/carta/">Documentation</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://github.com/BearToCode/carta">GitHub</a>
|
<a href="https://github.com/BearToCode/carta">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,81 +34,38 @@
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
> **NOTE**:
|
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer, based on [Marked](https://github.com/markedjs/marked). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
|
||||||
> Carta has recently been updated to `v4`, which features numerous major changes.
|
Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
|
||||||
>
|
|
||||||
> Follow the [Migration Guide](http://beartocode.github.io/carta/migration) to update your project.
|
|
||||||
|
|
||||||
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer. It is powered by [unified](https://github.com/unifiedjs/unified), [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
|
|
||||||
Differently from most editors, Carta does not include a code editor, but it is _just_ a textarea with syntax highlighting, shortcuts and more.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
|
- Keyboard **shortcuts** (extensible);
|
||||||
- 🛠️ Toolbar (extensible);
|
- Toolbar (extensible);
|
||||||
- ⌨️ Keyboard **shortcuts** (extensible);
|
- Markdown syntax highlighting;
|
||||||
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark;
|
- Scroll sync;
|
||||||
- 🔀 Scroll sync;
|
- **SSR** compatible;
|
||||||
- ✅ Accessibility friendly;
|
- **Katex** support (plugin);
|
||||||
- 🖥️ **SSR** compatible;
|
- **Slash** commands (plugin);
|
||||||
- ⚗️ **KaTeX** support (plugin);
|
- **Emojis**, with included search (plugin);
|
||||||
- 🔨 **Slash** commands (plugin);
|
- **Tikz** support(plugin);
|
||||||
- 😄 **Emojis**, with included search (plugin);
|
- **Attachment** support(plugin);
|
||||||
- ✏️ **TikZ** support (plugin);
|
- Code blocks **syntax highlighting** (plugin).
|
||||||
- 📂 **Attachment** support (plugin);
|
|
||||||
- ⚓ **Anchor** links in headings (plugin);
|
|
||||||
- 🌈 Code blocks **syntax highlighting** (plugin).
|
|
||||||
|
|
||||||
## Packages
|
|
||||||
|
|
||||||
| Package | Status | Docs |
|
|
||||||
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
||||||
| [carta-md](https://www.npmjs.com/package/carta-md) |  | [/](https://beartocode.github.io/carta/introduction) |
|
|
||||||
| [plugin-math](https://www.npmjs.com/package/@cartamd/plugin-math) |  | [/plugins/math](https://beartocode.github.io/carta/plugins/math) |
|
|
||||||
| [plugin-code](https://www.npmjs.com/package/@cartamd/plugin-code) |  | [/plugins/code](https://beartocode.github.io/carta/plugins/code) |
|
|
||||||
| [plugin-emoji](https://www.npmjs.com/package/@cartamd/plugin-emoji) |  | [/plugins/emoji](https://beartocode.github.io/carta/plugins/emoji) |
|
|
||||||
| [plugin-slash](https://www.npmjs.com/package/@cartamd/plugin-slash) |  | [/plugins/slash](https://beartocode.github.io/carta/plugins/slash) |
|
|
||||||
| [plugin-tikz](https://www.npmjs.com/package/@cartamd/plugin-tikz) |  | [/plugins/tikz](https://beartocode.github.io/carta/plugins/tikz) |
|
|
||||||
| [plugin-attachment](https://www.npmjs.com/package/@cartamd/plugin-attachment) |  | [/plugins/attachment](https://beartocode.github.io/carta/plugins/attachment) |
|
|
||||||
| [plugin-anchor](https://www.npmjs.com/package/@cartamd/plugin-anchor) |  | [/plugins/anchor](https://beartocode.github.io/carta/plugins/anchor) |
|
|
||||||
|
|
||||||
## Community plugins
|
|
||||||
|
|
||||||
| Plugin | Description |
|
|
||||||
| ----------------------------------------------------------------------------- | ---------------------------------- |
|
|
||||||
| [carta-plugin-video](https://github.com/maisonsmd/carta-plugin-video) | Render online videos |
|
|
||||||
| [carta-plugin-imsize](https://github.com/maisonsmd/carta-plugin-imsize) | Render images in specific sizes |
|
|
||||||
| [carta-plugin-subscript](https://github.com/maisonsmd/carta-plugin-subscript) | Render subscripts and superscripts |
|
|
||||||
| [carta-plugin-ins-del](https://github.com/maisonsmd/carta-plugin-ins-del) | `<ins>` and `<del>` tags support |
|
|
||||||
|
|
||||||
# Getting started
|
# Getting started
|
||||||
|
|
||||||
> **WARNING**
|
> **Warning**
|
||||||
> Sanitization is not dealt with by Carta. You need to provide a `sanitizer` in the options.
|
> Sanitization is not dealt with by Carta. You need to provide a `sanitizer` in the options.
|
||||||
> Common sanitizers are [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) (suggested) and [sanitize-html](https://www.npmjs.com/package/sanitize-html).
|
> Common sanitizers are [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) (suggested) and [sanitize-html](https://www.npmjs.com/package/sanitize-html).
|
||||||
> Checkout the documentation for an example.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Core package:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm i carta-md
|
|
||||||
```
|
|
||||||
|
|
||||||
Plugins:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm i @cartamd/plugin-name
|
|
||||||
```
|
|
||||||
|
|
||||||
## Basic configuration
|
## Basic configuration
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
// Component default theme
|
// Component default theme
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
|
// Markdown input theme (Speed Highlight)
|
||||||
|
import 'carta-md/light.css';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
// Remember to use a sanitizer to prevent XSS attacks
|
// Remember to use a sanitizer to prevent XSS attacks
|
||||||
|
@ -110,14 +73,13 @@ npm i @cartamd/plugin-name
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Or in global stylesheet */
|
/* Or in global stylesheet */
|
||||||
/* Set your custom monospace font */
|
/* Set your custom monospace font */
|
||||||
:global(.carta-font-code) {
|
:global(.carta-font-code) {
|
||||||
font-family: '...', monospace;
|
font-family: '...', monospace;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
```
|
```
|
||||||
|
@ -137,33 +99,7 @@ For the full documentation, examples, guides and more checkout the [website](htt
|
||||||
- [Slash](https://beartocode.github.io/carta/plugins/slash)
|
- [Slash](https://beartocode.github.io/carta/plugins/slash)
|
||||||
- [TikZ](https://beartocode.github.io/carta/plugins/tikz)
|
- [TikZ](https://beartocode.github.io/carta/plugins/tikz)
|
||||||
- [Attachment](https://beartocode.github.io/carta/plugins/attachment)
|
- [Attachment](https://beartocode.github.io/carta/plugins/attachment)
|
||||||
- [Anchor](https://beartocode.github.io/carta/plugins/anchor)
|
|
||||||
- API:
|
- API:
|
||||||
- [Utilities](https://beartocode.github.io/carta/api/utilities)
|
- [Utilities](https://beartocode.github.io/carta/api/utilities)
|
||||||
- [Core](https://beartocode.github.io/carta/api/core)
|
- [Core](https://beartocode.github.io/carta/api/core)
|
||||||
- [Extension](https://beartocode.github.io/carta/api/extension)
|
- [Extension](https://beartocode.github.io/carta/api/extension)
|
||||||
|
|
||||||
# Contributing & Development
|
|
||||||
|
|
||||||
Every contribution is well accepted. If you have a feature request you can open a new issue.
|
|
||||||
|
|
||||||
This package uses a [pnpm workspace](https://pnpm.io/workspaces), so pnpm is required to download and put everything together properly.
|
|
||||||
|
|
||||||
### Committing
|
|
||||||
|
|
||||||
This repository is [commitizen](https://github.com/commitizen/cz-cli) friendly. To commit use:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run commit
|
|
||||||
# or, if you have commitizen installed globally
|
|
||||||
git cz
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running docs
|
|
||||||
|
|
||||||
If you want to preview the docs:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd docs
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
|
@ -14,7 +14,10 @@
|
||||||
"svelte": "./dist/index.js",
|
"svelte": "./dist/index.js",
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./default.css": "./dist/default.css"
|
"./default.css": "./dist/default.css",
|
||||||
|
"./default-theme.css": "./dist/default.css",
|
||||||
|
"./light.css": "./dist/light.css",
|
||||||
|
"./dark.css": "./dist/dark.css"
|
||||||
},
|
},
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -35,12 +38,8 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rehype-stringify": "^10.0.0",
|
"@speed-highlight/core": "1.2.2",
|
||||||
"remark-gfm": "^4.0.0",
|
"marked": "^9.1.5"
|
||||||
"remark-parse": "^11.0.0",
|
|
||||||
"remark-rehype": "^11.1.0",
|
|
||||||
"shiki": "^1.4.0",
|
|
||||||
"unified": "^11.0.4"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Carta } from './internal/carta';
|
import type { Carta } from './internal/carta';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Renderer from './internal/components/Renderer.svelte';
|
import CartaRenderer from './internal/components/CartaRenderer.svelte';
|
||||||
import Input from './internal/components/Input.svelte';
|
import MarkdownInput from './internal/components/MarkdownInput.svelte';
|
||||||
import { debounce } from './internal/utils';
|
import { debounce } from './internal/utils';
|
||||||
import type { TextAreaProps } from './internal/textarea-props';
|
import type { TextAreaProps } from './internal/textarea-props';
|
||||||
import { defaultLabels, type Labels } from './internal/labels';
|
import { DefaultCartaLabels, type CartaLabels } from './internal/labels';
|
||||||
import Toolbar from './internal/components/Toolbar.svelte';
|
import Toolbar from './internal/components/Toolbar.svelte';
|
||||||
|
|
||||||
export let carta: Carta;
|
export let carta: Carta;
|
||||||
|
@ -17,10 +17,10 @@
|
||||||
export let placeholder = '';
|
export let placeholder = '';
|
||||||
export let textarea: TextAreaProps = {};
|
export let textarea: TextAreaProps = {};
|
||||||
|
|
||||||
let userLabels: Partial<Labels> = {};
|
let userLabels: Partial<CartaLabels> = {};
|
||||||
export { userLabels as labels };
|
export { userLabels as labels };
|
||||||
const labels: Labels = {
|
const labels: CartaLabels = {
|
||||||
...defaultLabels,
|
...DefaultCartaLabels,
|
||||||
...userLabels
|
...userLabels
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
<div class="carta-wrapper">
|
<div class="carta-wrapper">
|
||||||
<div class="carta-container mode-{windowMode}">
|
<div class="carta-container mode-{windowMode}">
|
||||||
{#if windowMode == 'split' || selectedTab == 'write'}
|
{#if windowMode == 'split' || selectedTab == 'write'}
|
||||||
<Input
|
<MarkdownInput
|
||||||
{carta}
|
{carta}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{handleScroll}
|
{handleScroll}
|
||||||
|
@ -121,10 +121,10 @@
|
||||||
<svelte:component this={component} {carta} {...props} />
|
<svelte:component this={component} {carta} {...props} />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</Input>
|
</MarkdownInput>
|
||||||
{/if}
|
{/if}
|
||||||
{#if windowMode == 'split' || selectedTab == 'preview'}
|
{#if windowMode == 'split' || selectedTab == 'preview'}
|
||||||
<Renderer {carta} {handleScroll} bind:value bind:elem={rendererElem}>
|
<CartaRenderer {carta} {handleScroll} bind:value bind:elem={rendererElem}>
|
||||||
<!-- Renderer extensions components -->
|
<!-- Renderer extensions components -->
|
||||||
{#if mounted}
|
{#if mounted}
|
||||||
{#each carta.components.filter(({ parent }) => [parent]
|
{#each carta.components.filter(({ parent }) => [parent]
|
||||||
|
@ -133,7 +133,7 @@
|
||||||
<svelte:component this={component} {carta} {...props} />
|
<svelte:component this={component} {carta} {...props} />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</Renderer>
|
</CartaRenderer>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { loadNestedLanguages, type Carta } from '.';
|
import type { Carta } from './';
|
||||||
|
|
||||||
export let carta: Carta;
|
export let carta: Carta;
|
||||||
export let value: string;
|
export let value: string;
|
||||||
|
@ -12,12 +12,7 @@
|
||||||
let rendered = carta.renderSSR(value);
|
let rendered = carta.renderSSR(value);
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
carta.$setRenderer(elem);
|
carta.$setRenderer(elem);
|
||||||
|
// Add code syntax highlighting (if plugin is present) once loaded on the client.
|
||||||
// Load highlighting languages
|
|
||||||
const highlighter = await carta.highlighter();
|
|
||||||
await loadNestedLanguages(highlighter, value);
|
|
||||||
|
|
||||||
// Render using asynchronous renderer
|
|
||||||
rendered = await carta.render(value);
|
rendered = await carta.render(value);
|
||||||
|
|
||||||
mounted = true;
|
mounted = true;
|
41
packages/carta-md/src/lib/dark.css
Normal file
41
packages/carta-md/src/lib/dark.css
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
@import 'light.css';
|
||||||
|
|
||||||
|
[class*='shj-lang-'] {
|
||||||
|
color: #f8f8f2;
|
||||||
|
background: #1a1a1c;
|
||||||
|
}
|
||||||
|
[class*='shj-lang-']:before {
|
||||||
|
color: #6f9aff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shj-syn-deleted,
|
||||||
|
.shj-syn-err,
|
||||||
|
.shj-syn-var {
|
||||||
|
color: #ff5261;
|
||||||
|
}
|
||||||
|
.shj-syn-section,
|
||||||
|
.shj-syn-kwd {
|
||||||
|
color: #ff7cc6;
|
||||||
|
}
|
||||||
|
.shj-syn-class {
|
||||||
|
color: #eab07c;
|
||||||
|
}
|
||||||
|
.shj-numbers,
|
||||||
|
.shj-syn-cmnt {
|
||||||
|
color: #7d828b;
|
||||||
|
}
|
||||||
|
.shj-syn-insert,
|
||||||
|
.shj-syn-type,
|
||||||
|
.shj-syn-func,
|
||||||
|
.shj-syn-bool {
|
||||||
|
color: #71d58a;
|
||||||
|
}
|
||||||
|
.shj-syn-num {
|
||||||
|
color: #b581fd;
|
||||||
|
}
|
||||||
|
.shj-syn-oper {
|
||||||
|
color: #80c6ff;
|
||||||
|
}
|
||||||
|
.shj-syn-str {
|
||||||
|
color: #4dacfa;
|
||||||
|
}
|
|
@ -3,15 +3,6 @@
|
||||||
--selection-color: #b5f0ff3d;
|
--selection-color: #b5f0ff3d;
|
||||||
--focus-outline: #76bbf3;
|
--focus-outline: #76bbf3;
|
||||||
--hover-color: #e9e9e9;
|
--hover-color: #e9e9e9;
|
||||||
--caret-color: #161616;
|
|
||||||
--text-color: #1a1a1a;
|
|
||||||
|
|
||||||
--border-color-dark: #4d4d4c;
|
|
||||||
--selection-color-dark: #b5f0ff3d;
|
|
||||||
--focus-outline-dark: #76bbf3;
|
|
||||||
--hover-color-dark: #4d4d4c;
|
|
||||||
--caret-color-dark: #ffffff;
|
|
||||||
--text-color-dark: #f1f1f1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-theme__default.carta-editor {
|
.carta-theme__default.carta-editor {
|
||||||
|
@ -38,14 +29,10 @@
|
||||||
|
|
||||||
/* Text settings */
|
/* Text settings */
|
||||||
.carta-theme__default .carta-input {
|
.carta-theme__default .carta-input {
|
||||||
caret-color: var(--caret-color);
|
caret-color: #4d4d4c;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-theme__default .carta-input ::placeholder {
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Splitter */
|
/* Splitter */
|
||||||
.carta-theme__default .mode-split.carta-container::after {
|
.carta-theme__default .mode-split.carta-container::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -75,10 +62,6 @@
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-theme__default button {
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Markdown input and renderer */
|
/* Markdown input and renderer */
|
||||||
.carta-theme__default .carta-input,
|
.carta-theme__default .carta-input,
|
||||||
.carta-theme__default .carta-renderer {
|
.carta-theme__default .carta-renderer {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export { default as MarkdownEditor } from '$lib/MarkdownEditor.svelte';
|
export { default as CartaEditor } from '$lib/CartaEditor.svelte';
|
||||||
export { default as Markdown } from '$lib/Markdown.svelte';
|
export { default as CartaViewer } from '$lib/CartaViewer.svelte';
|
||||||
export type { InputEnhancer, TextSelection } from '$lib/internal/input';
|
export type { CartaInput, TextSelection } from '$lib/internal/input';
|
||||||
export type { Icon } from '$lib/internal/icons';
|
export type { CartaIcon } from '$lib/internal/icons';
|
||||||
export type { KeyboardShortcut } from '$lib/internal/shortcuts';
|
export type { KeyboardShortcut } from '$lib/internal/shortcuts';
|
||||||
export type { Prefix } from '$lib/internal/prefixes';
|
export type { Prefix } from '$lib/internal/prefixes';
|
||||||
export * from '$lib/internal/carta';
|
export * from '$lib/internal/carta';
|
||||||
|
@ -9,7 +9,4 @@ export * from '$lib/internal/highlight';
|
||||||
export * from '$lib/internal/textarea-props';
|
export * from '$lib/internal/textarea-props';
|
||||||
export * from '$lib/internal/labels';
|
export * from '$lib/internal/labels';
|
||||||
export * from './default.css?inline';
|
export * from './default.css?inline';
|
||||||
|
export * from './light.css?inline';
|
||||||
// Legacy
|
|
||||||
export { default as CartaEditor } from '$lib/MarkdownEditor.svelte';
|
|
||||||
export { default as CartaViewer } from '$lib/Markdown.svelte';
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,330 +0,0 @@
|
||||||
import { type ThemeInput } from 'shiki';
|
|
||||||
|
|
||||||
const theme = {
|
|
||||||
displayName: 'Carta Dark' as const,
|
|
||||||
name: 'carta-dark' as const,
|
|
||||||
semanticHighlighting: true,
|
|
||||||
fg: '#f8f8f2',
|
|
||||||
bg: 'transparent',
|
|
||||||
tokenColors: [
|
|
||||||
{
|
|
||||||
scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#6a737d'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['variable.other.constant', 'variable.other.enummember', 'variable.language'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#fff'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['constant', 'entity.name.constant'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['entity', 'entity.name'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#b392f0'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'variable.parameter.function',
|
|
||||||
settings: {
|
|
||||||
foreground: '#e1e4e8'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'entity.name.tag',
|
|
||||||
settings: {
|
|
||||||
foreground: '#85e89d'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['keyword', 'punctuation.definition.template-expression'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#ff7cc6'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['storage', 'storage.type'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#ff7cc6'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['storage.modifier.package', 'storage.modifier.import', 'storage.type.java'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#e1e4e8'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: [
|
|
||||||
'string',
|
|
||||||
'punctuation.definition.string',
|
|
||||||
'string punctuation.section.embedded source'
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
foreground: '#4dacfa'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'support',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.property-name',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'variable',
|
|
||||||
settings: {
|
|
||||||
foreground: '#b581fd'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'variable.other',
|
|
||||||
settings: {
|
|
||||||
foreground: '#e1e4e8'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.broken',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#fdaeb7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.deprecated',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#fdaeb7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.illegal',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#fdaeb7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.unimplemented',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#fdaeb7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'carriage-return',
|
|
||||||
settings: {
|
|
||||||
background: '#ff7cc6',
|
|
||||||
fontStyle: 'italic underline',
|
|
||||||
foreground: '#24292e'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'message.error',
|
|
||||||
settings: {
|
|
||||||
foreground: '#fdaeb7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'string variable',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['source.regexp', 'string.regexp'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#4dacfa'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: [
|
|
||||||
'string.regexp.character-class',
|
|
||||||
'string.regexp constant.character.escape',
|
|
||||||
'string.regexp source.ruby.embedded',
|
|
||||||
'string.regexp string.regexp.arbitrary-repitition'
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
foreground: '#4dacfa'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'string.regexp constant.character.escape',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#85e89d'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'support.constant',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'support.variable',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.module-reference',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'punctuation.definition.list.begin.markdown',
|
|
||||||
settings: {
|
|
||||||
foreground: '#ff7cc6'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.heading', 'markup.heading entity.name'],
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#e8e8e8'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.quote',
|
|
||||||
settings: {
|
|
||||||
foreground: '#7d828b'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.italic',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#ff7cc6'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.bold',
|
|
||||||
settings: {
|
|
||||||
foreground: '#b581fd'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.underline'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a',
|
|
||||||
fontStyle: 'underline'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.strikethrough'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#ff5261',
|
|
||||||
fontStyle: 'strikethrough'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.inline.raw',
|
|
||||||
settings: {
|
|
||||||
foreground: '#4dacfa'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.deleted', 'meta.diff.header.from-file', 'punctuation.definition.deleted'],
|
|
||||||
settings: {
|
|
||||||
background: '#86181d',
|
|
||||||
foreground: '#fdaeb7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.inserted', 'meta.diff.header.to-file', 'punctuation.definition.inserted'],
|
|
||||||
settings: {
|
|
||||||
background: '#144620',
|
|
||||||
foreground: '#85e89d'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.changed', 'punctuation.definition.changed'],
|
|
||||||
settings: {
|
|
||||||
background: '#c24e00',
|
|
||||||
foreground: '#b581fd'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.ignored', 'markup.untracked'],
|
|
||||||
settings: {
|
|
||||||
background: '#71d58a',
|
|
||||||
foreground: '#2f363d'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.diff.range',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#b392f0'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.diff.header',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.separator',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.output',
|
|
||||||
settings: {
|
|
||||||
foreground: '#71d58a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: [
|
|
||||||
'brackethighlighter.tag',
|
|
||||||
'brackethighlighter.curly',
|
|
||||||
'brackethighlighter.round',
|
|
||||||
'brackethighlighter.square',
|
|
||||||
'brackethighlighter.angle',
|
|
||||||
'brackethighlighter.quote'
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
foreground: '#d1d5da'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'brackethighlighter.unmatched',
|
|
||||||
settings: {
|
|
||||||
foreground: '#fdaeb7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['constant.other.reference.link', 'string.other.link'],
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'underline',
|
|
||||||
foreground: '#4dacfa'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['punctuation.definition.markdown', 'fenced_code.block.language'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#ff7cc6'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
type: 'light'
|
|
||||||
} satisfies ThemeInput;
|
|
||||||
|
|
||||||
export default theme;
|
|
|
@ -1,329 +0,0 @@
|
||||||
import { type ThemeInput } from 'shiki';
|
|
||||||
|
|
||||||
const theme = {
|
|
||||||
displayName: 'Carta Light' as const,
|
|
||||||
name: 'carta-light' as const,
|
|
||||||
semanticHighlighting: true,
|
|
||||||
fg: '#333333',
|
|
||||||
bg: 'transparent',
|
|
||||||
tokenColors: [
|
|
||||||
{
|
|
||||||
scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#6a737d'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['variable.other.constant', 'variable.other.enummember', 'variable.language'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#000'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['constant', 'entity.name.constant'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['entity', 'entity.name'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#6f42c1'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'variable.parameter.function',
|
|
||||||
settings: {
|
|
||||||
foreground: '#24292e'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'entity.name.tag',
|
|
||||||
settings: {
|
|
||||||
foreground: '#22863a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['keyword', 'punctuation.definition.template-expression'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#e16'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['storage', 'storage.type'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#e16'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['storage.modifier.package', 'storage.modifier.import', 'storage.type.java'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#24292e'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: [
|
|
||||||
'string',
|
|
||||||
'punctuation.definition.string',
|
|
||||||
'string punctuation.section.embedded source'
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
foreground: '#7d8'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'support',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.property-name',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'variable',
|
|
||||||
settings: {
|
|
||||||
foreground: '#f60'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'variable.other',
|
|
||||||
settings: {
|
|
||||||
foreground: '#24292e'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.broken',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#b31d28'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.deprecated',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#b31d28'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.illegal',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#b31d28'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'invalid.unimplemented',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'italic',
|
|
||||||
foreground: '#b31d28'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'carriage-return',
|
|
||||||
settings: {
|
|
||||||
background: '#e16',
|
|
||||||
fontStyle: 'italic underline',
|
|
||||||
foreground: '#fafbfc'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'message.error',
|
|
||||||
settings: {
|
|
||||||
foreground: '#b31d28'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'string variable',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['source.regexp', 'string.regexp'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#7d8'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: [
|
|
||||||
'string.regexp.character-class',
|
|
||||||
'string.regexp constant.character.escape',
|
|
||||||
'string.regexp source.ruby.embedded',
|
|
||||||
'string.regexp string.regexp.arbitrary-repitition'
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
foreground: '#7d8'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'string.regexp constant.character.escape',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#22863a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'support.constant',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'support.variable',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.module-reference',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'punctuation.definition.list.begin.markdown',
|
|
||||||
settings: {
|
|
||||||
foreground: '#e16'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.heading', 'markup.heading entity.name'],
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#212121'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.quote',
|
|
||||||
settings: {
|
|
||||||
foreground: '#999'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.italic',
|
|
||||||
settings: {
|
|
||||||
foreground: '#e16'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.bold',
|
|
||||||
settings: {
|
|
||||||
foreground: '#f60'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.underline'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#84f',
|
|
||||||
fontStyle: 'underline'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.strikethrough'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#f44',
|
|
||||||
fontStyle: 'strikethrough'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'markup.inline.raw',
|
|
||||||
settings: {
|
|
||||||
foreground: '#5af'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.deleted', 'meta.diff.header.from-file', 'punctuation.definition.deleted'],
|
|
||||||
settings: {
|
|
||||||
background: '#ffeef0',
|
|
||||||
foreground: '#b31d28'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.inserted', 'meta.diff.header.to-file', 'punctuation.definition.inserted'],
|
|
||||||
settings: {
|
|
||||||
background: '#f0fff4',
|
|
||||||
foreground: '#22863a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.changed', 'punctuation.definition.changed'],
|
|
||||||
settings: {
|
|
||||||
background: '#ffebda',
|
|
||||||
foreground: '#f60'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['markup.ignored', 'markup.untracked'],
|
|
||||||
settings: {
|
|
||||||
background: '#3bf',
|
|
||||||
foreground: '#f6f8fa'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.diff.range',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#6f42c1'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.diff.header',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.separator',
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'bold',
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'meta.output',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: [
|
|
||||||
'brackethighlighter.tag',
|
|
||||||
'brackethighlighter.curly',
|
|
||||||
'brackethighlighter.round',
|
|
||||||
'brackethighlighter.square',
|
|
||||||
'brackethighlighter.angle',
|
|
||||||
'brackethighlighter.quote'
|
|
||||||
],
|
|
||||||
settings: {
|
|
||||||
foreground: '#586069'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: 'brackethighlighter.unmatched',
|
|
||||||
settings: {
|
|
||||||
foreground: '#b31d28'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['constant.other.reference.link', 'string.other.link'],
|
|
||||||
settings: {
|
|
||||||
fontStyle: 'underline',
|
|
||||||
foreground: '#5af'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scope: ['punctuation.definition.markdown', 'fenced_code.block.language'],
|
|
||||||
settings: {
|
|
||||||
foreground: '#e16'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
type: 'light'
|
|
||||||
} satisfies ThemeInput;
|
|
||||||
|
|
||||||
export default theme;
|
|
|
@ -1,61 +1,53 @@
|
||||||
import type { SvelteComponent } from 'svelte';
|
import type { CartaHistoryOptions } from './history';
|
||||||
import { unified, type Processor } from 'unified';
|
import type { SvelteComponentTyped } from 'svelte';
|
||||||
import remarkParse from 'remark-parse';
|
import type { ShjLanguageDefinition } from '@speed-highlight/core/index';
|
||||||
import remarkGfm, { type Options as GfmOptions } from 'remark-gfm';
|
import { Marked, type MarkedExtension } from 'marked';
|
||||||
import remarkRehype from 'remark-rehype';
|
import { CartaInput } from './input';
|
||||||
import rehypeStringify from 'rehype-stringify';
|
|
||||||
import type { TextAreaHistoryOptions } from './history';
|
|
||||||
import { InputEnhancer } from './input';
|
|
||||||
import {
|
import {
|
||||||
type DefaultShortcutId,
|
type DefaultShortcutId,
|
||||||
type KeyboardShortcut,
|
type KeyboardShortcut,
|
||||||
defaultKeyboardShortcuts
|
defaultKeyboardShortcuts
|
||||||
} from './shortcuts';
|
} from './shortcuts';
|
||||||
import { defaultIcons, type Icon, type DefaultIconId } from './icons';
|
import { defaultIcons, type CartaIcon, type DefaultIconId } from './icons';
|
||||||
import { defaultPrefixes, type DefaultPrefixId, type Prefix } from './prefixes';
|
import { defaultPrefixes, type DefaultPrefixId, type Prefix } from './prefixes';
|
||||||
import { Renderer } from './renderer';
|
import { CartaRenderer } from './renderer';
|
||||||
import { CustomEvent, type MaybeArray } from './utils';
|
|
||||||
import {
|
import {
|
||||||
loadHighlighter,
|
type HighlightFunctions,
|
||||||
loadDefaultTheme,
|
loadCustomLanguage,
|
||||||
type Highlighter,
|
highlight,
|
||||||
type GrammarRule,
|
highlightAutodetect,
|
||||||
type ShikiOptions,
|
loadCustomMarkdown
|
||||||
type DualTheme,
|
} from './highlight.js';
|
||||||
type Theme,
|
import { CustomEvent } from './utils';
|
||||||
type HighlightingRule
|
|
||||||
} from './highlight';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta-specific event with extra payload.
|
* Carta-specific event with extra payload.
|
||||||
*/
|
*/
|
||||||
export type Event = CustomEvent<{ carta: Carta }>;
|
export type CartaEvent = CustomEvent<{ carta: Carta }>;
|
||||||
const cartaEvents = ['carta-render', 'carta-render-ssr'] as const;
|
const cartaEvents = ['carta-render', 'carta-render-ssr'] as const;
|
||||||
type CartaEventType = (typeof cartaEvents)[number];
|
type CartaEventType = (typeof cartaEvents)[number];
|
||||||
|
|
||||||
/**
|
export type CartaListener<K extends CartaEventType | keyof HTMLElementEventMap> = [
|
||||||
* Custom listeners for the textarea element.
|
|
||||||
*/
|
|
||||||
export type Listener<K extends CartaEventType | keyof HTMLElementEventMap> = [
|
|
||||||
type: K,
|
type: K,
|
||||||
listener: (
|
listener: (
|
||||||
this: HTMLTextAreaElement,
|
this: HTMLTextAreaElement,
|
||||||
ev: K extends CartaEventType
|
ev: K extends CartaEventType
|
||||||
? Event
|
? CartaEvent
|
||||||
: K extends keyof HTMLElementEventMap
|
: K extends keyof HTMLElementEventMap
|
||||||
? HTMLElementEventMap[K]
|
? HTMLElementEventMap[K]
|
||||||
: Event
|
: Event
|
||||||
) => unknown,
|
) => unknown,
|
||||||
options?: boolean | AddEventListenerOptions
|
options?: boolean | AddEventListenerOptions
|
||||||
];
|
];
|
||||||
/**
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
* Custom Svelte component for extensions.
|
type CartaListeners = CartaListener<any>[];
|
||||||
*/
|
|
||||||
export interface ExtensionComponent<T extends object | undefined> {
|
type MaybeArray<T> = T | Array<T>;
|
||||||
|
|
||||||
|
export interface CartaExtensionComponent<T extends object> {
|
||||||
/**
|
/**
|
||||||
* Svelte components that exports `carta: Carta` and all the other properties specified in `props`.
|
* Svelte components that exports `carta: Carta` and all the other properties specified in `props`.
|
||||||
*/
|
*/
|
||||||
component: typeof SvelteComponent<T & { carta: Carta }>;
|
component: typeof SvelteComponentTyped<T & { carta: Carta }>;
|
||||||
/**
|
/**
|
||||||
* Properties that will be handed to the component.
|
* Properties that will be handed to the component.
|
||||||
*/
|
*/
|
||||||
|
@ -67,22 +59,16 @@ export interface ExtensionComponent<T extends object | undefined> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type Listeners = Listener<any>[];
|
type CartaExtensionComponents = Array<CartaExtensionComponent<any>>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
type ExtensionComponents = Array<ExtensionComponent<any>>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta editor options.
|
* Carta editor options.
|
||||||
*/
|
*/
|
||||||
export interface Options {
|
export interface CartaOptions {
|
||||||
/**
|
|
||||||
* GitHub Flavored Markdown options.
|
|
||||||
*/
|
|
||||||
gfmOptions?: GfmOptions;
|
|
||||||
/**
|
/**
|
||||||
* Editor/viewer extensions.
|
* Editor/viewer extensions.
|
||||||
*/
|
*/
|
||||||
extensions?: Plugin[];
|
extensions?: CartaExtension[];
|
||||||
/**
|
/**
|
||||||
* Renderer debouncing timeout, in ms.
|
* Renderer debouncing timeout, in ms.
|
||||||
* @defaults 300ms
|
* @defaults 300ms
|
||||||
|
@ -103,47 +89,21 @@ export interface Options {
|
||||||
/**
|
/**
|
||||||
* History (Undo/Redo) options.
|
* History (Undo/Redo) options.
|
||||||
*/
|
*/
|
||||||
historyOptions?: TextAreaHistoryOptions;
|
historyOptions?: Partial<CartaHistoryOptions>;
|
||||||
/**
|
/**
|
||||||
* HTML sanitizer.
|
* HTML sanitizer.
|
||||||
*/
|
*/
|
||||||
sanitizer: ((html: string) => string) | false;
|
sanitizer?: (html: string) => string;
|
||||||
/**
|
|
||||||
* Highlighter options.
|
|
||||||
*/
|
|
||||||
shikiOptions?: ShikiOptions;
|
|
||||||
/**
|
|
||||||
* ShikiJS theme
|
|
||||||
* @default 'carta-light' for light mode and 'carta-dark' for dark mode.
|
|
||||||
*/
|
|
||||||
theme?: Theme | DualTheme;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified transformers plugins.
|
|
||||||
*/
|
|
||||||
export type UnifiedTransformer<E extends 'sync' | 'async'> = {
|
|
||||||
execution: 'sync' | 'async';
|
|
||||||
type: 'remark' | 'rehype';
|
|
||||||
transform: ({
|
|
||||||
processor,
|
|
||||||
carta
|
|
||||||
}: {
|
|
||||||
processor: Processor;
|
|
||||||
carta: Carta;
|
|
||||||
}) => E extends 'sync' ? void : Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta editor extensions.
|
* Carta editor extensions.
|
||||||
*/
|
*/
|
||||||
export interface Plugin {
|
export interface CartaExtension {
|
||||||
/**
|
/**
|
||||||
* Unified transformers plugins.
|
* Marked extensions, more on that [here](https://marked.js.org/using_advanced).
|
||||||
* @important If the plugin is async, it will not run in SSR rendering.
|
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
markedExtensions?: MarkedExtension[];
|
||||||
transformers?: UnifiedTransformer<'sync' | 'async'>[];
|
|
||||||
/**
|
/**
|
||||||
* Additional keyboard shortcuts.
|
* Additional keyboard shortcuts.
|
||||||
*/
|
*/
|
||||||
|
@ -151,7 +111,7 @@ export interface Plugin {
|
||||||
/**
|
/**
|
||||||
* Additional icons.
|
* Additional icons.
|
||||||
*/
|
*/
|
||||||
icons?: Icon[];
|
icons?: CartaIcon[];
|
||||||
/**
|
/**
|
||||||
* Additional prefixes.
|
* Additional prefixes.
|
||||||
*/
|
*/
|
||||||
|
@ -159,78 +119,58 @@ export interface Plugin {
|
||||||
/**
|
/**
|
||||||
* Textarea event listeners.
|
* Textarea event listeners.
|
||||||
*/
|
*/
|
||||||
listeners?: Listeners;
|
listeners?: CartaListeners;
|
||||||
/**
|
/**
|
||||||
* Additional components, that will be put after the editor.
|
* Additional components, that will be put after the editor.
|
||||||
* All components are given a `carta: Carta` prop.
|
* All components are given a `carta: Carta` prop.
|
||||||
* The editor has a `relative` position, so you can position
|
* The editor has a `relative` position, so you can position
|
||||||
* elements absolutely.
|
* elements absolutely.
|
||||||
*/
|
*/
|
||||||
components?: ExtensionComponents;
|
components?: CartaExtensionComponents;
|
||||||
/**
|
/**
|
||||||
* Custom markdown grammar highlight rules for ShiKi.
|
* Custom markdown highlight rules. See [Speed-Highlight Wiki](https://github.com/speed-highlight/core/wiki/Create-or-suggest-new-languages).
|
||||||
*/
|
*/
|
||||||
grammarRules?: GrammarRule[];
|
highlightRules?: ShjLanguageDefinition;
|
||||||
/**
|
|
||||||
* Custom markdown highlighting rules for ShiKi.
|
|
||||||
*/
|
|
||||||
highlightingRules?: HighlightingRule[];
|
|
||||||
/**
|
/**
|
||||||
* Use this callback to execute code when one Carta instance loads the extension.
|
* Use this callback to execute code when one Carta instance loads the extension.
|
||||||
* @param data General Carta related data.
|
* @param data General Carta related data.
|
||||||
*/
|
*/
|
||||||
onLoad?: (data: { carta: Carta }) => void;
|
onLoad?: (data: { carta: Carta; highlight: HighlightFunctions }) => void;
|
||||||
|
/**
|
||||||
|
* This function can be used to access a reference to the `Carta` class immediately after initialization.
|
||||||
|
* @deprecated Use `onLoad` instead.
|
||||||
|
*/
|
||||||
|
cartaRef?: (carta: Carta) => void;
|
||||||
|
/**
|
||||||
|
* This function can be used to access a reference to all highlight functions immediately after initialization.
|
||||||
|
* @deprecated Use `onLoad` instead.
|
||||||
|
*/
|
||||||
|
shjRef?: (functions: HighlightFunctions) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Carta {
|
export class Carta {
|
||||||
public readonly sanitizer?: (html: string) => string;
|
|
||||||
public readonly historyOptions?: TextAreaHistoryOptions;
|
|
||||||
public readonly theme?: Theme | DualTheme;
|
|
||||||
public readonly shikiOptions?: ShikiOptions;
|
|
||||||
public readonly rendererDebounce: number;
|
|
||||||
public readonly keyboardShortcuts: KeyboardShortcut[];
|
public readonly keyboardShortcuts: KeyboardShortcut[];
|
||||||
public readonly icons: Icon[];
|
public readonly icons: CartaIcon[];
|
||||||
public readonly prefixes: Prefix[];
|
public readonly prefixes: Prefix[];
|
||||||
public readonly grammarRules: GrammarRule[];
|
public readonly highlightRules: ShjLanguageDefinition;
|
||||||
public readonly highlightingRules: HighlightingRule[];
|
public readonly textareaListeners: CartaListeners;
|
||||||
public readonly textareaListeners: Listeners;
|
public readonly cartaListeners: CartaListeners;
|
||||||
public readonly cartaListeners: Listeners;
|
public readonly components: CartaExtensionComponents;
|
||||||
public readonly components: ExtensionComponents;
|
|
||||||
public readonly dispatcher = new EventTarget();
|
public readonly dispatcher = new EventTarget();
|
||||||
public readonly syncProcessor: Processor;
|
public readonly markedAsync = new Marked();
|
||||||
public readonly asyncProcessor: Promise<Processor>;
|
public readonly markedSync = new Marked();
|
||||||
|
|
||||||
private mElement: HTMLDivElement | undefined;
|
|
||||||
private mInput: InputEnhancer | undefined;
|
|
||||||
private mRenderer: Renderer | undefined;
|
|
||||||
private mHighlighter: Highlighter | Promise<Highlighter> | undefined;
|
|
||||||
private mSyncTransformers: UnifiedTransformer<'sync'>[] = [];
|
|
||||||
private mAsyncTransformers: UnifiedTransformer<'async'>[] = [];
|
|
||||||
|
|
||||||
|
private _element: HTMLDivElement | undefined;
|
||||||
|
private _input: CartaInput | undefined;
|
||||||
|
private _renderer: CartaRenderer | undefined;
|
||||||
public get element() {
|
public get element() {
|
||||||
return this.mElement;
|
return this._element;
|
||||||
}
|
}
|
||||||
public get input() {
|
public get input() {
|
||||||
return this.mInput;
|
return this._input;
|
||||||
}
|
}
|
||||||
public get renderer() {
|
public get renderer() {
|
||||||
return this.mRenderer;
|
return this._renderer;
|
||||||
}
|
|
||||||
|
|
||||||
public async highlighter(): Promise<Highlighter> {
|
|
||||||
if (!this.mHighlighter) {
|
|
||||||
const promise = async () => {
|
|
||||||
return loadHighlighter({
|
|
||||||
theme: this.theme ?? (await loadDefaultTheme()),
|
|
||||||
grammarRules: this.grammarRules,
|
|
||||||
highlightingRules: this.highlightingRules,
|
|
||||||
shiki: this.shikiOptions
|
|
||||||
});
|
|
||||||
};
|
|
||||||
this.mHighlighter = promise();
|
|
||||||
this.mHighlighter = await this.mHighlighter;
|
|
||||||
}
|
|
||||||
return this.mHighlighter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private elementsToBind: {
|
private elementsToBind: {
|
||||||
|
@ -239,31 +179,23 @@ export class Carta {
|
||||||
callback: (() => void) | undefined;
|
callback: (() => void) | undefined;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
public constructor(options?: Options) {
|
public constructor(public readonly options?: CartaOptions) {
|
||||||
this.sanitizer = options?.sanitizer || undefined;
|
|
||||||
this.historyOptions = options?.historyOptions;
|
|
||||||
this.theme = options?.theme;
|
|
||||||
this.shikiOptions = options?.shikiOptions;
|
|
||||||
this.rendererDebounce = options?.rendererDebounce ?? 300;
|
|
||||||
|
|
||||||
// Load plugins
|
|
||||||
this.keyboardShortcuts = [];
|
this.keyboardShortcuts = [];
|
||||||
this.icons = [];
|
this.icons = [];
|
||||||
this.prefixes = [];
|
this.prefixes = [];
|
||||||
this.textareaListeners = [];
|
this.textareaListeners = [];
|
||||||
this.cartaListeners = [];
|
this.cartaListeners = [];
|
||||||
this.components = [];
|
this.components = [];
|
||||||
this.grammarRules = [];
|
this.highlightRules = [];
|
||||||
this.highlightingRules = [];
|
|
||||||
|
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
|
|
||||||
for (const ext of options?.extensions ?? []) {
|
for (const ext of options?.extensions ?? []) {
|
||||||
this.keyboardShortcuts.push(...(ext.shortcuts ?? []));
|
this.keyboardShortcuts.push(...(ext.shortcuts ?? []));
|
||||||
this.icons.push(...(ext.icons ?? []));
|
this.icons.push(...(ext.icons ?? []));
|
||||||
this.prefixes.push(...(ext.prefixes ?? []));
|
this.prefixes.push(...(ext.prefixes ?? []));
|
||||||
this.components.push(...(ext.components ?? []));
|
this.components.push(...(ext.components ?? []));
|
||||||
this.grammarRules.push(...(ext.grammarRules ?? []));
|
this.highlightRules.push(...(ext.highlightRules ?? []));
|
||||||
this.highlightingRules.push(...(ext.highlightingRules ?? []));
|
|
||||||
|
|
||||||
listeners.push(...(ext.listeners ?? []));
|
listeners.push(...(ext.listeners ?? []));
|
||||||
}
|
}
|
||||||
|
@ -300,82 +232,41 @@ export class Carta {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load unified extensions
|
// Load marked extensions
|
||||||
this.mSyncTransformers = [];
|
const markedExtensions = this.options?.extensions
|
||||||
this.mAsyncTransformers = [];
|
?.flatMap((ext) => ext.markedExtensions)
|
||||||
|
.filter((ext) => ext != null) as MarkedExtension[] | undefined;
|
||||||
|
if (markedExtensions)
|
||||||
|
markedExtensions.forEach((ext) => {
|
||||||
|
this.useMarkedExtension(ext);
|
||||||
|
});
|
||||||
|
|
||||||
for (const ext of options?.extensions ?? []) {
|
// Load highlight custom language
|
||||||
for (const transformer of ext.transformers ?? []) {
|
loadCustomMarkdown(this.options?.extensions);
|
||||||
if (transformer.execution === 'sync') {
|
|
||||||
this.mSyncTransformers.push(transformer);
|
|
||||||
} else {
|
|
||||||
this.mAsyncTransformers.push(transformer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncProcessor = this.setupSynchronousProcessor({ gfmOptions: options?.gfmOptions });
|
for (const ext of this.options?.extensions ?? []) {
|
||||||
this.asyncProcessor = this.setupAsynchronousProcessor({ gfmOptions: options?.gfmOptions });
|
ext.cartaRef && ext.cartaRef(this);
|
||||||
|
ext.shjRef &&
|
||||||
for (const ext of options?.extensions ?? []) {
|
ext.shjRef({
|
||||||
if (ext.onLoad) {
|
highlight,
|
||||||
ext.onLoad({
|
highlightAutodetect,
|
||||||
carta: this
|
loadCustomLanguage
|
||||||
|
});
|
||||||
|
ext.onLoad &&
|
||||||
|
ext.onLoad({
|
||||||
|
carta: this,
|
||||||
|
highlight: {
|
||||||
|
highlight,
|
||||||
|
highlightAutodetect,
|
||||||
|
loadCustomLanguage
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupSynchronousProcessor({ gfmOptions }: { gfmOptions?: GfmOptions }) {
|
private useMarkedExtension(exts: MarkedExtension) {
|
||||||
const syncProcessor = unified();
|
this.markedAsync.use(exts);
|
||||||
|
if (!exts.async) this.markedSync.use(exts);
|
||||||
const remarkPlugins = this.mSyncTransformers.filter((it) => it.type === 'remark');
|
|
||||||
const rehypePlugins = this.mSyncTransformers.filter((it) => it.type === 'rehype');
|
|
||||||
|
|
||||||
syncProcessor.use(remarkParse);
|
|
||||||
syncProcessor.use(remarkGfm, gfmOptions);
|
|
||||||
|
|
||||||
for (const plugin of remarkPlugins) {
|
|
||||||
plugin.transform({ processor: syncProcessor, carta: this });
|
|
||||||
}
|
|
||||||
|
|
||||||
syncProcessor.use(remarkRehype);
|
|
||||||
|
|
||||||
for (const plugin of rehypePlugins) {
|
|
||||||
plugin.transform({ processor: syncProcessor, carta: this });
|
|
||||||
}
|
|
||||||
|
|
||||||
syncProcessor.use(rehypeStringify);
|
|
||||||
|
|
||||||
return syncProcessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setupAsynchronousProcessor({ gfmOptions }: { gfmOptions?: GfmOptions }) {
|
|
||||||
const asyncProcessor = unified();
|
|
||||||
|
|
||||||
const remarkPlugins = [...this.mSyncTransformers, ...this.mAsyncTransformers].filter(
|
|
||||||
(it) => it.type === 'remark'
|
|
||||||
);
|
|
||||||
const rehypePlugins = [...this.mSyncTransformers, ...this.mAsyncTransformers].filter(
|
|
||||||
(it) => it.type === 'rehype'
|
|
||||||
);
|
|
||||||
|
|
||||||
asyncProcessor.use(remarkParse);
|
|
||||||
asyncProcessor.use(remarkGfm, gfmOptions);
|
|
||||||
|
|
||||||
for (const plugin of remarkPlugins) {
|
|
||||||
await plugin.transform({ processor: asyncProcessor, carta: this });
|
|
||||||
}
|
|
||||||
|
|
||||||
asyncProcessor.use(remarkRehype);
|
|
||||||
|
|
||||||
for (const plugin of rehypePlugins) {
|
|
||||||
await plugin.transform({ processor: asyncProcessor, carta: this });
|
|
||||||
}
|
|
||||||
|
|
||||||
asyncProcessor.use(rehypeStringify);
|
|
||||||
|
|
||||||
return asyncProcessor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -384,13 +275,12 @@ export class Carta {
|
||||||
* @returns Rendered html.
|
* @returns Rendered html.
|
||||||
*/
|
*/
|
||||||
public async render(markdown: string): Promise<string> {
|
public async render(markdown: string): Promise<string> {
|
||||||
const processor = await this.asyncProcessor;
|
const dirty = await this.markedAsync.parse(markdown, { async: true });
|
||||||
const dirty = String(await processor.process(markdown));
|
|
||||||
if (!dirty) return '';
|
if (!dirty) return '';
|
||||||
this.dispatcher.dispatchEvent(
|
this.dispatcher.dispatchEvent(
|
||||||
new CustomEvent<{ carta: Carta }>('carta-render', { detail: { carta: this } })
|
new CustomEvent<{ carta: Carta }>('carta-render', { detail: { carta: this } })
|
||||||
);
|
);
|
||||||
return (this.sanitizer && this.sanitizer(dirty)) ?? dirty;
|
return (this.options?.sanitizer && this.options?.sanitizer(dirty)) ?? dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -399,12 +289,12 @@ export class Carta {
|
||||||
* @returns Rendered html.
|
* @returns Rendered html.
|
||||||
*/
|
*/
|
||||||
public renderSSR(markdown: string): string {
|
public renderSSR(markdown: string): string {
|
||||||
const dirty = String(this.syncProcessor.processSync(markdown));
|
const dirty = this.markedSync.parse(markdown, { async: false });
|
||||||
if (typeof dirty != 'string') return '';
|
if (typeof dirty != 'string') return '';
|
||||||
this.dispatcher.dispatchEvent(
|
this.dispatcher.dispatchEvent(
|
||||||
new CustomEvent<{ carta: Carta }>('carta-render-ssr', { detail: { carta: this } })
|
new CustomEvent<{ carta: Carta }>('carta-render-ssr', { detail: { carta: this } })
|
||||||
);
|
);
|
||||||
if (this.sanitizer) return this.sanitizer(dirty);
|
if (this.options?.sanitizer) return this.options.sanitizer(dirty);
|
||||||
return dirty;
|
return dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,7 +303,7 @@ export class Carta {
|
||||||
* @param element The editor element.
|
* @param element The editor element.
|
||||||
*/
|
*/
|
||||||
public $setElement(element: HTMLDivElement) {
|
public $setElement(element: HTMLDivElement) {
|
||||||
this.mElement = element;
|
this._element = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -425,19 +315,19 @@ export class Carta {
|
||||||
// Remove old listeners if any
|
// Remove old listeners if any
|
||||||
const previousInput = this.input;
|
const previousInput = this.input;
|
||||||
|
|
||||||
this.mInput = new InputEnhancer(textarea, container, {
|
this._input = new CartaInput(textarea, container, {
|
||||||
shortcuts: this.keyboardShortcuts,
|
shortcuts: this.keyboardShortcuts,
|
||||||
prefixes: this.prefixes,
|
prefixes: this.prefixes,
|
||||||
listeners: this.textareaListeners,
|
listeners: this.textareaListeners,
|
||||||
historyOpts: this.historyOptions
|
historyOpts: this.options?.historyOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
if (previousInput) {
|
if (previousInput) {
|
||||||
previousInput.events.removeEventListener('update', callback);
|
previousInput.events.removeEventListener('update', callback);
|
||||||
this.mInput.history = previousInput.history;
|
this._input.history = previousInput.history;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mInput.events.addEventListener('update', callback);
|
this._input.events.addEventListener('update', callback);
|
||||||
|
|
||||||
// Bind elements
|
// Bind elements
|
||||||
this.elementsToBind.forEach((it) => {
|
this.elementsToBind.forEach((it) => {
|
||||||
|
@ -453,7 +343,7 @@ export class Carta {
|
||||||
* @param container Div container of the rendered element.
|
* @param container Div container of the rendered element.
|
||||||
*/
|
*/
|
||||||
public $setRenderer(container: HTMLDivElement) {
|
public $setRenderer(container: HTMLDivElement) {
|
||||||
this.mRenderer = new Renderer(container);
|
this._renderer = new CartaRenderer(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -490,4 +380,14 @@ export class Carta {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight Markdown using Speed-Highlight and this Carta instance highlighting rules.
|
||||||
|
* @param text Text to highlight.
|
||||||
|
* @returns Highlighted html text.
|
||||||
|
*/
|
||||||
|
public async highlight(text: string) {
|
||||||
|
loadCustomMarkdown(this.options?.extensions);
|
||||||
|
return highlight(text, 'cartamd', true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
const debouncedRenderer = debounce(() => {
|
const debouncedRenderer = debounce(() => {
|
||||||
carta.render(value).then((rendered) => (renderedHtml = rendered));
|
carta.render(value).then((rendered) => (renderedHtml = rendered));
|
||||||
}, carta.rendererDebounce ?? 300);
|
}, carta.options?.rendererDebounce ?? 300);
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// On value updates
|
// On value updates
|
|
@ -2,8 +2,6 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Carta } from '../carta';
|
import type { Carta } from '../carta';
|
||||||
import type { TextAreaProps } from '../textarea-props';
|
import type { TextAreaProps } from '../textarea-props';
|
||||||
import { debounce } from '../utils';
|
|
||||||
import { isSingleTheme, loadNestedLanguages } from '../highlight';
|
|
||||||
|
|
||||||
export let carta: Carta;
|
export let carta: Carta;
|
||||||
export let value = '';
|
export let value = '';
|
||||||
|
@ -13,7 +11,7 @@
|
||||||
export let props: TextAreaProps = {};
|
export let props: TextAreaProps = {};
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
let textarea: HTMLTextAreaElement;
|
||||||
let highlighElem: HTMLDivElement;
|
let highlighElem: HTMLPreElement;
|
||||||
let highlighted = value;
|
let highlighted = value;
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
|
@ -38,44 +36,10 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const highlight = async (text: string) => {
|
const highlight = async (text: string) => (highlighted = (await carta.highlight(text)) as string);
|
||||||
const highlighter = await carta.highlighter();
|
|
||||||
let html: string;
|
|
||||||
|
|
||||||
if (isSingleTheme(highlighter.theme)) {
|
|
||||||
// Single theme
|
|
||||||
html = highlighter.codeToHtml(text, {
|
|
||||||
lang: highlighter.lang,
|
|
||||||
theme: highlighter.theme
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Dual theme
|
|
||||||
html = highlighter.codeToHtml(text, {
|
|
||||||
lang: highlighter.lang,
|
|
||||||
themes: highlighter.theme
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (carta.sanitizer) {
|
|
||||||
highlighted = carta.sanitizer(html);
|
|
||||||
} else {
|
|
||||||
highlighted = html;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightNestedLanguages = debounce(async (text: string) => {
|
|
||||||
const highlighter = await carta.highlighter();
|
|
||||||
const { updated } = await loadNestedLanguages(highlighter, text);
|
|
||||||
if (updated) highlight(text);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
$: highlight(value).then(resize);
|
$: highlight(value).then(resize);
|
||||||
$: highlightNestedLanguages(value);
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => (mounted = true));
|
||||||
mounted = true;
|
|
||||||
requestAnimationFrame(resize);
|
|
||||||
});
|
|
||||||
onMount(setInput);
|
onMount(setInput);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -92,14 +56,11 @@
|
||||||
bind:this={elem}
|
bind:this={elem}
|
||||||
>
|
>
|
||||||
<div class="carta-input-wrapper">
|
<div class="carta-input-wrapper">
|
||||||
<div
|
<pre
|
||||||
class="carta-highlight carta-font-code"
|
class="shj-lang-md carta-font-code"
|
||||||
tabindex="-1"
|
|
||||||
aria-hidden="true"
|
|
||||||
bind:this={highlighElem}
|
bind:this={highlighElem}
|
||||||
>
|
tabindex="-1"
|
||||||
<!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}
|
aria-hidden="true"><!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}</pre>
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
name="md"
|
name="md"
|
||||||
|
@ -149,11 +110,12 @@
|
||||||
color: transparent;
|
color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
|
font-size: inherit;
|
||||||
|
|
||||||
outline: none;
|
outline: none;
|
||||||
tab-size: 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-highlight {
|
pre {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -171,21 +133,6 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-highlight .shiki) {
|
|
||||||
margin: 0;
|
|
||||||
tab-size: 4;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.carta-highlight *) {
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor-unfocus-suggestion {
|
#editor-unfocus-suggestion {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Labels } from '../labels';
|
import type { CartaLabels } from '../labels';
|
||||||
import { handleArrowKeysNavigation } from '../accessibility';
|
import { handleArrowKeysNavigation } from '../accessibility';
|
||||||
import type { Carta } from '../carta';
|
import type { Carta } from '../carta';
|
||||||
import MenuIcon from './icons/MenuIcon.svelte';
|
import MenuIcon from './icons/MenuIcon.svelte';
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
export let carta: Carta;
|
export let carta: Carta;
|
||||||
export let mode: 'tabs' | 'split';
|
export let mode: 'tabs' | 'split';
|
||||||
export let tab: 'write' | 'preview';
|
export let tab: 'write' | 'preview';
|
||||||
export let labels: Labels;
|
export let labels: CartaLabels;
|
||||||
|
|
||||||
let toolbar: HTMLDivElement;
|
let toolbar: HTMLDivElement;
|
||||||
let menu: HTMLDivElement;
|
let menu: HTMLDivElement;
|
||||||
|
|
|
@ -1,268 +1,87 @@
|
||||||
|
import { detectLanguage } from '@speed-highlight/core/detect.js';
|
||||||
import {
|
import {
|
||||||
getHighlighter,
|
highlightText,
|
||||||
type BundledTheme,
|
loadLanguage,
|
||||||
type ThemeInput,
|
type ShjLanguage,
|
||||||
type StringLiteralUnion,
|
type ShjLanguageDefinition
|
||||||
type BundledLanguage,
|
} from '@speed-highlight/core';
|
||||||
type SpecialLanguage,
|
import type { CartaExtension } from './carta';
|
||||||
type LanguageInput,
|
import cartaMarkdown from './shj';
|
||||||
type LanguageRegistration,
|
|
||||||
type HighlighterGeneric,
|
|
||||||
bundledLanguages,
|
|
||||||
bundledThemes,
|
|
||||||
type ThemeRegistration
|
|
||||||
} from 'shiki';
|
|
||||||
import type { Intellisense } from './utils';
|
import type { Intellisense } from './utils';
|
||||||
|
|
||||||
/**
|
type Lang = Intellisense<ShjLanguage>;
|
||||||
* Custom TextMate grammar rule for the highlighter.
|
|
||||||
*/
|
|
||||||
export type GrammarRule = {
|
|
||||||
name: string;
|
|
||||||
type: 'block' | 'inline';
|
|
||||||
definition: LanguageRegistration['repository'][string];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom TextMate highlighting rule for the highlighter.
|
* Highlight text using Speed-Highlight. May return null on error(usually if requested
|
||||||
|
* language is not supported).
|
||||||
|
* @param text Text to highlight.
|
||||||
|
* @param lang Language to use, for example "js" or "c"
|
||||||
|
* @param hideLineNumbers Whether to hide line numbering.
|
||||||
|
* @returns Highlighted html text.
|
||||||
*/
|
*/
|
||||||
export type HighlightingRule = {
|
export async function highlight(
|
||||||
light: NonNullable<ThemeRegistration['tokenColors']>[number];
|
text: string,
|
||||||
dark: NonNullable<ThemeRegistration['tokenColors']>[number];
|
lang: Lang,
|
||||||
};
|
hideLineNumbers?: boolean
|
||||||
|
): Promise<string | null> {
|
||||||
/**
|
try {
|
||||||
* Shiki options for the highlighter.
|
return await highlightText(text, lang, true, { hideLineNumbers: hideLineNumbers ?? true });
|
||||||
*/
|
} catch (_) {
|
||||||
export type ShikiOptions = {
|
return null;
|
||||||
themes?: Array<ThemeInput | StringLiteralUnion<BundledTheme>>;
|
|
||||||
langs?: (LanguageInput | StringLiteralUnion<BundledLanguage> | SpecialLanguage)[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type CustomMarkdownLangName = Awaited<(typeof import('./assets/markdown'))['default']['name']>;
|
|
||||||
type DefaultLightThemeName = Awaited<(typeof import('./assets/theme-light'))['default']['name']>;
|
|
||||||
type DefaultDarkThemeName = Awaited<(typeof import('./assets/theme-dark'))['default']['name']>;
|
|
||||||
export const customMarkdownLangName: CustomMarkdownLangName = 'cartamd';
|
|
||||||
export const defaultLightThemeName: DefaultLightThemeName = 'carta-light';
|
|
||||||
export const defaultDarkThemeName: DefaultDarkThemeName = 'carta-dark';
|
|
||||||
export const loadDefaultTheme = async (): Promise<{
|
|
||||||
light: ThemeRegistration;
|
|
||||||
dark: ThemeRegistration;
|
|
||||||
}> => ({
|
|
||||||
light: structuredClone((await import('./assets/theme-light')).default),
|
|
||||||
dark: structuredClone((await import('./assets/theme-dark')).default)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Language for the highlighter.
|
|
||||||
*/
|
|
||||||
export type Language = Intellisense<BundledLanguage | CustomMarkdownLangName>;
|
|
||||||
/**
|
|
||||||
* Theme name for the highlighter.
|
|
||||||
*/
|
|
||||||
export type ThemeName = Intellisense<BundledTheme | DefaultLightThemeName | DefaultDarkThemeName>;
|
|
||||||
/**
|
|
||||||
* Theme for the highlighter.
|
|
||||||
*/
|
|
||||||
export type Theme = ThemeName | ThemeRegistration;
|
|
||||||
/**
|
|
||||||
* Dual theme for light and dark mode.
|
|
||||||
*/
|
|
||||||
export type DualTheme = {
|
|
||||||
light: Theme;
|
|
||||||
dark: Theme;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for the highlighter.
|
|
||||||
*/
|
|
||||||
export type HighlighterOptions = {
|
|
||||||
grammarRules: GrammarRule[];
|
|
||||||
highlightingRules: HighlightingRule[];
|
|
||||||
theme: Theme | DualTheme;
|
|
||||||
shiki?: ShikiOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the highlighter instance, with custom rules and options. Uses Shiki under the hood.
|
|
||||||
* @param rules Custom rules for the highlighter, from plugins.
|
|
||||||
* @param options Custom options for the highlighter.
|
|
||||||
* @returns The highlighter instance.
|
|
||||||
*/
|
|
||||||
export async function loadHighlighter({
|
|
||||||
grammarRules,
|
|
||||||
highlightingRules,
|
|
||||||
theme,
|
|
||||||
shiki
|
|
||||||
}: HighlighterOptions): Promise<Highlighter> {
|
|
||||||
// Inject rules into the custom markdown language
|
|
||||||
const injectGrammarRules = (
|
|
||||||
lang: Awaited<(typeof import('./assets/markdown'))['default']>,
|
|
||||||
rules: GrammarRule[]
|
|
||||||
) => {
|
|
||||||
lang.repository = {
|
|
||||||
...langDefinition.repository,
|
|
||||||
...Object.fromEntries(rules.map(({ name, definition }) => [name, definition]))
|
|
||||||
};
|
|
||||||
for (const rule of rules) {
|
|
||||||
if (rule.type === 'block') {
|
|
||||||
lang.repository.block.patterns.unshift({ include: `#${rule.name}` });
|
|
||||||
} else {
|
|
||||||
lang.repository.inline.patterns.unshift({ include: `#${rule.name}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const injectHighlightRules = (theme: ThemeRegistration, rules: HighlightingRule[]) => {
|
|
||||||
if (theme.type === 'light') {
|
|
||||||
theme.tokenColors ||= [];
|
|
||||||
theme.tokenColors.unshift(...rules.map(({ light }) => light));
|
|
||||||
} else {
|
|
||||||
theme.tokenColors ||= [];
|
|
||||||
theme.tokenColors.unshift(...rules.map(({ dark }) => dark));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Additional themes and languages provided by the user
|
|
||||||
const themes = shiki?.themes ?? [];
|
|
||||||
const langs = shiki?.langs ?? [];
|
|
||||||
|
|
||||||
const highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> = await getHighlighter({
|
|
||||||
themes,
|
|
||||||
langs
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom markdown language
|
|
||||||
const langDefinition = (await import('./assets/markdown')).default;
|
|
||||||
injectGrammarRules(langDefinition, grammarRules);
|
|
||||||
await highlighter.loadLanguage(langDefinition);
|
|
||||||
|
|
||||||
// Custom themes
|
|
||||||
if (isSingleTheme(theme)) {
|
|
||||||
let registration: ThemeRegistration;
|
|
||||||
if (isThemeRegistration(theme)) {
|
|
||||||
registration = theme;
|
|
||||||
} else {
|
|
||||||
registration = (await bundledThemes[theme as BundledTheme]()).default;
|
|
||||||
}
|
|
||||||
|
|
||||||
injectHighlightRules(registration, highlightingRules);
|
|
||||||
|
|
||||||
await highlighter.loadTheme(registration);
|
|
||||||
} else {
|
|
||||||
const { light, dark } = theme;
|
|
||||||
|
|
||||||
let lightRegistration: ThemeRegistration;
|
|
||||||
let darkRegistration: ThemeRegistration;
|
|
||||||
|
|
||||||
if (isThemeRegistration(light)) {
|
|
||||||
lightRegistration = light;
|
|
||||||
} else {
|
|
||||||
lightRegistration = (await bundledThemes[light as BundledTheme]()).default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isThemeRegistration(dark)) {
|
|
||||||
darkRegistration = dark;
|
|
||||||
} else {
|
|
||||||
darkRegistration = (await bundledThemes[dark as BundledTheme]()).default;
|
|
||||||
}
|
|
||||||
|
|
||||||
injectHighlightRules(lightRegistration, highlightingRules);
|
|
||||||
injectHighlightRules(darkRegistration, highlightingRules);
|
|
||||||
|
|
||||||
await highlighter.loadTheme(lightRegistration);
|
|
||||||
await highlighter.loadTheme(darkRegistration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
theme,
|
|
||||||
lang: customMarkdownLangName,
|
|
||||||
...highlighter
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export interface Highlighter extends HighlighterGeneric<BundledLanguage, BundledTheme> {
|
|
||||||
/**
|
|
||||||
* The language specified for the highlighter.
|
|
||||||
*/
|
|
||||||
theme: Theme | DualTheme;
|
|
||||||
/**
|
|
||||||
* The theme specified for the highlighter.
|
|
||||||
*/
|
|
||||||
lang: Language;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a language is a bundled language.
|
* Highlight text using Speed-Highlight with detected language.
|
||||||
* @param lang The language to check.
|
* @param text Text to highlight.
|
||||||
* @returns Whether the language is a bundled language.
|
* @param hideLineNumbers Whether to hide line numbering.
|
||||||
|
* @returns Highlighted html text.
|
||||||
*/
|
*/
|
||||||
export const isBundleLanguage = (lang: string): lang is BundledLanguage =>
|
export async function highlightAutodetect(text: string, hideLineNumbers?: boolean) {
|
||||||
Object.keys(bundledLanguages).includes(lang);
|
const lang = await detectLanguage(text);
|
||||||
/**
|
return await highlightText(text, lang, true, { hideLineNumbers: hideLineNumbers ?? true });
|
||||||
* Checks if a theme is a bundled theme.
|
}
|
||||||
* @param theme The theme to check.
|
|
||||||
* @returns Whether the theme is a bundled theme.
|
|
||||||
*/
|
|
||||||
export const isBundleTheme = (theme: string): theme is BundledTheme =>
|
|
||||||
Object.keys(bundledThemes).includes(theme);
|
|
||||||
/**
|
|
||||||
* Checks if a theme is a dual theme.
|
|
||||||
* @param theme The theme to check.
|
|
||||||
* @returns Whether the theme is a dual theme.
|
|
||||||
*/
|
|
||||||
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme =>
|
|
||||||
typeof theme == 'object' && 'light' in theme && 'dark' in theme;
|
|
||||||
/**
|
|
||||||
* Checks if a theme is a single theme.
|
|
||||||
* @param theme The theme to check.
|
|
||||||
* @returns Whether the theme is a single theme.
|
|
||||||
*/
|
|
||||||
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme);
|
|
||||||
/**
|
|
||||||
* Checks if a theme is a theme registration.
|
|
||||||
* @param theme The theme to check.
|
|
||||||
* @returns Whether the theme is a theme registration.
|
|
||||||
*/
|
|
||||||
export const isThemeRegistration = (theme: Theme): theme is ThemeRegistration =>
|
|
||||||
typeof theme == 'object';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all nested languages in the markdown text and load them into the highlighter.
|
* Load a custom language for reference in highlight rules.
|
||||||
* @param text Markdown text to parse for nested languages.
|
* @param id Id of the language.
|
||||||
* @returns The set of nested languages found in the text.
|
* @param langModule A module that has the default export set to an array of HighlightRule.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* // language.ts
|
||||||
|
* import type { HighlightLanguage } from 'carta-md';
|
||||||
|
*
|
||||||
|
* export default [
|
||||||
|
* {
|
||||||
|
* match: /helloworld/g,
|
||||||
|
* type: 'kwd'
|
||||||
|
* }
|
||||||
|
* ] satisfies HighlightLanguage;
|
||||||
|
* ```
|
||||||
|
* And in another file:
|
||||||
|
* ```
|
||||||
|
* import("./path/to/language")
|
||||||
|
* .then(module => Carta.loadCustomLanguage("lang-name", module));
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
const findNestedLanguages = (text: string) => {
|
export function loadCustomLanguage(id: string, langModule: { default: ShjLanguageDefinition }) {
|
||||||
const languages = new Set<string>();
|
return loadLanguage(id, langModule);
|
||||||
|
}
|
||||||
|
|
||||||
const regex = /```([a-z]+)\n([\s\S]+?)\n```/g;
|
export interface HighlightFunctions {
|
||||||
let match: RegExpExecArray | null;
|
highlight: typeof highlight;
|
||||||
while ((match = regex.exec(text))) {
|
highlightAutodetect: typeof highlightAutodetect;
|
||||||
languages.add(match[1]);
|
loadCustomLanguage: typeof loadCustomLanguage;
|
||||||
}
|
}
|
||||||
return languages;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all nested languages found in the markdown text into the highlighter.
|
* Load custom markdown syntax highlighting rules.
|
||||||
* @param highlighter The highlighter instance.
|
* Automatically called when a Carta instance is created.
|
||||||
* @param text The text to parse for nested languages.
|
* @param extensions Additional extensions used in Carta.
|
||||||
* @returns Whether the highlighter was updated with new languages.
|
|
||||||
*/
|
*/
|
||||||
export const loadNestedLanguages = async (highlighter: Highlighter, text: string) => {
|
export function loadCustomMarkdown(extensions: CartaExtension[] = []) {
|
||||||
text = text.replaceAll('\r\n', '\n'); // Normalize line endings
|
const highlightRules = extensions.map((ext) => ext.highlightRules ?? []).flat();
|
||||||
|
const lang = [];
|
||||||
const languages = findNestedLanguages(text);
|
lang.push(...cartaMarkdown, ...highlightRules);
|
||||||
const loadedLanguages = highlighter.getLoadedLanguages();
|
loadCustomLanguage('cartamd', { default: lang });
|
||||||
let updated = false;
|
}
|
||||||
for (const lang of languages) {
|
|
||||||
if (isBundleLanguage(lang) && !loadedLanguages.includes(lang)) {
|
|
||||||
await highlighter.loadLanguage(lang);
|
|
||||||
loadedLanguages.push(lang);
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updated
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -6,20 +6,20 @@ interface HistoryState {
|
||||||
cursor: number;
|
cursor: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextAreaHistoryOptions {
|
export interface CartaHistoryOptions {
|
||||||
/**
|
/**
|
||||||
* Minimum interval between save states in ms.
|
* Minimum interval between save states in ms.
|
||||||
* @default 300ms
|
* @default 300ms
|
||||||
*/
|
*/
|
||||||
minInterval?: number;
|
minInterval: number;
|
||||||
/**
|
/**
|
||||||
* Maximum history size in bytes.
|
* Maximum history size in bytes.
|
||||||
* @default 1MB
|
* @default 1MB
|
||||||
*/
|
*/
|
||||||
maxSize?: number;
|
maxSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultHistoryOptions: TextAreaHistoryOptions = {
|
const defaultHistoryOptions: CartaHistoryOptions = {
|
||||||
minInterval: 300,
|
minInterval: 300,
|
||||||
maxSize: 1_000_000
|
maxSize: 1_000_000
|
||||||
};
|
};
|
||||||
|
@ -27,11 +27,11 @@ const defaultHistoryOptions: TextAreaHistoryOptions = {
|
||||||
/**
|
/**
|
||||||
* Input undo/redo functionality.
|
* Input undo/redo functionality.
|
||||||
*/
|
*/
|
||||||
export class TextAreaHistory {
|
export class CartaHistory {
|
||||||
private states: HistoryState[] = [];
|
private states: HistoryState[] = [];
|
||||||
private currentIndex = -1; // Only <= 0 numbers
|
private currentIndex = -1; // Only <= 0 numbers
|
||||||
private readonly options: TextAreaHistoryOptions;
|
private readonly options: CartaHistoryOptions;
|
||||||
constructor(options?: Partial<TextAreaHistoryOptions>) {
|
constructor(options?: Partial<CartaHistoryOptions>) {
|
||||||
this.options = mergeDefaultInterface(options, defaultHistoryOptions);
|
this.options = mergeDefaultInterface(options, defaultHistoryOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ export class TextAreaHistory {
|
||||||
}
|
}
|
||||||
this.currentIndex = -1;
|
this.currentIndex = -1;
|
||||||
|
|
||||||
if (latest && Date.now() - latest.timestamp.getTime() <= (this.options.minInterval ?? 300)) {
|
if (latest && Date.now() - latest.timestamp.getTime() <= this.options.minInterval) {
|
||||||
this.states.pop();
|
this.states.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ export class TextAreaHistory {
|
||||||
// every char is 2 bytes
|
// every char is 2 bytes
|
||||||
size += value.length * 2;
|
size += value.length * 2;
|
||||||
|
|
||||||
while (size > (this.options.maxSize ?? 1_000_000)) {
|
while (size > this.options.maxSize) {
|
||||||
const removed = this.states.shift();
|
const removed = this.states.shift();
|
||||||
if (!removed) break; // This should never happen
|
if (!removed) break; // This should never happen
|
||||||
size -= removed.value.length * 2;
|
size -= removed.value.length * 2;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { ComponentType } from 'svelte';
|
import type { ComponentType } from 'svelte';
|
||||||
import type { InputEnhancer } from './input';
|
import type { CartaInput } from './input';
|
||||||
import HeadingIcon from './components/icons/HeadingIcon.svelte';
|
import HeadingIcon from './components/icons/HeadingIcon.svelte';
|
||||||
import ItalicIcon from './components/icons/ItalicIcon.svelte';
|
import ItalicIcon from './components/icons/ItalicIcon.svelte';
|
||||||
import BoldIcon from './components/icons/BoldIcon.svelte';
|
import BoldIcon from './components/icons/BoldIcon.svelte';
|
||||||
|
@ -14,16 +14,16 @@ import StrikethroughIcon from './components/icons/StrikethroughIcon.svelte';
|
||||||
/**
|
/**
|
||||||
* Editor toolbar icon information.
|
* Editor toolbar icon information.
|
||||||
*/
|
*/
|
||||||
export interface Icon {
|
export interface CartaIcon {
|
||||||
/**
|
/**
|
||||||
* The icon's unique identifier.
|
* The icon's unique identifier.
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
/**
|
||||||
* Callback function to execute when the icon is clicked.
|
* Callback function to execute when the icon is clicked.
|
||||||
* @param input InputEnhancer instance
|
* @param input CartaInput instance
|
||||||
*/
|
*/
|
||||||
action: (input: InputEnhancer) => void;
|
action: (input: CartaInput) => void;
|
||||||
/**
|
/**
|
||||||
* The icon's component.
|
* The icon's component.
|
||||||
*/
|
*/
|
||||||
|
@ -100,6 +100,6 @@ export const defaultIcons = [
|
||||||
component: ListTaskIcon,
|
component: ListTaskIcon,
|
||||||
label: 'Task list'
|
label: 'Task list'
|
||||||
}
|
}
|
||||||
] as const satisfies readonly Icon[];
|
] as const satisfies readonly CartaIcon[];
|
||||||
|
|
||||||
export type DefaultIconId = (typeof defaultIcons)[number]['id'] | 'menu';
|
export type DefaultIconId = (typeof defaultIcons)[number]['id'] | 'menu';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Listener } from './carta';
|
import type { CartaListener } from './carta';
|
||||||
import type { Prefix } from './prefixes';
|
import type { Prefix } from './prefixes';
|
||||||
import type { KeyboardShortcut } from './shortcuts';
|
import type { KeyboardShortcut } from './shortcuts';
|
||||||
import { TextAreaHistory as TextAreaHistory, type TextAreaHistoryOptions } from './history';
|
import { CartaHistory, type CartaHistoryOptions } from './history';
|
||||||
import { areEqualSets } from './utils';
|
import { areEqualSets } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,17 +21,17 @@ export interface InputSettings {
|
||||||
readonly shortcuts: KeyboardShortcut[];
|
readonly shortcuts: KeyboardShortcut[];
|
||||||
readonly prefixes: Prefix[];
|
readonly prefixes: Prefix[];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
readonly listeners: Listener<any>[];
|
readonly listeners: CartaListener<any>[];
|
||||||
readonly historyOpts?: Partial<TextAreaHistoryOptions>;
|
readonly historyOpts?: Partial<CartaHistoryOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InputEnhancer {
|
export class CartaInput {
|
||||||
private pressedKeys: Set<string>;
|
private pressedKeys: Set<string>;
|
||||||
private escapePressed = false;
|
private escapePressed = false;
|
||||||
// Used to detect keys that actually changed the textarea value
|
// Used to detect keys that actually changed the textarea value
|
||||||
private onKeyDownValue: string | undefined;
|
private onKeyDownValue: string | undefined;
|
||||||
|
|
||||||
public history: TextAreaHistory;
|
public history: CartaHistory;
|
||||||
public readonly events = new EventTarget();
|
public readonly events = new EventTarget();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -54,7 +54,7 @@ export class InputEnhancer {
|
||||||
|
|
||||||
textarea.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
textarea.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||||
|
|
||||||
this.history = new TextAreaHistory(settings.historyOpts);
|
this.history = new CartaHistory(settings.historyOpts);
|
||||||
// Save initial value
|
// Save initial value
|
||||||
this.history.saveState(this.textarea.value, this.textarea.selectionStart);
|
this.history.saveState(this.textarea.value, this.textarea.selectionStart);
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,13 @@ type IconId = Intellisense<DefaultIconId>;
|
||||||
/**
|
/**
|
||||||
* Labels that may appear in the editor.
|
* Labels that may appear in the editor.
|
||||||
*/
|
*/
|
||||||
export interface Labels {
|
export interface CartaLabels {
|
||||||
writeTab: string;
|
writeTab: string;
|
||||||
previewTab: string;
|
previewTab: string;
|
||||||
iconsLabels: Partial<Record<IconId, string>>;
|
iconsLabels: Partial<Record<IconId, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultLabels: Labels = {
|
export const DefaultCartaLabels: CartaLabels = {
|
||||||
writeTab: 'Write',
|
writeTab: 'Write',
|
||||||
previewTab: 'Preview',
|
previewTab: 'Preview',
|
||||||
iconsLabels: {}
|
iconsLabels: {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export class Renderer {
|
export class CartaRenderer {
|
||||||
constructor(public readonly container: HTMLDivElement) {}
|
constructor(public readonly container: HTMLDivElement) {}
|
||||||
// Reserved for future use
|
// Reserved for future use
|
||||||
}
|
}
|
||||||
|
|
54
packages/carta-md/src/lib/internal/shj.ts
Normal file
54
packages/carta-md/src/lib/internal/shj.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { detectLanguage } from '@speed-highlight/core/detect.js';
|
||||||
|
import type { ShjLanguageDefinition } from '@speed-highlight/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown syntax highlighting rules.
|
||||||
|
*/
|
||||||
|
const cartaMarkdown: ShjLanguageDefinition = [
|
||||||
|
{
|
||||||
|
type: 'cmnt',
|
||||||
|
match: /^>.*|(=|-)\1+/gm
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'class',
|
||||||
|
match: /\*\*((?!\*\*).)*\*\*/g
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /```((?!```)[^])*\n```/g,
|
||||||
|
sub: (code) => ({
|
||||||
|
type: 'kwd',
|
||||||
|
sub: [
|
||||||
|
{
|
||||||
|
match: /\n[^]*(?=```)/g,
|
||||||
|
sub: code.split('\n')[0].slice(3) || detectLanguage(code)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'str',
|
||||||
|
match: /`[^`]*`/g
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'var',
|
||||||
|
match: /~~((?!~~).)*~~/g
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'kwd',
|
||||||
|
match: /_[^_]*_|\*[^*]*\*/g
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'kwd',
|
||||||
|
match: /^\s*(\*|\d+\.)\s/gm
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'oper',
|
||||||
|
match: /\[[^\]]*]/g
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'func',
|
||||||
|
match: /\([^)]*\)/g
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default cartaMarkdown;
|
|
@ -1,4 +1,4 @@
|
||||||
import type { InputEnhancer } from './input';
|
import type { CartaInput } from './input';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keyboard shortcut data.
|
* Keyboard shortcut data.
|
||||||
|
@ -13,7 +13,7 @@ export interface KeyboardShortcut {
|
||||||
* Callback action.
|
* Callback action.
|
||||||
* @param input Input helper.
|
* @param input Input helper.
|
||||||
*/
|
*/
|
||||||
action: (input: InputEnhancer) => void;
|
action: (input: CartaInput) => void;
|
||||||
/**
|
/**
|
||||||
* Prevent saving the current state in history.
|
* Prevent saving the current state in history.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,8 +4,6 @@ interface Nothing {}
|
||||||
type Union<T, U> = T | (U & Nothing);
|
type Union<T, U> = T | (U & Nothing);
|
||||||
|
|
||||||
export type Intellisense<T> = Union<T, string>;
|
export type Intellisense<T> = Union<T, string>;
|
||||||
export type MaybeArray<T> = T | Array<T>;
|
|
||||||
export type NonNullable<T> = Exclude<T, null | undefined>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounce the provided function.
|
* Debounce the provided function.
|
||||||
|
|
82
packages/carta-md/src/lib/light.css
Normal file
82
packages/carta-md/src/lib/light.css
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
.shj-inline {
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 5px;
|
||||||
|
display: inline-table;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shj-numbers {
|
||||||
|
padding-left: 5px;
|
||||||
|
counter-reset: line;
|
||||||
|
}
|
||||||
|
.shj-numbers div {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.shj-numbers div::before {
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
content: counter(line);
|
||||||
|
opacity: 0.5;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 5px;
|
||||||
|
counter-increment: line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shj-syn-cmnt {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shj-syn-err,
|
||||||
|
.shj-syn-kwd {
|
||||||
|
color: #e16;
|
||||||
|
}
|
||||||
|
.shj-syn-num,
|
||||||
|
.shj-syn-class {
|
||||||
|
color: #f60;
|
||||||
|
}
|
||||||
|
.shj-numbers,
|
||||||
|
.shj-syn-cmnt {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.shj-syn-insert,
|
||||||
|
.shj-syn-str {
|
||||||
|
color: #7d8;
|
||||||
|
}
|
||||||
|
.shj-syn-bool {
|
||||||
|
color: #3bf;
|
||||||
|
}
|
||||||
|
.shj-syn-type,
|
||||||
|
.shj-syn-oper {
|
||||||
|
color: #5af;
|
||||||
|
}
|
||||||
|
.shj-syn-section,
|
||||||
|
.shj-syn-func {
|
||||||
|
color: #84f;
|
||||||
|
}
|
||||||
|
.shj-syn-deleted,
|
||||||
|
.shj-syn-var {
|
||||||
|
color: #f44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shj-oneline {
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.shj-lang-http.shj-oneline .shj-syn-kwd {
|
||||||
|
background: #25f;
|
||||||
|
color: #fff;
|
||||||
|
padding: 5px 7px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shj-multiline.shj-mode-header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.shj-multiline.shj-mode-header:before {
|
||||||
|
content: attr(data-lang);
|
||||||
|
color: #58f;
|
||||||
|
display: block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #58f3;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { MarkdownEditor } from '$lib';
|
import { CartaEditor } from '$lib';
|
||||||
import { Carta } from '$lib/internal/carta';
|
import { Carta } from '$lib/internal/carta';
|
||||||
import ToggleTheme from './ToggleTheme.svelte';
|
|
||||||
import sampleText from './sample.md?raw';
|
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
import '$lib/light.css';
|
||||||
|
|
||||||
const carta = new Carta();
|
const carta = new Carta();
|
||||||
</script>
|
</script>
|
||||||
|
@ -23,8 +23,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<ToggleTheme class="toggle-theme" />
|
<CartaEditor placeholder="Some text..." mode="tabs" {carta} />
|
||||||
<MarkdownEditor value={sampleText} placeholder="Some text..." mode="tabs" {carta} />
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -34,10 +33,9 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code) {
|
:global(.carta-font-code, code) {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
font-variant-ligatures: normal;
|
font-variant-ligatures: normal;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(input, textarea, button) {
|
:global(input, textarea, button) {
|
||||||
|
@ -45,21 +43,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
position: relative;
|
|
||||||
|
|
||||||
max-width: 1536px;
|
max-width: 1536px;
|
||||||
margin: 2rem auto 2rem auto;
|
margin: 0 auto 0 auto;
|
||||||
}
|
padding: 2rem 0 2rem 0;
|
||||||
|
|
||||||
:global(img) {
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.toggle-theme) {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -6px;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive main */
|
/* Responsive main */
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
let className = '';
|
|
||||||
let theme: 'light' | 'dark' = 'light';
|
|
||||||
|
|
||||||
export { className as class };
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="{className} {theme}"
|
|
||||||
on:click={() => {
|
|
||||||
if (theme === 'light') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
theme = 'dark';
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
theme = 'light';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M235.54 150.21a104.84 104.84 0 0 1-37 52.91A104 104 0 0 1 32 120a103.09 103.09 0 0 1 20.88-62.52a104.84 104.84 0 0 1 52.91-37a8 8 0 0 1 10 10a88.08 88.08 0 0 0 109.8 109.8a8 8 0 0 1 10 10Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
button {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
background: none;
|
|
||||||
border: 1px solid #b9b9b9;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
font-size: 1.5rem;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.dark {
|
|
||||||
color: #fff;
|
|
||||||
border-color: #4d4d4c;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.dark) {
|
|
||||||
background: #1b1b1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.dark .markdown-body) {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Editor dark mode */
|
|
||||||
|
|
||||||
:global(html.dark .carta-theme__default) {
|
|
||||||
--border-color: var(--border-color-dark);
|
|
||||||
--selection-color: var(--selection-color-dark);
|
|
||||||
--focus-outline: var(--focus-outline-dark);
|
|
||||||
--hover-color: var(--hover-color-dark);
|
|
||||||
--caret-color: var(--caret-color-dark);
|
|
||||||
--text-color: var(--text-color-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code dark mode */
|
|
||||||
|
|
||||||
:global(html.dark .shiki),
|
|
||||||
:global(html.dark .shiki span) {
|
|
||||||
color: var(--shiki-dark) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,79 +0,0 @@
|
||||||
# Heading
|
|
||||||
|
|
||||||
## Sub-heading
|
|
||||||
|
|
||||||
Paragraphs are separated
|
|
||||||
by a blank line.
|
|
||||||
|
|
||||||
Two spaces at the end of a line
|
|
||||||
produce a line break.
|
|
||||||
|
|
||||||
Text attributes _italic_,
|
|
||||||
**bold**, `monospace`. Some `console.log(lst.filter(e => e == true))` implementations may use _single-asterisks_ for italic text.
|
|
||||||
|
|
||||||
Horizontal rule:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```js
|
|
||||||
function resolveAfter2Seconds(x) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(x);
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// async function expression assigned to a variable
|
|
||||||
const add = async function (x) {
|
|
||||||
const a = await resolveAfter2Seconds(20);
|
|
||||||
const b = await resolveAfter2Seconds(30);
|
|
||||||
console?.log(`http://localhost:${PORT}/`.match(/:[0-9]{2,4}^/g));
|
|
||||||
return x + a + b;
|
|
||||||
};
|
|
||||||
|
|
||||||
add(10).then((v) => {
|
|
||||||
console.log(v); // prints 60 after 4 seconds.
|
|
||||||
});
|
|
||||||
|
|
||||||
// async function expression used as an IIFE
|
|
||||||
(async function (x) {
|
|
||||||
const p1 = resolveAfter2Seconds(20);
|
|
||||||
const p2 = resolveAfter2Seconds(30);
|
|
||||||
return x + (await p1) + (await p2);
|
|
||||||
})(10).then((v) => {
|
|
||||||
console.log(v); // prints 60 after 2 seconds.
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
```beurihiuerh
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Strikethrough:
|
|
||||||
~~strikethrough~~
|
|
||||||
|
|
||||||
Bullet list:
|
|
||||||
|
|
||||||
- apples
|
|
||||||
- oranges
|
|
||||||
- pears
|
|
||||||
|
|
||||||
Numbered list:
|
|
||||||
|
|
||||||
1. lather
|
|
||||||
2. rinse
|
|
||||||
3. repeat
|
|
||||||
|
|
||||||
An [example](http://example.com).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> Markdown uses email-style
|
|
||||||
> characters for blockquoting.
|
|
||||||
> Multiple paragraphs need to be prepended individually.
|
|
||||||
|
|
||||||
| Item | Price | In stock |
|
|
||||||
| ------------ | -------- | -------- |
|
|
||||||
| Juicy Apples | 1.99 | _7_ |
|
|
||||||
| Bananas | **1.89** | 5234 |
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.4 MiB |
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-anchor/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { anchor } from '@cartamd/plugin-anchor';
|
import { anchor } from '@cartamd/plugin-anchor';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -28,7 +28,7 @@ import '@cartamd/plugin-anchor/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -32,11 +32,11 @@
|
||||||
"!dist/**/*.spec.*"
|
"!dist/**/*.spec.*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"slugify": "^1.6.6"
|
||||||
"rehype-slug": "^6.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^4.0.0",
|
"carta-md": "^3.1.0",
|
||||||
|
"marked": "^9.1.5",
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,32 +1,13 @@
|
||||||
.carta-viewer h1,
|
.carta-renderer .anchor-link {
|
||||||
.carta-viewer h2,
|
visibility: hidden;
|
||||||
.carta-viewer h3,
|
opacity: 0.6;
|
||||||
.carta-viewer h4,
|
|
||||||
.carta-viewer h5,
|
|
||||||
.carta-viewer h6 {
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-viewer h1 .icon.icon-link,
|
.carta-renderer h1:hover .anchor-link,
|
||||||
.carta-viewer h2 .icon.icon-link,
|
.carta-renderer h2:hover .anchor-link,
|
||||||
.carta-viewer h3 .icon.icon-link,
|
.carta-renderer h3:hover .anchor-link,
|
||||||
.carta-viewer h4 .icon.icon-link,
|
.carta-renderer h4:hover .anchor-link,
|
||||||
.carta-viewer h5 .icon.icon-link,
|
.carta-renderer h5:hover .anchor-link,
|
||||||
.carta-viewer h6 .icon.icon-link {
|
.carta-renderer h6:hover .anchor-link {
|
||||||
opacity: 0;
|
visibility: visible;
|
||||||
content: url('./link.svg');
|
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
|
||||||
top: 50%;
|
|
||||||
padding-right: 4px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.carta-viewer h1:hover .icon-link,
|
|
||||||
.carta-viewer h2:hover .icon-link,
|
|
||||||
.carta-viewer h3:hover .icon-link,
|
|
||||||
.carta-viewer h4:hover .icon-link,
|
|
||||||
.carta-viewer h5:hover .icon-link,
|
|
||||||
.carta-viewer h6:hover .icon-link {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,47 @@
|
||||||
import rehypeSlug, { type Options as SlugOptions } from 'rehype-slug';
|
import type { CartaExtension } from 'carta-md';
|
||||||
import rehypeAutolinkHeadings, { type Options as AutolinkOptions } from 'rehype-autolink-headings';
|
import { generateUniqueSlug } from './slug';
|
||||||
import type { Plugin } from 'carta-md';
|
|
||||||
export * from './default.css?inline';
|
export * from './default.css?inline';
|
||||||
|
|
||||||
export interface AnchorExtensionOptions {
|
export interface AnchorExtensionOptions {
|
||||||
/**
|
/**
|
||||||
* rehype-slug options.
|
* Maximum depth of headers to generate anchors for. Defaults to 6.
|
||||||
*/
|
*/
|
||||||
slug?: SlugOptions;
|
maxDepth?: number;
|
||||||
/**
|
|
||||||
* rehype-autolink-headings options.
|
|
||||||
*/
|
|
||||||
autolink?: AutolinkOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta anchor plugin. Adds support to render anchor links in header tags.
|
* Carta anchor plugin. Adds support to render anchor links in header tags.
|
||||||
*/
|
*/
|
||||||
export const anchor = (options?: AnchorExtensionOptions): Plugin => {
|
export const anchor = (options?: AnchorExtensionOptions): CartaExtension => {
|
||||||
|
let slugs: string[] = [];
|
||||||
|
|
||||||
|
const maxDepth = options?.maxDepth ?? 6;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transformers: [
|
// Reset the slug history after rendering completes, so the links persist after re-rendering
|
||||||
|
listeners: [
|
||||||
|
['carta-render', () => (slugs = [])],
|
||||||
|
['carta-render-ssr', () => (slugs = [])]
|
||||||
|
],
|
||||||
|
markedExtensions: [
|
||||||
{
|
{
|
||||||
execution: 'sync',
|
renderer: {
|
||||||
type: 'rehype',
|
heading(text, level, raw) {
|
||||||
transform({ processor }) {
|
if (level > maxDepth) {
|
||||||
processor.use(rehypeSlug, options?.slug).use(rehypeAutolinkHeadings, options?.autolink);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = generateUniqueSlug(raw, slugs);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<h${level}>
|
||||||
|
<span>${text}</span>
|
||||||
|
<a id="${slug}" href="#${slug}" class="anchor-link">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16" width="16" height="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>
|
||||||
|
</a>
|
||||||
|
</h${level}>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
|
|
||||||
<path fill="currentColor" fill-rule="evenodd" d="M9.929 3.132a2.078 2.078 0 1 1 2.94 2.94l-.65.648a.75.75 0 0 0 1.061 1.06l.649-.648a3.579 3.579 0 0 0-5.06-5.06L6.218 4.72a3.58 3.58 0 0 0 0 5.06a.75.75 0 0 0 1.061-1.06a2.08 2.08 0 0 1 0-2.94zm-.15 3.086a.75.75 0 0 0-1.057 1.064c.816.81.818 2.13.004 2.942l-2.654 2.647a2.08 2.08 0 0 1-2.94-2.944l.647-.647a.75.75 0 0 0-1.06-1.06l-.648.647a3.58 3.58 0 0 0 5.06 5.066l2.654-2.647a3.575 3.575 0 0 0-.007-5.068Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 575 B |
23
packages/plugin-anchor/src/lib/slug.ts
Normal file
23
packages/plugin-anchor/src/lib/slug.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import slugify from 'slugify';
|
||||||
|
|
||||||
|
function generateSlug(raw: string) {
|
||||||
|
const base = slugify(raw, {
|
||||||
|
lower: true,
|
||||||
|
remove: /[^a-zA-Z0-9_\- ]/g
|
||||||
|
});
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateUniqueSlug(raw: string, slugs: string[]) {
|
||||||
|
const base = generateSlug(raw);
|
||||||
|
let slug = base;
|
||||||
|
|
||||||
|
let i = 1;
|
||||||
|
// Add unique suffix to slug if it already exists
|
||||||
|
while (slugs.includes(slug)) {
|
||||||
|
slug = `${base}-${i}`;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
slugs.push(slug);
|
||||||
|
return slug;
|
||||||
|
}
|
|
@ -1,14 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { anchor } from '$lib';
|
import { anchor } from '$lib';
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
sanitizer: false,
|
|
||||||
extensions: [
|
extensions: [
|
||||||
anchor({
|
anchor({
|
||||||
autolink: {}
|
maxDepth: 2
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -29,7 +28,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<MarkdownEditor {carta} {value} />
|
<CartaEditor {carta} {value} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -39,15 +38,9 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code) {
|
:global(.carta-font-code, code) {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
font-variant-ligatures: normal;
|
font-variant-ligatures: normal;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.carta-renderer) {
|
|
||||||
/* Add some space to show icons */
|
|
||||||
padding-left: 2.5rem !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(input, textarea, button) {
|
:global(input, textarea, button) {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-attachment/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { attachment } from '@cartamd/plugin-attachment';
|
import { attachment } from '@cartamd/plugin-attachment';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -34,7 +34,7 @@ import '@cartamd/plugin-attachment/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"!dist/**/*.spec.*"
|
"!dist/**/*.spec.*"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^4.0.0",
|
"carta-md": "^3.4.0",
|
||||||
"marked": "^9.1.5",
|
"marked": "^9.1.5",
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Carta, Plugin, Listener } from 'carta-md';
|
import type { Carta, CartaExtension, CartaListener } from 'carta-md';
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import type { SvelteComponent } from 'svelte';
|
import type { SvelteComponent } from 'svelte';
|
||||||
import DropOverlay from './DropOverlay.svelte';
|
import DropOverlay from './DropOverlay.svelte';
|
||||||
|
@ -40,7 +40,7 @@ const ImageMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml']
|
||||||
/**
|
/**
|
||||||
* Carta attachment plugin.
|
* Carta attachment plugin.
|
||||||
*/
|
*/
|
||||||
export const attachment = (options: AttachmentExtensionOptions): Plugin => {
|
export const attachment = (options: AttachmentExtensionOptions): CartaExtension => {
|
||||||
let carta: Carta | undefined;
|
let carta: Carta | undefined;
|
||||||
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
|
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
|
||||||
|
|
||||||
|
@ -102,31 +102,15 @@ export const attachment = (options: AttachmentExtensionOptions): Plugin => {
|
||||||
for (const file of files) handleFile(file);
|
for (const file of files) handleFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePaste(this: HTMLTextAreaElement, e: ClipboardEvent) {
|
|
||||||
const items = e.clipboardData?.items;
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
const itemsArray = Array.from(items);
|
|
||||||
for (const item of itemsArray) {
|
|
||||||
if (item.kind === 'file') {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
if (!file) continue;
|
|
||||||
e.preventDefault();
|
|
||||||
handleFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onLoad: ({ carta: c }) => {
|
onLoad: ({ carta: c }) => {
|
||||||
carta = c;
|
carta = c;
|
||||||
},
|
},
|
||||||
listeners: [
|
listeners: [
|
||||||
['drop', handleDrop, false] satisfies Listener<'drop'>,
|
['drop', handleDrop, false] satisfies CartaListener<'drop'>,
|
||||||
['dragenter', () => draggingOverTextArea.set(true)] satisfies Listener<'dragenter'>,
|
['dragenter', () => draggingOverTextArea.set(true)] satisfies CartaListener<'dragenter'>,
|
||||||
['dragleave', () => draggingOverTextArea.set(false)] satisfies Listener<'dragleave'>,
|
['dragleave', () => draggingOverTextArea.set(false)] satisfies CartaListener<'dragleave'>,
|
||||||
['dragover', (e) => e.preventDefault()] satisfies Listener<'dragover'>,
|
['dragover', (e) => e.preventDefault()] satisfies CartaListener<'dragover'>
|
||||||
['paste', handlePaste, false] satisfies Listener<'paste'>
|
|
||||||
],
|
],
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { attachment } from '$lib';
|
import { attachment } from '$lib';
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
|
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
# Carta Code Plugin
|
# Carta Code Plugin
|
||||||
|
|
||||||
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
|
This plugin adds support for code blocks **syntax highlighting**. Install it using:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm i @cartamd/plugin-code
|
npm i @cartamd/plugin-code
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This is done using [Speed-highlight JS](https://github.com/speed-highlight/core), which supports dynamic imports. This way, languages definitions are only imported at the moment of need.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Styles
|
### Styles
|
||||||
|
@ -18,31 +20,28 @@ import '@cartamd/plugin-code/default.css';
|
||||||
|
|
||||||
### Using the default highlighter
|
### Using the default highlighter
|
||||||
|
|
||||||
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options. Remember to also have it loaded into the highlighter, by specifying it in `shikiOptions`.
|
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default.
|
||||||
|
The theme is the same as the one used in the main carta package (`carta-md/light.css` or `carta-md/dark.css`).
|
||||||
```ts
|
[Here](https://github.com/speed-highlight/core/tree/main/src/themes) you can find other themes.
|
||||||
const carta = new Carta({
|
|
||||||
// ...
|
|
||||||
extensions: [
|
|
||||||
code({
|
|
||||||
theme: 'ayu-light'
|
|
||||||
})
|
|
||||||
],
|
|
||||||
shikiOptions: {
|
|
||||||
themes: ['ayu-light']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using a custom highlighter
|
### Using a custom highlighter
|
||||||
|
|
||||||
It is no longer possible to specify a custom highlighter in this plugin. However, there are many different [Remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) that provide syntax highlighting.
|
You can also provide a custom highlighter, that can be either sync or async.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
code({
|
||||||
|
customHighlight: {
|
||||||
|
highlighter: (code, lang) => myCustomHighlighter(code, lang),
|
||||||
|
langPrefix: 'my-highlighter-'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Extension
|
### Extension
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { code } from '@cartamd/plugin-code';
|
import { code } from '@cartamd/plugin-code';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -50,7 +49,7 @@ It is no longer possible to specify a custom highlighter in this plugin. However
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -16,22 +16,21 @@
|
||||||
"build": "tsc && tscp"
|
"build": "tsc && tscp"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@shikijs/rehype": "^1.4.0",
|
|
||||||
"@types/node": "^18.16.3",
|
"@types/node": "^18.16.3",
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"typescript-cp": "^0.1.8"
|
"typescript-cp": "^0.1.8",
|
||||||
|
"marked": "^9.1.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^4.0.0"
|
"carta-md": "^3.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"version": "4.0.0",
|
"version": "3.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/rehype": "^1.4.0",
|
"marked-highlight": "^2.0.6"
|
||||||
"unified": "^11.0.4"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
|
|
|
@ -1,52 +1,70 @@
|
||||||
import type { DualTheme, Theme, Plugin } from 'carta-md';
|
import type { CartaExtension, HighlightFunctions } from 'carta-md';
|
||||||
import type { RehypeShikiOptions } from '@shikijs/rehype';
|
import { markedHighlight } from 'marked-highlight';
|
||||||
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
|
|
||||||
|
|
||||||
export type CodeExtensionOptions = Omit<RehypeShikiOptions, 'theme' | 'themes'> & {
|
interface CodeExtensionOptions {
|
||||||
theme?: Theme | DualTheme;
|
/**
|
||||||
};
|
* Default language when none is provided.
|
||||||
|
*/
|
||||||
|
defaultLanguage?: string;
|
||||||
|
/**
|
||||||
|
* Whether to autodetect a language when none is provided.
|
||||||
|
* Overwritten by `defaultLanguage`.
|
||||||
|
*/
|
||||||
|
autoDetect?: string;
|
||||||
|
/**
|
||||||
|
* Line numbering.
|
||||||
|
* @defaults false.
|
||||||
|
*/
|
||||||
|
lineNumbering?: boolean;
|
||||||
|
|
||||||
// FIXME: find a better solution then copy-pasting these functions in next version.
|
/**
|
||||||
// However, when importing from carta-md, this causes a MODULE_NOT_FOUND error
|
* Options for custom syntax highlighting.
|
||||||
// for some reason.
|
*/
|
||||||
/**
|
customHighlight?: {
|
||||||
* Checks if a theme is a dual theme.
|
/**
|
||||||
* @param theme The theme to check.
|
* Custom highlight function. Beware that you'll have to provide your own styles.
|
||||||
* @returns Whether the theme is a dual theme.
|
* This function needs to convert a string of code into html.
|
||||||
*/
|
*/
|
||||||
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme =>
|
highlighter: (code: string, lang: string) => string | Promise<string>;
|
||||||
typeof theme == 'object' && 'light' in theme && 'dark' in theme;
|
/**
|
||||||
/**
|
* The language tag found immediately after the code block opening marker is
|
||||||
* Checks if a theme is a single theme.
|
* appended to this to form the class attribute added to the `<code>` element.
|
||||||
* @param theme The theme to check.
|
*/
|
||||||
* @returns Whether the theme is a single theme.
|
langPrefix: string;
|
||||||
*/
|
};
|
||||||
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme);
|
}
|
||||||
|
|
||||||
|
let shj: HighlightFunctions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta code highlighting plugin. Themes available on [GitHub](https://github.com/speed-highlight/core/tree/main/dist/themes).
|
* Carta code highlighting plugin. Themes available on [GitHub](https://github.com/speed-highlight/core/tree/main/dist/themes).
|
||||||
*/
|
*/
|
||||||
export const code = (options?: CodeExtensionOptions): Plugin => {
|
export const code = (options?: CodeExtensionOptions): CartaExtension => {
|
||||||
return {
|
return {
|
||||||
transformers: [
|
onLoad: ({ highlight }) => (shj = highlight),
|
||||||
{
|
markedExtensions: [
|
||||||
execution: 'async',
|
markedHighlight({
|
||||||
type: 'rehype',
|
langPrefix: options?.customHighlight?.langPrefix ?? 'shj-lang-',
|
||||||
async transform({ processor, carta }) {
|
async: true,
|
||||||
let theme = options?.theme;
|
async highlight(code, lang) {
|
||||||
|
if (options?.customHighlight) {
|
||||||
const highlighter = await carta.highlighter();
|
return await options.customHighlight.highlighter(code, lang);
|
||||||
if (!theme) {
|
|
||||||
theme = highlighter.theme; // Use the theme specified in the highlighter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSingleTheme(theme)) {
|
const { highlight, highlightAutodetect } = shj;
|
||||||
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, theme });
|
|
||||||
} else {
|
lang ||= options?.defaultLanguage ?? '';
|
||||||
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, themes: theme });
|
let highlighted: string | null = null;
|
||||||
}
|
|
||||||
|
if (lang) highlighted = await highlight(code, lang, !(options?.lineNumbering ?? false));
|
||||||
|
if (highlighted) return highlighted;
|
||||||
|
|
||||||
|
if (options?.autoDetect ?? true)
|
||||||
|
return await highlightAutodetect(code, !(options?.lineNumbering ?? false));
|
||||||
|
|
||||||
|
return (await highlight(code, 'plain', !(options?.lineNumbering ?? false))) as string;
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-emoji/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { emoji } from '@cartamd/plugin-emoji';
|
import { emoji } from '@cartamd/plugin-emoji';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -28,7 +28,7 @@ import '@cartamd/plugin-emoji/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@cartamd/plugin-emoji",
|
"name": "@cartamd/plugin-emoji",
|
||||||
"version": "4.0.0",
|
"version": "3.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
@ -33,11 +33,11 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bezier-easing": "^2.1.0",
|
"bezier-easing": "^2.1.0",
|
||||||
"node-emoji": "^1.11.0",
|
"node-emoji": "^1.11.0"
|
||||||
"remark-gemoji": "^8.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^4.0.0",
|
"carta-md": "^3.1.0",
|
||||||
|
"marked": "^9.1.5",
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Plugin, ExtensionComponent, GrammarRule, HighlightingRule } from 'carta-md';
|
import type { CartaExtension, CartaExtensionComponent } from 'carta-md';
|
||||||
import remarkGemoji from 'remark-gemoji';
|
import type { TokenizerAndRendererExtension } from 'marked';
|
||||||
import { fade, scale, type TransitionConfig } from 'svelte/transition';
|
import { fade, scale, type TransitionConfig } from 'svelte/transition';
|
||||||
|
import nodeEmoji from 'node-emoji';
|
||||||
import Emoji from './Emoji.svelte';
|
import Emoji from './Emoji.svelte';
|
||||||
import BezierEasing from 'bezier-easing';
|
import BezierEasing from 'bezier-easing';
|
||||||
export * from './default.css?inline';
|
export * from './default.css?inline';
|
||||||
|
@ -24,7 +25,7 @@ interface ComponentProps {
|
||||||
/**
|
/**
|
||||||
* Carta emoji plugin. Adds support to render emojis as well as an emojis snippet.
|
* Carta emoji plugin. Adds support to render emojis as well as an emojis snippet.
|
||||||
*/
|
*/
|
||||||
export const emoji = (options?: EmojiExtensionOptions): Plugin => {
|
export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
|
||||||
const inTransition =
|
const inTransition =
|
||||||
options?.inTransition ??
|
options?.inTransition ??
|
||||||
((node: Element) =>
|
((node: Element) =>
|
||||||
|
@ -39,7 +40,7 @@ export const emoji = (options?: EmojiExtensionOptions): Plugin => {
|
||||||
duration: 100
|
duration: 100
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const emojiComponent: ExtensionComponent<ComponentProps> = {
|
const emojiComponent: CartaExtensionComponent<ComponentProps> = {
|
||||||
component: Emoji,
|
component: Emoji,
|
||||||
parent: 'input',
|
parent: 'input',
|
||||||
props: {
|
props: {
|
||||||
|
@ -48,42 +49,39 @@ export const emoji = (options?: EmojiExtensionOptions): Plugin => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const grammar = {
|
|
||||||
name: 'emoji',
|
|
||||||
type: 'inline',
|
|
||||||
definition: {
|
|
||||||
match: ':[a-zA-Z_]+:',
|
|
||||||
name: 'markup.emoji.markdown'
|
|
||||||
}
|
|
||||||
} satisfies GrammarRule;
|
|
||||||
|
|
||||||
const highlighting = {
|
|
||||||
light: {
|
|
||||||
scope: 'markup.emoji',
|
|
||||||
settings: {
|
|
||||||
foreground: '#3bf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
scope: 'markup.emoji',
|
|
||||||
settings: {
|
|
||||||
foreground: '#4dacfa'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} satisfies HighlightingRule;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transformers: [
|
markedExtensions: [
|
||||||
{
|
{
|
||||||
execution: 'sync',
|
extensions: [emojiTokenizerAndRenderer()]
|
||||||
type: 'remark',
|
|
||||||
transform({ processor }) {
|
|
||||||
processor.use(remarkGemoji);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
components: [emojiComponent],
|
components: [emojiComponent],
|
||||||
grammarRules: [grammar],
|
highlightRules: [
|
||||||
highlightingRules: [highlighting]
|
{
|
||||||
|
type: 'oper',
|
||||||
|
match: /:[a-z0-9_]+:/g
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function emojiTokenizerAndRenderer(): TokenizerAndRendererExtension {
|
||||||
|
return {
|
||||||
|
name: 'emoji',
|
||||||
|
level: 'inline',
|
||||||
|
start: (src) => src.indexOf(':'),
|
||||||
|
tokenizer: (src) => {
|
||||||
|
const match = src.match(/^:.*?:/)?.at(0);
|
||||||
|
if (!match) return undefined;
|
||||||
|
const emoji = nodeEmoji.find(match)?.emoji;
|
||||||
|
if (emoji) {
|
||||||
|
return {
|
||||||
|
type: 'emoji',
|
||||||
|
raw: match,
|
||||||
|
emoji
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer: (token) => token.emoji
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { emoji } from '$lib';
|
import { emoji } from '$lib';
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -32,10 +32,9 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code) {
|
:global(.carta-font-code, code) {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
font-variant-ligatures: normal;
|
font-variant-ligatures: normal;
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(input, textarea, button) {
|
:global(input, textarea, button) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ or by using a content delivery network:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { math } from '@cartamd/plugin-math';
|
import { math } from '@cartamd/plugin-math';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -46,7 +46,7 @@ or by using a content delivery network:
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
@ -17,20 +17,23 @@
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/katex": "^0.16.0",
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4",
|
||||||
|
"marked": "^9.1.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^4.0.0"
|
"carta-md": "^3.0.0",
|
||||||
|
"katex": "^0.16.7",
|
||||||
|
"marked": "^9.1.5"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rehype-katex": "^7.0.0",
|
"katex": "^0.16.7"
|
||||||
"remark-math": "^6.0.0"
|
|
||||||
},
|
},
|
||||||
"version": "4.0.1",
|
"version": "3.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import type { Plugin } from 'carta-md';
|
import type { Carta, CartaExtension } from 'carta-md';
|
||||||
import remarkMath, { type Options as RemarkMathOptions } from 'remark-math';
|
import { TokenizerAndRendererExtension } from 'marked';
|
||||||
import rehypeKatex, { type Options as RehypeKatexOptions } from 'rehype-katex';
|
import katex, { KatexOptions } from 'katex';
|
||||||
|
|
||||||
interface MathExtensionOptions {
|
interface MathExtensionOptions {
|
||||||
/**
|
/**
|
||||||
* Options for inline katex, eg: $a^2+b^2=c^2$
|
* Options for inline katex, eg: $a^2+b^2=c^2$
|
||||||
*/
|
*/
|
||||||
inline?: {
|
inline?: {
|
||||||
|
katexOptions?: KatexOptions;
|
||||||
/**
|
/**
|
||||||
* @default control+m
|
* @default control+m
|
||||||
*/
|
*/
|
||||||
|
@ -19,45 +20,51 @@ interface MathExtensionOptions {
|
||||||
* $$
|
* $$
|
||||||
*/
|
*/
|
||||||
block?: {
|
block?: {
|
||||||
|
/**
|
||||||
|
* Tag the generated katex will be put into. Must have `display: block`.
|
||||||
|
*/
|
||||||
|
tag?: string;
|
||||||
|
/**
|
||||||
|
* Whether to center the generated expression.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
center?: boolean;
|
||||||
|
/**
|
||||||
|
* Class for generated katex.
|
||||||
|
*/
|
||||||
|
class?: string;
|
||||||
/**
|
/**
|
||||||
* @default ctrl+shift+m
|
* @default ctrl+shift+m
|
||||||
*/
|
*/
|
||||||
shortcut?: Set<string>;
|
shortcut?: Set<string>;
|
||||||
|
katexOptions?: KatexOptions;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Options for remark-math
|
|
||||||
*/
|
|
||||||
remarkMath?: RemarkMathOptions;
|
|
||||||
/**
|
|
||||||
* Options for rehype-katex
|
|
||||||
*/
|
|
||||||
rehypeKatex?: RehypeKatexOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeRender(tex: string, options?: KatexOptions | undefined) {
|
||||||
|
try {
|
||||||
|
return katex.renderToString(tex, options);
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let carta: Carta;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta math plugin. Code adapted from [marked-katex-extension](https://github.com/UziTech/marked-katex-extension).
|
* Carta math plugin. Code adapted from [marked-katex-extension](https://github.com/UziTech/marked-katex-extension).
|
||||||
*/
|
*/
|
||||||
export const math = (options?: MathExtensionOptions): Plugin => {
|
export const math = (options?: MathExtensionOptions): CartaExtension => {
|
||||||
return {
|
return {
|
||||||
onLoad: async ({ carta }) => {
|
onLoad: ({ carta: c, highlight: shj }) => {
|
||||||
const highlighter = await carta.highlighter();
|
carta = c;
|
||||||
await highlighter.loadLanguage('latex');
|
import('./latex.js')
|
||||||
carta.input?.update();
|
.then((module) => shj.loadCustomLanguage('latex', module))
|
||||||
|
.then(() => carta.input?.update());
|
||||||
},
|
},
|
||||||
transformers: [
|
markedExtensions: [
|
||||||
{
|
{
|
||||||
execution: 'sync',
|
extensions: [inlineKatex(options?.inline), blockKatex(options?.block)]
|
||||||
type: 'remark',
|
|
||||||
transform({ processor }) {
|
|
||||||
processor.use(remarkMath, options?.remarkMath);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
execution: 'sync',
|
|
||||||
type: 'rehype',
|
|
||||||
transform({ processor }) {
|
|
||||||
processor.use(rehypeKatex, options?.rehypeKatex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
|
@ -72,57 +79,64 @@ export const math = (options?: MathExtensionOptions): Plugin => {
|
||||||
action: (input) => input.toggleSelectionSurrounding(['$$\n', '\n$$'])
|
action: (input) => input.toggleSelectionSurrounding(['$$\n', '\n$$'])
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
grammarRules: [
|
highlightRules: [
|
||||||
{
|
{
|
||||||
name: 'inline_math',
|
match: /\$[{}[\]a-zA-Z0-9.+-_=*/\\ ]+\$/g,
|
||||||
type: 'inline',
|
sub: 'latex'
|
||||||
definition: {
|
|
||||||
match: '(\\$+)((?:[^\\$]|(?!(?<!\\$)\\1(?!\\$))\\$)*+)(\\1)',
|
|
||||||
name: 'markup.inline.math.markdown',
|
|
||||||
captures: {
|
|
||||||
'1': { name: 'punctuation.definition.latex.inline' },
|
|
||||||
'2': { name: 'meta.embedded.block.latex', patterns: [{ include: 'text.tex.latex' }] },
|
|
||||||
'3': { name: 'punctuation.definition.latex.inline' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'block_math',
|
match: /^\$\$+\n([^$]+?)\n\$\$+\n/gm,
|
||||||
type: 'block',
|
sub: 'latex'
|
||||||
definition: {
|
|
||||||
begin: '(^|\\G)(\\s*)(\\${2,})\\s*(?=([^$]*)?$)',
|
|
||||||
beginCaptures: {
|
|
||||||
'3': { name: 'punctuation.definition.latex.block' }
|
|
||||||
},
|
|
||||||
endCaptures: { '3': { name: 'punctuation.definition.latex.block' } },
|
|
||||||
end: '(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$',
|
|
||||||
name: 'markup.block.math.markdown',
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
begin: '(^|\\G)(\\s*)(.*)',
|
|
||||||
contentName: 'meta.embedded.block.latex',
|
|
||||||
patterns: [{ include: 'text.tex.latex' }],
|
|
||||||
while: '(^|\\G)(?!\\s*([$]{2,})\\s*$)'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
highlightingRules: [
|
|
||||||
{
|
|
||||||
light: {
|
|
||||||
scope: 'punctuation.definition.latex',
|
|
||||||
settings: {
|
|
||||||
foreground: '#5AF'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
scope: 'punctuation.definition.latex',
|
|
||||||
settings: {
|
|
||||||
foreground: '#4DACFA'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inlineKatex = (options?: MathExtensionOptions['inline']): TokenizerAndRendererExtension => {
|
||||||
|
return {
|
||||||
|
name: 'inlineKatex',
|
||||||
|
level: 'inline',
|
||||||
|
start: (src) => src.indexOf('$'),
|
||||||
|
tokenizer: (src) => {
|
||||||
|
const match = src.match(/^\$+([^$\n]+?)\$+/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'inlineKatex',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[1].trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer: (token) => safeRender(token.text, options?.katexOptions)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockKatex = (options?: MathExtensionOptions['block']): TokenizerAndRendererExtension => {
|
||||||
|
return {
|
||||||
|
name: 'blockKatex',
|
||||||
|
level: 'block',
|
||||||
|
start: (src) => src.indexOf('\n$$'),
|
||||||
|
tokenizer: (src) => {
|
||||||
|
const match = src.match(/^\$\$+\n([^$]+?)\n\$\$+\n/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'blockKatex',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[1].trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer: (token) => {
|
||||||
|
const tag = options?.tag ?? 'p';
|
||||||
|
const center = options?.center ?? true;
|
||||||
|
const katexOptions = options?.katexOptions ?? {};
|
||||||
|
if (katexOptions?.displayMode === undefined) katexOptions.displayMode = true;
|
||||||
|
return `
|
||||||
|
<${tag}
|
||||||
|
class="${options?.class ?? ''}"
|
||||||
|
${center ? 'align="center"' : ''}
|
||||||
|
>${safeRender(token.text, katexOptions)}
|
||||||
|
</${tag}>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
38
packages/plugin-math/src/latex.ts
Normal file
38
packages/plugin-math/src/latex.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
match:
|
||||||
|
/\\(frac|tfrac|dfrac|sqrt|over|above|cfrac|binom|dbinom|brace|choose|tbinom|brack)(?![a-zA-Z0-9])/g,
|
||||||
|
type: 'str'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match:
|
||||||
|
/\\(amalg|circledast|ldotp|rtimes&&|And|circledcirc|lor|setminus|ast|circleddash|lessdot|smallsetminus|barwedge|Cup|lhd|sqcap|bigcirc|cup|ltimes|sqcupmodmod|bmod|curlyveexmodaxmodax|moda|times|boxdot|curlywedge|mp|unlhd|boxminus|div|odot|unrhd|boxplus|divideontimes|ominus|uplus|boxtimes|dotplus|oplus|vee|bullet|doublebarwedge|otimes|veebar|Cap|doublecap|oslash|wedge|cap|doublecup|pmor|plusmn|wr|centerdot|land|rhd|circ|leftthreetimes|rightthreetimes|cdot|gtrdot|pmod|cdotp|intercal|pod)(?![a-zA-Z0-9])/g,
|
||||||
|
type: 'class'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match:
|
||||||
|
/\\(mathscr|mathrm|mathbf|mathit|mathnormal|textbf|textit|textrm|bf|it|rm|bold|textup|textnormal|boldsymbol{Ab}|Bbb|text|bm|mathbb|mathsf|textmd|frak|textsf|mathtt|mathfrak|sf|texttt|mathcal|tt|cal)(?![a-zA-Z0-9])/g,
|
||||||
|
type: 'insert'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match:
|
||||||
|
/\\(sum|prod|bigotimes|bigvee|int|coprod|bigoplus|bigwedge|iint|intop|bigodot|bigcap|iiint|smallint|biguplus|bigcup|oint|oiint|oiiint|bigsqcup)(?![a-zA-Z0-9])/g,
|
||||||
|
type: 'func'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /\\[a-zA-Z0-9]+/g,
|
||||||
|
type: 'oper'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(\(|\)|\{|\}|\[|\])/g,
|
||||||
|
type: 'esc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /[a-zA-Z]+/g,
|
||||||
|
type: 'var'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /[0-9]+/g,
|
||||||
|
type: 'num'
|
||||||
|
}
|
||||||
|
];
|
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-slash/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { slash } from '@cartamd/plugin-slash';
|
import { slash } from '@cartamd/plugin-slash';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -28,7 +28,7 @@ import '@cartamd/plugin-slash/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@cartamd/plugin-slash",
|
"name": "@cartamd/plugin-slash",
|
||||||
"version": "4.0.1",
|
"version": "3.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
"bezier-easing": "^2.1.0"
|
"bezier-easing": "^2.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^4.0.0",
|
"carta-md": "^3.1.0",
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { fade, scale, type TransitionConfig } from 'svelte/transition';
|
import { fade, scale, type TransitionConfig } from 'svelte/transition';
|
||||||
import SlashComponent from './Slash.svelte';
|
import SlashComponent from './Slash.svelte';
|
||||||
import type { Plugin, ExtensionComponent } from 'carta-md';
|
import type { CartaExtension, CartaExtensionComponent } from 'carta-md';
|
||||||
import BezierEasing from 'bezier-easing';
|
import BezierEasing from 'bezier-easing';
|
||||||
import { defaultSnippets, type DefaultSnippetId, type SlashSnippet } from './snippets';
|
import { defaultSnippets, type DefaultSnippetId, type SlashSnippet } from './snippets';
|
||||||
export * from './default.css?inline';
|
export * from './default.css?inline';
|
||||||
|
@ -35,7 +35,7 @@ interface ComponentProps {
|
||||||
* @param options Extension options.
|
* @param options Extension options.
|
||||||
* @returns The slash extension.
|
* @returns The slash extension.
|
||||||
*/
|
*/
|
||||||
export const slash = (options?: SlashExtensionOptions): Plugin => {
|
export const slash = (options?: SlashExtensionOptions): CartaExtension => {
|
||||||
const snippets: SlashSnippet[] = defaultSnippets.filter((snippet) =>
|
const snippets: SlashSnippet[] = defaultSnippets.filter((snippet) =>
|
||||||
options?.disableDefaultSnippets === true
|
options?.disableDefaultSnippets === true
|
||||||
? false
|
? false
|
||||||
|
@ -56,7 +56,7 @@ export const slash = (options?: SlashExtensionOptions): Plugin => {
|
||||||
fade(node, {
|
fade(node, {
|
||||||
duration: 100
|
duration: 100
|
||||||
}));
|
}));
|
||||||
const slashComponent: ExtensionComponent<ComponentProps> = {
|
const slashComponent: CartaExtensionComponent<ComponentProps> = {
|
||||||
component: SlashComponent,
|
component: SlashComponent,
|
||||||
props: {
|
props: {
|
||||||
snippets,
|
snippets,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { InputEnhancer } from 'carta-md';
|
import type { CartaInput } from 'carta-md';
|
||||||
|
|
||||||
export interface SlashSnippet {
|
export interface SlashSnippet {
|
||||||
/**
|
/**
|
||||||
|
@ -12,10 +12,10 @@ export interface SlashSnippet {
|
||||||
* Snippet callback.
|
* Snippet callback.
|
||||||
* @param input Carta input.
|
* @param input Carta input.
|
||||||
*/
|
*/
|
||||||
action: (input: InputEnhancer) => void;
|
action: (input: CartaInput) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertLine(input: InputEnhancer, string: string) {
|
function insertLine(input: CartaInput, string: string) {
|
||||||
const line = input.getLine();
|
const line = input.getLine();
|
||||||
if (line.value !== '') {
|
if (line.value !== '') {
|
||||||
input.insertAt(line.end, `\n${string}`);
|
input.insertAt(line.end, `\n${string}`);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { slash } from '$lib';
|
import { slash } from '$lib';
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -16,7 +16,7 @@ npm i @cartamd/plugin-tikz
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, MarkdownEditor } from 'carta-md';
|
import { Carta, CartaEditor } from 'carta-md';
|
||||||
import { tikz } from '@cartamd/plugin-tikz';
|
import { tikz } from '@cartamd/plugin-tikz';
|
||||||
|
|
||||||
import '@cartamd/plugin-tikz/fonts.css';
|
import '@cartamd/plugin-tikz/fonts.css';
|
||||||
|
@ -26,7 +26,7 @@ npm i @cartamd/plugin-tikz
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor {carta} />
|
<CartaEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -18,23 +18,23 @@
|
||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/hast": "^3.0.4",
|
|
||||||
"@types/md5": "^2.3.2",
|
"@types/md5": "^2.3.2",
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
|
"marked": "^9.1.5",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"unified": "^11.0.4",
|
|
||||||
"vite": "^5.1.6",
|
"vite": "^5.1.6",
|
||||||
"vite-plugin-dts": "^3.7.3",
|
"vite-plugin-dts": "^3.7.3",
|
||||||
"vite-raw-plugin": "^1.0.2"
|
"vite-raw-plugin": "^1.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^4.0.0"
|
"carta-md": "^3.0.0",
|
||||||
|
"marked": "^9.1.5"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"version": "4.0.0",
|
"version": "3.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
@ -46,9 +46,5 @@
|
||||||
"syntax highlighting",
|
"syntax highlighting",
|
||||||
"emoji",
|
"emoji",
|
||||||
"katex"
|
"katex"
|
||||||
],
|
]
|
||||||
"dependencies": {
|
|
||||||
"hast-util-from-dom": "^5.0.0",
|
|
||||||
"unist-util-visit": "^5.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import type { Carta, Event, Plugin } from 'carta-md';
|
import type { Carta, CartaEvent, CartaExtension } from 'carta-md';
|
||||||
import type { Plugin as UnifiedPlugin } from 'unified';
|
import { TokenizerAndRendererExtension } from 'marked';
|
||||||
import { visit, SKIP } from 'unist-util-visit';
|
|
||||||
import { fromDom } from 'hast-util-from-dom';
|
|
||||||
import type * as hast from 'hast';
|
|
||||||
import md5 from 'md5';
|
import md5 from 'md5';
|
||||||
|
|
||||||
interface TikzExtensionOptions {
|
interface TikzExtensionOptions {
|
||||||
|
@ -21,124 +18,102 @@ interface TikzExtensionOptions {
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
/**
|
/**
|
||||||
* Post processing function for html.
|
* Post processing function for html.
|
||||||
* This also runs on stored html.
|
* This also runs on stored html, differently
|
||||||
|
* from `postProcess`, which only runs when
|
||||||
|
* the element is first created.
|
||||||
*/
|
*/
|
||||||
postProcessing?: (html: string) => string;
|
postProcessing?: (html: string) => string;
|
||||||
|
/**
|
||||||
|
* Post processing function for rendered SVGs Elem.
|
||||||
|
* @deprecated Use `postProcessing` instead.
|
||||||
|
*/
|
||||||
|
postProcess?: (elem: SVGElement) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let carta: Carta;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TikzJax extension for Carta.
|
* TikzJax extension for Carta.
|
||||||
* @param options Tikz options.
|
* @param options Tikz options.
|
||||||
*/
|
*/
|
||||||
export const tikz = (options?: TikzExtensionOptions): Plugin => {
|
export const tikz = (options?: TikzExtensionOptions): CartaExtension => {
|
||||||
let carta: Carta;
|
|
||||||
return {
|
return {
|
||||||
onLoad: async ({ carta: c }) => {
|
cartaRef: (c) => (carta = c),
|
||||||
carta = c;
|
shjRef: (shj) => {
|
||||||
|
import('./tikz')
|
||||||
const highlighter = await carta.highlighter();
|
.then((module) => shj.loadCustomLanguage('tikz', module))
|
||||||
await highlighter.loadLanguage('latex');
|
.then(() => carta.input?.update());
|
||||||
carta.input?.update();
|
|
||||||
},
|
},
|
||||||
transformers: [
|
markedExtensions: [
|
||||||
{
|
{
|
||||||
execution: 'async',
|
async: true,
|
||||||
type: 'rehype',
|
extensions: [tikzTokenizer(options)]
|
||||||
transform({ carta, processor }) {
|
|
||||||
processor.use(tikzTransformer, { carta, options });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
listeners: [['carta-render', (e) => generateTikzImages(e, options)]],
|
listeners: [['carta-render', (e) => generateTikzImages(e, options)]]
|
||||||
grammarRules: [
|
|
||||||
{
|
|
||||||
name: 'tikz',
|
|
||||||
type: 'block',
|
|
||||||
definition: {
|
|
||||||
begin: '(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tikz)((\\s+|:|,|\\{|\\?)[^`]*)?$)',
|
|
||||||
beginCaptures: {
|
|
||||||
'3': { name: 'punctuation.definition.markdown' },
|
|
||||||
'4': { name: 'fenced_code.block.language.markdown' },
|
|
||||||
'5': { name: 'fenced_code.block.language.attributes.markdown' }
|
|
||||||
},
|
|
||||||
end: '(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$',
|
|
||||||
endCaptures: { '3': { name: 'punctuation.definition.markdown' } },
|
|
||||||
name: 'markup.fenced_code.block.markdown',
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
begin: '(^|\\G)(\\s*)(.*)',
|
|
||||||
contentName: 'meta.embedded.block.latex',
|
|
||||||
patterns: [{ include: 'text.tex.latex' }],
|
|
||||||
while: '(^|\\G)(?!\\s*([`~]{3,})\\s*$)'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keeps track of tikz generation to remove previous items
|
// Keeps track of tikz generation to remove previous items
|
||||||
let currentGeneration = 0;
|
let currentGeneration = 0;
|
||||||
|
|
||||||
const tikzTransformer: UnifiedPlugin<
|
const tikzTokenizer = (options?: TikzExtensionOptions): TokenizerAndRendererExtension => {
|
||||||
[{ carta: Carta; options: TikzExtensionOptions | undefined }],
|
return {
|
||||||
hast.Root
|
name: 'tikz',
|
||||||
> = ({ carta, options }) => {
|
level: 'block',
|
||||||
return async function (tree) {
|
start: (src) => src.indexOf('\n```tikz'),
|
||||||
visit(tree, (pre, index, parent) => {
|
tokenizer: (src) => {
|
||||||
|
const match = src.match(/^```tikz+\n([^`]+?)\n```+\n/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'tikz',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[1].trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer: (token) => {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
// Cannot run outside the browser
|
// Cannot run outside the browser
|
||||||
return;
|
return ``;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pre.type !== 'element') return;
|
|
||||||
const preElement = pre as hast.Element;
|
|
||||||
if (preElement.tagName !== 'pre') return;
|
|
||||||
const element = pre.children.at(0) as hast.Element | undefined;
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
if (element.tagName !== 'code') return;
|
|
||||||
if (!element.properties['className']) return;
|
|
||||||
if (!(element.properties['className'] as string[]).includes('language-tikz')) return;
|
|
||||||
|
|
||||||
// Element is a TikZ code block
|
|
||||||
const source = tidyTikzSource((element.children[0] as hast.Text).value as string);
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const template = document.createElement('div');
|
const template = document.createElement('div');
|
||||||
const text = document.createTextNode(source);
|
|
||||||
|
|
||||||
container.classList.add('tikz-generated');
|
|
||||||
container.setAttribute('tikz-generation', currentGeneration.toString());
|
|
||||||
if (options?.center ?? true) container.setAttribute('align', 'center');
|
|
||||||
if (options?.class) container.classList.add(...options.class.split(' '));
|
|
||||||
|
|
||||||
|
const center = options?.center ?? true;
|
||||||
template.setAttribute('type', 'tikzjax');
|
template.setAttribute('type', 'tikzjax');
|
||||||
if (options?.debug) template.setAttribute('data-show-console', 'true');
|
if (options?.debug) template.setAttribute('data-show-console', 'true');
|
||||||
|
|
||||||
|
const text = document.createTextNode(
|
||||||
|
tidyTikzSource(token.raw.slice(8, token.raw.length - 4))
|
||||||
|
);
|
||||||
template.appendChild(text);
|
template.appendChild(text);
|
||||||
|
|
||||||
const hash = md5(JSON.stringify(template.dataset) + text.nodeValue);
|
// Try accessing cached HTML
|
||||||
let savedSvg = window.localStorage.getItem(hash);
|
const hash = md5(JSON.stringify(template.dataset) + template.childNodes[0].nodeValue);
|
||||||
|
const savedSvg = window.localStorage.getItem(hash);
|
||||||
|
|
||||||
|
let html: string;
|
||||||
if (savedSvg) {
|
if (savedSvg) {
|
||||||
if (options?.postProcessing) savedSvg = options.postProcessing(savedSvg);
|
html = savedSvg;
|
||||||
container.innerHTML = savedSvg;
|
if (options?.postProcessing) html = options.postProcessing(html);
|
||||||
} else {
|
} else {
|
||||||
container.appendChild(template);
|
html = template.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (carta.sanitizer) {
|
const sanitizer = carta.options?.sanitizer;
|
||||||
container.innerHTML = carta.sanitizer(container.innerHTML);
|
if (sanitizer) html = sanitizer(html);
|
||||||
}
|
|
||||||
|
|
||||||
const hastNode = fromDom(container) as hast.Element;
|
return `
|
||||||
|
<div
|
||||||
parent?.children.splice(index!, 1, hastNode);
|
${center ? 'align="center"' : ''}
|
||||||
|
class="tikz-generated ${options?.class ?? ''}"
|
||||||
return [SKIP, index!];
|
tikz-generation="${currentGeneration}"
|
||||||
});
|
>
|
||||||
|
${html}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -148,7 +123,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTikzImages(e: Event, options?: TikzExtensionOptions) {
|
function generateTikzImages(e: CartaEvent, options?: TikzExtensionOptions) {
|
||||||
const carta = e.detail.carta;
|
const carta = e.detail.carta;
|
||||||
const container = carta.renderer?.container;
|
const container = carta.renderer?.container;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
|
@ -175,7 +150,7 @@ async function loadTikz(options?: TikzExtensionOptions) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const tikzjax = (await import('./assets/tikzjax.js')).default;
|
const tikzjax = (await import('./assets/tikzjax.js')).default;
|
||||||
|
|
||||||
const script = /* html */ `<script type="text/javascript" id="tikzjax">${tikzjax}</script>`;
|
const script = `<script type="text/javascript" id="tikzjax">${tikzjax}</script>`;
|
||||||
|
|
||||||
// Simply appending the element does not work as the script is not executed
|
// Simply appending the element does not work as the script is not executed
|
||||||
// By doing the following we ensure that it is run.
|
// By doing the following we ensure that it is run.
|
||||||
|
@ -186,6 +161,9 @@ async function loadTikz(options?: TikzExtensionOptions) {
|
||||||
|
|
||||||
document.addEventListener('tikzjax-load-finished', (e) => {
|
document.addEventListener('tikzjax-load-finished', (e) => {
|
||||||
const elem = e.target as SVGElement;
|
const elem = e.target as SVGElement;
|
||||||
|
// Support old version
|
||||||
|
options?.postProcess && options.postProcess(elem);
|
||||||
|
|
||||||
if (options?.postProcessing) elem.outerHTML = options.postProcessing(elem.outerHTML);
|
if (options?.postProcessing) elem.outerHTML = options.postProcessing(elem.outerHTML);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
26
packages/plugin-tikz/src/tikz.ts
Normal file
26
packages/plugin-tikz/src/tikz.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
match: /\\(usepackage|input|usemodule)(?![a-zA-Z0-9])/g,
|
||||||
|
type: 'str'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /\\(begin|end|node)(?![a-zA-Z0-9])/g,
|
||||||
|
type: 'class'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /\\[a-zA-Z0-9]+/g,
|
||||||
|
type: 'oper'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /%.+$/gm,
|
||||||
|
type: 'cmnt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(\(|\)|\{|\}|\[|\])/g,
|
||||||
|
type: 'esc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /[0-9]+[a-z]{0,3}/g,
|
||||||
|
type: 'num'
|
||||||
|
}
|
||||||
|
];
|
912
pnpm-lock.yaml
generated
912
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue