Compare commits
42 commits
@cartamd/p
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
2177545454 | ||
|
a303834127 | ||
|
3fd18a6796 | ||
|
486a885704 | ||
|
8e5803a1c4 | ||
|
0ff9e5b124 | ||
|
eb114a7311 | ||
|
34d4f400e6 | ||
|
cc79b25288 | ||
|
a44b8e971d | ||
|
bafc86d8d3 | ||
|
a086ece088 | ||
|
dddaad1f1b | ||
|
7261d295e2 | ||
|
482fd4b8af | ||
|
dfc8812123 | ||
|
54358b649b | ||
|
7ad874e6aa | ||
|
545864a202 | ||
|
0cef0b94ed | ||
|
20d58a856b | ||
|
1a61f58e7d | ||
|
7d4034c316 | ||
|
26241b5bcd | ||
|
3785fd01c0 | ||
|
c9be45a1ee | ||
|
c4428b8150 | ||
|
036af273f9 | ||
|
58a7a8848f | ||
|
255913bd35 | ||
|
e175e87bcc | ||
|
7e4a387523 | ||
|
342c8d24d0 | ||
|
9e1b0d1850 | ||
|
b30b2af6d9 | ||
|
c6a81acedd | ||
|
d1590ea384 | ||
|
24be57a10f | ||
|
eb647b416f | ||
|
ae15b5b590 | ||
|
6e9cb68141 | ||
|
f0abf195b8 |
101 changed files with 5337 additions and 1291 deletions
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
@ -5,11 +5,15 @@
|
||||||
"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"
|
||||||
|
@ -17,5 +21,6 @@
|
||||||
"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
Normal file
55
.vscode/tailwind.json
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"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,31 +1,25 @@
|
||||||
<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=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
|
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&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=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
|
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&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=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
|
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&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=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
|
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
[](https://beartocode.github.io/carta/)
|
||||||
<a href="https://beartocode.github.io/carta/">
|
|
||||||
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
<h1 align="center"><strong>Carta</strong></h1>
|
||||||
|
<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>
|
||||||
|
@ -34,24 +28,30 @@
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
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.
|
> [!NOTE]
|
||||||
Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
|
> Carta has recently been updated to `v4`, which features numerous major changes.
|
||||||
|
>
|
||||||
|
> 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
|
||||||
|
|
||||||
- Keyboard **shortcuts** (extensible);
|
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
|
||||||
- Toolbar (extensible);
|
- 🛠️ Toolbar (extensible);
|
||||||
- Markdown syntax highlighting;
|
- ⌨️ Keyboard **shortcuts** (extensible);
|
||||||
- Scroll sync;
|
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark;
|
||||||
- Accessibility friendly;
|
- 🔀 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);
|
||||||
- **Anchor** links in headings;
|
- 📂 **Attachment** support (plugin);
|
||||||
- Code blocks **syntax highlighting** (plugin).
|
- ⚓ **Anchor** links in headings (plugin);
|
||||||
|
- 🌈 Code blocks **syntax highlighting** (plugin).
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ Differently from most editors, Carta includes neither ProseMirror nor CodeMirror
|
||||||
> [!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
|
||||||
|
|
||||||
|
@ -99,11 +100,9 @@ npm i @cartamd/plugin-name
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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
|
||||||
|
@ -111,13 +110,14 @@ npm i @cartamd/plugin-name
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {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.10",
|
"katex": "^0.16.10",
|
||||||
"radix-icons-svelte": "^1.2.1",
|
|
||||||
"tailwind-merge": "^2.0.0"
|
"tailwind-merge": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Copy } from 'radix-icons-svelte';
|
|
||||||
|
|
||||||
let elem: HTMLElement;
|
let elem: HTMLElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -13,10 +11,10 @@
|
||||||
navigator.clipboard.writeText(elem.innerText);
|
navigator.clipboard.writeText(elem.innerText);
|
||||||
}}
|
}}
|
||||||
class="
|
class="
|
||||||
absolute right-4 top-[min(50%_,_32px)] -translate-y-1/2 transform
|
absolute right-4 top-[min(50%_,_32px)] aspect-square -translate-y-1/2 transform
|
||||||
rounded p-2 hover:bg-neutral-800 hover:text-neutral-300 active:text-sky-300
|
rounded hover:bg-neutral-800 hover:text-neutral-300 active:text-sky-300
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Copy class="h-5 w-5" />
|
<iconify-icon icon="octicon:copy-16" class="p-2 text-lg"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onNavigate } from '$app/navigation';
|
||||||
|
import { debounce, throttle } from '$lib/utils';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const PADDING = 80;
|
const PADDING = 80;
|
||||||
|
|
||||||
|
@ -7,49 +9,58 @@
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
let headers: HTMLElement[] = [];
|
let headers: HTMLElement[] = [];
|
||||||
let scrollY = 0;
|
let selectedHeaderIndex = 0;
|
||||||
|
|
||||||
function retrieveHeaders() {
|
function retrieveHeaders() {
|
||||||
const markdownContainer = document.querySelector('.markdown');
|
headers = Array.from(
|
||||||
if (!markdownContainer) return;
|
document.querySelectorAll('.markdown > h1, .markdown > h2, .markdown > h3')
|
||||||
headers = Array.from(markdownContainer.querySelectorAll('h1, h2, h3')) as HTMLElement[];
|
) as HTMLElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightHeader(header: HTMLElement, nextHeader: HTMLElement | null, index: number) {
|
const highlightHeader = () => {
|
||||||
const headerHasReachedTop = header.getBoundingClientRect().top <= PADDING || index == 0;
|
for (let index = headers.length - 1; index >= 0; index--) {
|
||||||
const nextHeaderReachedTop = nextHeader && nextHeader.getBoundingClientRect().top <= PADDING;
|
const header = headers[index];
|
||||||
return !nextHeaderReachedTop && headerHasReachedTop;
|
const rect = header.getBoundingClientRect();
|
||||||
}
|
if (rect.top < PADDING) {
|
||||||
|
selectedHeaderIndex = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedHeaderIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
let observer: MutationObserver;
|
const [throttledHighlightHeader] = throttle(highlightHeader, 100);
|
||||||
|
const debouncedHighlightHeader = debounce(highlightHeader, 100);
|
||||||
|
|
||||||
onMount(() => {
|
onNavigate(() => {
|
||||||
observer = new MutationObserver(retrieveHeaders);
|
setTimeout(() => {
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
retrieveHeaders();
|
||||||
retrieveHeaders();
|
highlightHeader();
|
||||||
});
|
}, 300);
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
observer?.disconnect();
|
|
||||||
});
|
});
|
||||||
|
onMount(retrieveHeaders);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:scrollY />
|
<svelte:window
|
||||||
|
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}
|
||||||
{@const nextHeader = headers[i + 1]}
|
{#key selectedHeaderIndex}
|
||||||
{#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 {highlightHeader(header, nextHeader, i)
|
class="block text-sm {selectedHeaderIndex === 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
|
||||||
>
|
>
|
||||||
{/key}
|
{/if}
|
||||||
{/if}
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
<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 inline-flex items-center space-x-3">
|
<div class="plugin-link mb-2 mt-6 flex items-end space-x-3">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<a href={githubLink}>
|
<a href={githubLink} class="flex aspect-square">
|
||||||
<GithubLogo class="h-6 w-6 text-white hover:text-sky-300" />
|
<iconify-icon icon="mdi:github" class="text-3xl text-white hover:text-sky-300"></iconify-icon>
|
||||||
</a>
|
</a>
|
||||||
<a href={npmLink}>
|
<a href={npmLink} class="flex aspect-square">
|
||||||
<Archive class="h-6 w-6 text-white hover:text-sky-300" />
|
<iconify-icon icon="gg:npm" class="text-3xl text-white hover:text-sky-300"></iconify-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<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';
|
||||||
|
|
||||||
|
@ -16,7 +15,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">
|
||||||
<HamburgerMenu class="h-7 w-7" />
|
<iconify-icon icon="ci:hamburger-lg" class="text-3xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -29,7 +28,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"
|
||||||
>
|
>
|
||||||
<Cross1 class="h-4 w-4" />
|
<iconify-icon icon="charm:cross" class="text-2xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<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 };
|
||||||
|
@ -19,15 +18,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}"
|
||||||
>
|
>
|
||||||
<GithubLogo class="h-5 w-5" />
|
<iconify-icon icon="mdi:github" class="text-2xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Star class="h-3 w-3" />
|
<iconify-icon icon="ic:round-star" class="h-3 w-3"></iconify-icon>
|
||||||
<span class="text-[0.8rem] leading-3">{stars}</span>
|
<span class="mt-1 text-[0.8rem] leading-3">{stars}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<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,
|
||||||
|
@ -54,8 +53,8 @@
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
<button on:click={() => (open = !open)} class="mr-2 block md:hidden">
|
<button on:click={() => (open = !open)} class="mr-2 block aspect-square md:hidden">
|
||||||
<MagnifyingGlass class="h-7 w-7 text-neutral-200" />
|
<iconify-icon icon="ion:search" class="text-2xl text-neutral-200"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -63,7 +62,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">
|
||||||
<MagnifyingGlass class="h-5 w-5 text-neutral-500" />
|
<iconify-icon icon="ion:search" class="text-xl text-neutral-500"></iconify-icon>
|
||||||
<span class="text-neutral-500">Search...</span>
|
<span class="text-neutral-500">Search...</span>
|
||||||
</div>
|
</div>
|
||||||
<kbd
|
<kbd
|
||||||
|
@ -102,7 +101,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">
|
||||||
<Enter class="h-5 w-5 text-neutral-400" />
|
<iconify-icon icon="mi:enter" class="text-xl text-neutral-400"></iconify-icon>
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,18 +1,4 @@
|
||||||
<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 };
|
||||||
|
@ -25,75 +11,87 @@
|
||||||
|
|
||||||
<!-- Introduction -->
|
<!-- Introduction -->
|
||||||
<SidebarLink href="/introduction">
|
<SidebarLink href="/introduction">
|
||||||
<Dashboard class="h-5 w-5" />
|
<iconify-icon icon="radix-icons:dashboard" class="text-xl"></iconify-icon>
|
||||||
<span class="text-[0.95rem]">Introduction</span>
|
<span class="text-[0.95rem]">Introduction</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<!-- Examples -->
|
<!-- Examples -->
|
||||||
<SidebarLink href="/examples">
|
<SidebarLink href="/examples">
|
||||||
<CodesandboxLogo class="h-5 w-5" />
|
<iconify-icon icon="ph:codesandbox-logo" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Download class="h-5 w-5" />
|
<iconify-icon icon="ic:round-download" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<FontStyle class="h-5 w-5" />
|
<iconify-icon icon="lucide:palette" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Stack class="h-5 w-5" />
|
<iconify-icon icon="ph:stack-fill" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<FontFamily class="h-5 w-5" />
|
<iconify-icon icon="tabler:math" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Code class="h-5 w-5" />
|
<iconify-icon icon="fluent:code-16-filled" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Face class="h-5 w-5" />
|
<iconify-icon icon="mingcute:emoji-line" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Slash class="h-5 w-5" />
|
<iconify-icon icon="tabler:slash" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Cube class="h-5 w-5" />
|
<iconify-icon icon="mdi:draw-pen" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<File class="h-5 w-5" />
|
<iconify-icon icon="tdesign:attach" class="text-xl"></iconify-icon>
|
||||||
<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">
|
||||||
<Link2 class="h-5 w-5" />
|
<iconify-icon icon="mingcute:link-fill" class="text-xl"></iconify-icon>
|
||||||
<span class="text-[0.95rem]">Anchor</span>
|
<span class="text-[0.95rem]">Anchor</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<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;
|
||||||
|
@ -11,7 +10,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="">
|
||||||
<MagnifyingGlass class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<iconify-icon icon="ion:search" class="mr-2 shrink-0 text-xl opacity-50"></iconify-icon>
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
bind:value
|
bind:value
|
||||||
class={cn(
|
class={cn(
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
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;
|
||||||
|
|
||||||
|
@ -27,9 +26,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-4 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-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"
|
||||||
>
|
>
|
||||||
<Cross2 class="h-4 w-4" />
|
<iconify-icon icon="basil:cross-solid" class="text-2xl"></iconify-icon>
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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 'radix-icons-svelte';
|
import PlusCircled from './assets/PlusIcon.svelte';
|
||||||
|
|
||||||
|
import '$lib/styles/discord.scss';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
sanitizer: false,
|
||||||
disableIcons: true,
|
disableIcons: true,
|
||||||
extensions: [
|
extensions: [
|
||||||
emoji(),
|
emoji(),
|
||||||
|
@ -15,8 +16,8 @@
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
component: PlusCircled,
|
component: PlusCircled,
|
||||||
props: { class: 'discord-plus-icon' },
|
parent: 'input',
|
||||||
parent: 'input'
|
props: {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -24,5 +25,4 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor placeholder="Send a message to @someone" mode="tabs" theme="discord" {carta}
|
<MarkdownEditor placeholder="Send a message to @someone" mode="tabs" theme="discord" {carta} />
|
||||||
></CartaEditor>
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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() {
|
||||||
|
@ -20,7 +21,10 @@
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
<CartaEditor bind:value mode="tabs" theme="github" {carta} />
|
<MarkdownEditor bind:value mode="tabs" theme="github" {carta} />
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor, CartaViewer } from 'carta-md';
|
import { Carta, MarkdownEditor, Markdown } 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({
|
||||||
|
@ -30,9 +31,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="math-stack-exchange-container">
|
<div class="math-stack-exchange-container">
|
||||||
<CartaEditor bind:value mode="tabs" theme="math-stack-exchange" {carta} />
|
<MarkdownEditor bind:value mode="tabs" theme="math-stack-exchange" {carta} />
|
||||||
|
|
||||||
{#key value}
|
{#key value}
|
||||||
<CartaViewer theme="math-stack-exchange" {value} {carta} />
|
<Markdown theme="math-stack-exchange" {value} {carta} />
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
3
docs/src/lib/examples/assets/PlusIcon.svelte
Normal file
3
docs/src/lib/examples/assets/PlusIcon.svelte
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="discord-plus-icon">
|
||||||
|
<iconify-icon icon="radix-icons:plus-circled" class="text-2xl"></iconify-icon>
|
||||||
|
</div>
|
|
@ -27,6 +27,7 @@
|
||||||
.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 {
|
||||||
|
@ -41,10 +42,6 @@
|
||||||
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
|
||||||
|
@ -79,3 +76,8 @@
|
||||||
background: $background-contrast;
|
background: $background-contrast;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.dark .shiki,
|
||||||
|
html.dark .shiki span {
|
||||||
|
color: var(--shiki-dark) !important;
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
.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 {
|
||||||
|
@ -115,10 +116,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*='shj-lang-'] {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin emoji
|
// Plugin emoji
|
||||||
|
@ -199,3 +196,8 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.dark .shiki,
|
||||||
|
html.dark .shiki span {
|
||||||
|
color: var(--shiki-dark) !important;
|
||||||
|
}
|
||||||
|
|
|
@ -43,9 +43,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@apply rounded;
|
@apply rounded bg-neutral-800 px-1 text-neutral-50;
|
||||||
&: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,6 +34,7 @@
|
||||||
.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 {
|
||||||
|
@ -64,10 +65,6 @@
|
||||||
.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 {
|
||||||
|
@ -104,3 +101,8 @@
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.dark .shiki,
|
||||||
|
html.dark .shiki span {
|
||||||
|
color: var(--shiki-dark) !important;
|
||||||
|
}
|
||||||
|
|
|
@ -54,3 +54,41 @@ 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,9 +17,15 @@ new Carta({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `gfmOptions`
|
||||||
|
|
||||||
|
Type: `GfmOptions`
|
||||||
|
|
||||||
|
GitHub Flavored Markdown options.
|
||||||
|
|
||||||
### `extensions`
|
### `extensions`
|
||||||
|
|
||||||
Type: `CartaExtension[]`
|
Type: `Extension[]`
|
||||||
|
|
||||||
List of extensions(plugins) to use.
|
List of extensions(plugins) to use.
|
||||||
|
|
||||||
|
@ -72,9 +78,21 @@ 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.
|
||||||
|
|
||||||
# `CartaEditor` options
|
### `shikiOptions`
|
||||||
|
|
||||||
List of options that can be used in the `<CartaEditor>` component.
|
Type: `ShikiOptions`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
@ -128,13 +146,13 @@ instead.
|
||||||
|
|
||||||
### `labels`
|
### `labels`
|
||||||
|
|
||||||
Type: `Partial<CartaLabels>`
|
Type: `Partial<Labels>`
|
||||||
|
|
||||||
Can be used to provide custom text for labels in the editor.
|
Can be used to provide custom text for labels in the editor.
|
||||||
|
|
||||||
# `CartaViewer` options
|
# `Markdown` options
|
||||||
|
|
||||||
List of options that can be used in the `<CartaViewer>` component.
|
List of options that can be used in the `<Markdown>` 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>
|
||||||
|
|
||||||
# `CartaExtension` properties
|
# `Plugin` 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: CartaExtension = {
|
const ext: Plugin = {
|
||||||
// ...
|
// ...
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,11 +25,47 @@ const carta = new Carta({
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
Here are all the `CartaExtension` properties:
|
Here are all the `Plugin` properties:
|
||||||
|
|
||||||
### `markedExtensions`
|
### `transformers`
|
||||||
|
|
||||||
List of marked extensions. For more information check out [Marked docs](https://marked.js.org/using_pro).
|
Type: `UnifiedTransformer`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
@ -63,7 +99,7 @@ Set of keys, corresponding to the `e.key` of `KeyboardEvent`s, but lowercase.
|
||||||
|
|
||||||
#### `KeyboardShortcut.action`
|
#### `KeyboardShortcut.action`
|
||||||
|
|
||||||
Type: `(input: CartaInput) => void`
|
Type: `(input: InputEnhancer) => void`
|
||||||
|
|
||||||
Shortcut callback.
|
Shortcut callback.
|
||||||
|
|
||||||
|
@ -73,14 +109,14 @@ Prevent saving the current state in history.
|
||||||
|
|
||||||
### `icons`
|
### `icons`
|
||||||
|
|
||||||
Type: `CartaIcon[]`
|
Type: `Icon[]`
|
||||||
|
|
||||||
Additional toolbar icons. For example:
|
Additional toolbar icons. For example:
|
||||||
|
|
||||||
<Code>
|
<Code>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const icon: CartaIcon = {
|
const icon: Icon = {
|
||||||
id: 'heading',
|
id: 'heading',
|
||||||
action: (input) => input.toggleLinePrefix('###'),
|
action: (input) => input.toggleLinePrefix('###'),
|
||||||
component: HeadingIcon
|
component: HeadingIcon
|
||||||
|
@ -89,19 +125,19 @@ const icon: CartaIcon = {
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
#### `CartaIcon.id`
|
#### `Icon.id`
|
||||||
|
|
||||||
Type: `string`
|
Type: `string`
|
||||||
|
|
||||||
Id of the icon.
|
Id of the icon.
|
||||||
|
|
||||||
#### `CartaIcon.action`
|
#### `Icon.action`
|
||||||
|
|
||||||
Type: `(input: CartaInput) => void`
|
Type: `(input: InputEnhancer) => void`
|
||||||
|
|
||||||
Click callback.
|
Click callback.
|
||||||
|
|
||||||
#### `CartaIcon.component`
|
#### `Icon.component`
|
||||||
|
|
||||||
Type: `ComponentType` (SvelteComponent)
|
Type: `ComponentType` (SvelteComponent)
|
||||||
|
|
||||||
|
@ -162,15 +198,15 @@ const prefix: Prefix = {
|
||||||
|
|
||||||
### `listeners`
|
### `listeners`
|
||||||
|
|
||||||
Type: `CartaListener[]`
|
Type: `Listener[]`
|
||||||
|
|
||||||
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: CartaListener = ['click', () => console.log('I was clicked!')];
|
const click: Listener = ['click', () => console.log('I was clicked!')];
|
||||||
const render: CartaListener = [
|
const render: Listener = [
|
||||||
'carta-render',
|
'carta-render',
|
||||||
(e) => {
|
(e) => {
|
||||||
const carta = e.detail.carta;
|
const carta = e.detail.carta;
|
||||||
|
@ -186,33 +222,39 @@ const render: CartaListener = [
|
||||||
|
|
||||||
### `components`
|
### `components`
|
||||||
|
|
||||||
Type: `CartaExtensionComponent[]`
|
Type: `ExtensionComponent[]`
|
||||||
|
|
||||||
Additional components to be added to the editor or viewer.
|
Additional components to be added to the editor or viewer.
|
||||||
|
|
||||||
#### `CartaExtensionComponent<T>.component`
|
#### `ExtensionComponent<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`.
|
||||||
|
|
||||||
#### `CartaExtensionComponent<T>.props`
|
#### `ExtensionComponent<T>.props`
|
||||||
|
|
||||||
Type: `T`
|
Type: `T`
|
||||||
|
|
||||||
Properties that will be handed to the component.
|
Properties that will be handed to the component.
|
||||||
|
|
||||||
#### `CartaExtensionComponent<T>.parent`
|
#### `ExtensionComponent<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: `ShjLanguageDefinition`
|
Type: `HighlightingRule[]`
|
||||||
|
|
||||||
Custom markdown highlighting rules. See [Speed-Highlight Wiki](https://github.com/speed-highlight/core/wiki/Create-or-suggest-new-languages) for more info.
|
Custom highlighting rules for ShiKi. They will be injected into the selected theme.
|
||||||
|
|
||||||
### `onLoad`
|
### `onLoad`
|
||||||
|
|
||||||
|
|
|
@ -40,3 +40,52 @@ 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,11 +43,68 @@ 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 the editor you can differentiate the themes of multiple editors.
|
By using the `theme` property in `<MarkdownEditor>` you can differentiate the themes of multiple editors.
|
||||||
|
|
||||||
## Changing Markdown color theme
|
## Dark mode
|
||||||
|
|
||||||
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.
|
When using dark mode, there are two different themes that have to be changed: the editor theme and the one used for syntax highlighting:
|
||||||
|
|
||||||
|
<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,9 +37,8 @@ Setup a basic editor:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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!
|
||||||
|
@ -50,12 +49,13 @@ Setup a basic editor:
|
||||||
let value = '';
|
let value = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} bind:value />
|
<MarkdownEditor {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, CartaViewer } from 'carta-md';
|
import { Carta, Markdown } 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>
|
||||||
|
|
||||||
<CartaViewer {carta} {value} />
|
<Markdown {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>
|
||||||
|
|
||||||
<CartaEditor {carta} bind:value />
|
<MarkdownEditor {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>
|
||||||
|
|
||||||
> Swiftly edit and render Markdown, with no overhead.
|
> Modern, lightweight, powerful Markdown Editor.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
- **Lightweight**: no code editor is included, just a textarea with syntax highlighting, with Markdown related utilities.
|
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
|
||||||
- **SSR compatible**: works great with SvelteKit.
|
- 🛠️ Toolbar (extensible);
|
||||||
- **Keyboard shortcuts**: extensible and configurable.
|
- ⌨️ Keyboard **shortcuts** (extensible);
|
||||||
- **Toolbar**: add or remove buttons according to your needs.
|
- 📦 Supports **[+150 plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark.
|
||||||
- **Plugins friendly**: easily create your own extension.
|
- 🔀 Scroll sync;
|
||||||
- **Accessibility**: includes ARIA roles, arrow keys navigation and labels.
|
- ✅ Accessibility friendly;
|
||||||
|
- 🖥️ **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>
|
||||||
<Icon.FontFamily class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="tabler:math" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.Code class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="fluent:code-16-filled" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.Face class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="mingcute:emoji-line" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.Slash class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="tabler:slash" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.Cube class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="mdi:draw-pen" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.File class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="tdesign:attach" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.Link2 class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="mingcute:link-fill" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.Stack class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="ph:stack-fill" class="text-3xl text-sky-300"></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.GithubLogo class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="mdi:github" class="text-3xl text-sky-300" ></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.DiscordLogo class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="ic:baseline-discord" class="text-3xl text-sky-300" ></iconify-icon>
|
||||||
<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>
|
||||||
<Icon.Cube class="w-8 h-8 text-sky-300" />
|
<iconify-icon icon="fluent:math-formula-16-filled" class="text-3xl text-sky-300" ></iconify-icon>
|
||||||
<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>
|
||||||
|
|
57
docs/src/pages/migration.svelte.md
Normal file
57
docs/src/pages/migration.svelte.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
@ -59,8 +59,12 @@ Here are the options you can pass to `anchor()`:
|
||||||
```ts
|
```ts
|
||||||
export interface AnchorExtensionOptions {
|
export interface AnchorExtensionOptions {
|
||||||
/**
|
/**
|
||||||
* Maximum depth of headers to generate anchors for. Defaults to 6.
|
* rehype-slug options.
|
||||||
*/
|
*/
|
||||||
maxDepth?: number;
|
slug?: SlugOptions;
|
||||||
|
/**
|
||||||
|
* rehype-autolink-headings options.
|
||||||
|
*/
|
||||||
|
autolink?: AutolinkOptions;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -35,7 +35,7 @@ import '@cartamd/plugin-attachment/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
|
@ -7,8 +7,7 @@ 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**.
|
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
|
||||||
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
|
||||||
|
|
||||||
|
@ -36,34 +35,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.
|
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.
|
||||||
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
|
||||||
code({
|
const carta = new Carta({
|
||||||
customHighlight: {
|
// ...
|
||||||
highlighter: (code, lang) => myCustomHighlighter(code, lang),
|
extensions: [
|
||||||
langPrefix: 'my-highlighter-'
|
code({
|
||||||
}
|
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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { code } from '@cartamd/plugin-code';
|
import { code } from '@cartamd/plugin-code';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -71,46 +70,11 @@ code({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
Here are the options you can pass to `code()`:
|
The options you can pass to `code()` extend the ones provided by [Shiki](https://shiki.matsu.io/guide/transformers).
|
||||||
|
|
||||||
```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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {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 { CartaViewer, Carta } from 'carta-md';
|
import { Markdown, 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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
@ -103,7 +103,7 @@ Pythagorean theorem: $a^2+b^2=c^2$
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
<CartaViewer {carta} value={inline} />
|
<Markdown {carta} value={inline} />
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ $$
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
||||||
<CartaViewer {carta} value={block} />
|
<Markdown {carta} value={block} />
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
@ -131,7 +131,6 @@ 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
|
||||||
*/
|
*/
|
||||||
|
@ -144,23 +143,18 @@ 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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
</Code>
|
</Code>
|
||||||
|
|
217
docs/src/pages/using-components.svelte.md
Normal file
217
docs/src/pages/using-components.svelte.md
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
---
|
||||||
|
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 '../app.postcss';
|
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
import '../app.postcss';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
@ -24,6 +24,6 @@
|
||||||
<slot />
|
<slot />
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
<HeaderTracker class="sticky top-24 hidden w-[30rem] xl:block" />
|
<HeaderTracker class="sticky top-24 hidden w-[15rem] flex-shrink-0 xl:block" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
|
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,31 +1,25 @@
|
||||||
<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=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
|
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&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=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
|
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&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=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
|
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&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=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
|
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
[](https://beartocode.github.io/carta/)
|
||||||
<a href="https://beartocode.github.io/carta/">
|
|
||||||
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
<h1 align="center"><strong>Carta</strong></h1>
|
||||||
|
<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>
|
||||||
|
@ -34,38 +28,81 @@
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
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.
|
> **NOTE**:
|
||||||
Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
|
> Carta has recently been updated to `v4`, which features numerous major changes.
|
||||||
|
>
|
||||||
|
> 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
|
||||||
|
|
||||||
- Keyboard **shortcuts** (extensible);
|
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
|
||||||
- Toolbar (extensible);
|
- 🛠️ Toolbar (extensible);
|
||||||
- Markdown syntax highlighting;
|
- ⌨️ Keyboard **shortcuts** (extensible);
|
||||||
- Scroll sync;
|
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark;
|
||||||
- **SSR** compatible;
|
- 🔀 Scroll sync;
|
||||||
- **Katex** support (plugin);
|
- ✅ Accessibility friendly;
|
||||||
- **Slash** commands (plugin);
|
- 🖥️ **SSR** compatible;
|
||||||
- **Emojis**, with included search (plugin);
|
- ⚗️ **KaTeX** support (plugin);
|
||||||
- **Tikz** support(plugin);
|
- 🔨 **Slash** commands (plugin);
|
||||||
- **Attachment** support(plugin);
|
- 😄 **Emojis**, with included search (plugin);
|
||||||
- Code blocks **syntax highlighting** (plugin).
|
- ✏️ **TikZ** support (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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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
|
||||||
|
@ -73,13 +110,14 @@ Differently from most editors, Carta includes neither ProseMirror nor CodeMirror
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {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>
|
||||||
```
|
```
|
||||||
|
@ -99,7 +137,33 @@ 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,10 +14,7 @@
|
||||||
"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": {
|
||||||
|
@ -38,8 +35,12 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@speed-highlight/core": "1.2.2",
|
"rehype-stringify": "^10.0.0",
|
||||||
"marked": "^9.1.5"
|
"remark-gfm": "^4.0.0",
|
||||||
|
"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,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { Carta } from './';
|
import { loadNestedLanguages, type Carta } from '.';
|
||||||
|
|
||||||
export let carta: Carta;
|
export let carta: Carta;
|
||||||
export let value: string;
|
export let value: string;
|
||||||
|
@ -12,7 +12,12 @@
|
||||||
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;
|
|
@ -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 CartaRenderer from './internal/components/CartaRenderer.svelte';
|
import Renderer from './internal/components/Renderer.svelte';
|
||||||
import MarkdownInput from './internal/components/MarkdownInput.svelte';
|
import Input from './internal/components/Input.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 { DefaultCartaLabels, type CartaLabels } from './internal/labels';
|
import { defaultLabels, type Labels } 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<CartaLabels> = {};
|
let userLabels: Partial<Labels> = {};
|
||||||
export { userLabels as labels };
|
export { userLabels as labels };
|
||||||
const labels: CartaLabels = {
|
const labels: Labels = {
|
||||||
...DefaultCartaLabels,
|
...defaultLabels,
|
||||||
...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'}
|
||||||
<MarkdownInput
|
<Input
|
||||||
{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}
|
||||||
</MarkdownInput>
|
</Input>
|
||||||
{/if}
|
{/if}
|
||||||
{#if windowMode == 'split' || selectedTab == 'preview'}
|
{#if windowMode == 'split' || selectedTab == 'preview'}
|
||||||
<CartaRenderer {carta} {handleScroll} bind:value bind:elem={rendererElem}>
|
<Renderer {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}
|
||||||
</CartaRenderer>
|
</Renderer>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,41 +0,0 @@
|
||||||
@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,6 +3,15 @@
|
||||||
--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 {
|
||||||
|
@ -29,10 +38,14 @@
|
||||||
|
|
||||||
/* Text settings */
|
/* Text settings */
|
||||||
.carta-theme__default .carta-input {
|
.carta-theme__default .carta-input {
|
||||||
caret-color: #4d4d4c;
|
caret-color: var(--caret-color);
|
||||||
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: '';
|
||||||
|
@ -62,6 +75,10 @@
|
||||||
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 CartaEditor } from '$lib/CartaEditor.svelte';
|
export { default as MarkdownEditor } from '$lib/MarkdownEditor.svelte';
|
||||||
export { default as CartaViewer } from '$lib/CartaViewer.svelte';
|
export { default as Markdown } from '$lib/Markdown.svelte';
|
||||||
export type { CartaInput, TextSelection } from '$lib/internal/input';
|
export type { InputEnhancer, TextSelection } from '$lib/internal/input';
|
||||||
export type { CartaIcon } from '$lib/internal/icons';
|
export type { Icon } 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,4 +9,7 @@ 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';
|
||||||
|
|
1779
packages/carta-md/src/lib/internal/assets/markdown.ts
Normal file
1779
packages/carta-md/src/lib/internal/assets/markdown.ts
Normal file
File diff suppressed because it is too large
Load diff
330
packages/carta-md/src/lib/internal/assets/theme-dark.ts
Normal file
330
packages/carta-md/src/lib/internal/assets/theme-dark.ts
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
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;
|
329
packages/carta-md/src/lib/internal/assets/theme-light.ts
Normal file
329
packages/carta-md/src/lib/internal/assets/theme-light.ts
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
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,53 +1,61 @@
|
||||||
import type { CartaHistoryOptions } from './history';
|
import type { SvelteComponent } from 'svelte';
|
||||||
import type { SvelteComponentTyped } from 'svelte';
|
import { unified, type Processor } from 'unified';
|
||||||
import type { ShjLanguageDefinition } from '@speed-highlight/core/index';
|
import remarkParse from 'remark-parse';
|
||||||
import { Marked, type MarkedExtension } from 'marked';
|
import remarkGfm, { type Options as GfmOptions } from 'remark-gfm';
|
||||||
import { CartaInput } from './input';
|
import remarkRehype from 'remark-rehype';
|
||||||
|
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 CartaIcon, type DefaultIconId } from './icons';
|
import { defaultIcons, type Icon, type DefaultIconId } from './icons';
|
||||||
import { defaultPrefixes, type DefaultPrefixId, type Prefix } from './prefixes';
|
import { defaultPrefixes, type DefaultPrefixId, type Prefix } from './prefixes';
|
||||||
import { CartaRenderer } from './renderer';
|
import { Renderer } from './renderer';
|
||||||
|
import { CustomEvent, type MaybeArray } from './utils';
|
||||||
import {
|
import {
|
||||||
type HighlightFunctions,
|
loadHighlighter,
|
||||||
loadCustomLanguage,
|
loadDefaultTheme,
|
||||||
highlight,
|
type Highlighter,
|
||||||
highlightAutodetect,
|
type GrammarRule,
|
||||||
loadCustomMarkdown
|
type ShikiOptions,
|
||||||
} from './highlight.js';
|
type DualTheme,
|
||||||
import { CustomEvent } from './utils';
|
type Theme,
|
||||||
|
type HighlightingRule
|
||||||
|
} from './highlight';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta-specific event with extra payload.
|
* Carta-specific event with extra payload.
|
||||||
*/
|
*/
|
||||||
export type CartaEvent = CustomEvent<{ carta: Carta }>;
|
export type Event = 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
|
||||||
? CartaEvent
|
? Event
|
||||||
: 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
|
/**
|
||||||
type CartaListeners = CartaListener<any>[];
|
* Custom Svelte component for extensions.
|
||||||
|
*/
|
||||||
type MaybeArray<T> = T | Array<T>;
|
export interface ExtensionComponent<T extends object | undefined> {
|
||||||
|
|
||||||
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 SvelteComponentTyped<T & { carta: Carta }>;
|
component: typeof SvelteComponent<T & { carta: Carta }>;
|
||||||
/**
|
/**
|
||||||
* Properties that will be handed to the component.
|
* Properties that will be handed to the component.
|
||||||
*/
|
*/
|
||||||
|
@ -59,16 +67,22 @@ export interface CartaExtensionComponent<T extends object> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type CartaExtensionComponents = Array<CartaExtensionComponent<any>>;
|
type Listeners = Listener<any>[];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type ExtensionComponents = Array<ExtensionComponent<any>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta editor options.
|
* Carta editor options.
|
||||||
*/
|
*/
|
||||||
export interface CartaOptions {
|
export interface Options {
|
||||||
|
/**
|
||||||
|
* GitHub Flavored Markdown options.
|
||||||
|
*/
|
||||||
|
gfmOptions?: GfmOptions;
|
||||||
/**
|
/**
|
||||||
* Editor/viewer extensions.
|
* Editor/viewer extensions.
|
||||||
*/
|
*/
|
||||||
extensions?: CartaExtension[];
|
extensions?: Plugin[];
|
||||||
/**
|
/**
|
||||||
* Renderer debouncing timeout, in ms.
|
* Renderer debouncing timeout, in ms.
|
||||||
* @defaults 300ms
|
* @defaults 300ms
|
||||||
|
@ -89,21 +103,47 @@ export interface CartaOptions {
|
||||||
/**
|
/**
|
||||||
* History (Undo/Redo) options.
|
* History (Undo/Redo) options.
|
||||||
*/
|
*/
|
||||||
historyOptions?: Partial<CartaHistoryOptions>;
|
historyOptions?: TextAreaHistoryOptions;
|
||||||
/**
|
/**
|
||||||
* HTML sanitizer.
|
* HTML sanitizer.
|
||||||
*/
|
*/
|
||||||
sanitizer?: (html: string) => string;
|
sanitizer: ((html: string) => string) | false;
|
||||||
|
/**
|
||||||
|
* 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 CartaExtension {
|
export interface Plugin {
|
||||||
/**
|
/**
|
||||||
* Marked extensions, more on that [here](https://marked.js.org/using_advanced).
|
* Unified transformers plugins.
|
||||||
|
* @important If the plugin is async, it will not run in SSR rendering.
|
||||||
*/
|
*/
|
||||||
markedExtensions?: MarkedExtension[];
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
transformers?: UnifiedTransformer<'sync' | 'async'>[];
|
||||||
/**
|
/**
|
||||||
* Additional keyboard shortcuts.
|
* Additional keyboard shortcuts.
|
||||||
*/
|
*/
|
||||||
|
@ -111,7 +151,7 @@ export interface CartaExtension {
|
||||||
/**
|
/**
|
||||||
* Additional icons.
|
* Additional icons.
|
||||||
*/
|
*/
|
||||||
icons?: CartaIcon[];
|
icons?: Icon[];
|
||||||
/**
|
/**
|
||||||
* Additional prefixes.
|
* Additional prefixes.
|
||||||
*/
|
*/
|
||||||
|
@ -119,58 +159,78 @@ export interface CartaExtension {
|
||||||
/**
|
/**
|
||||||
* Textarea event listeners.
|
* Textarea event listeners.
|
||||||
*/
|
*/
|
||||||
listeners?: CartaListeners;
|
listeners?: Listeners;
|
||||||
/**
|
/**
|
||||||
* 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?: CartaExtensionComponents;
|
components?: ExtensionComponents;
|
||||||
/**
|
/**
|
||||||
* Custom markdown highlight rules. See [Speed-Highlight Wiki](https://github.com/speed-highlight/core/wiki/Create-or-suggest-new-languages).
|
* Custom markdown grammar highlight rules for ShiKi.
|
||||||
*/
|
*/
|
||||||
highlightRules?: ShjLanguageDefinition;
|
grammarRules?: GrammarRule[];
|
||||||
|
/**
|
||||||
|
* 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; highlight: HighlightFunctions }) => void;
|
onLoad?: (data: { carta: Carta }) => 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: CartaIcon[];
|
public readonly icons: Icon[];
|
||||||
public readonly prefixes: Prefix[];
|
public readonly prefixes: Prefix[];
|
||||||
public readonly highlightRules: ShjLanguageDefinition;
|
public readonly grammarRules: GrammarRule[];
|
||||||
public readonly textareaListeners: CartaListeners;
|
public readonly highlightingRules: HighlightingRule[];
|
||||||
public readonly cartaListeners: CartaListeners;
|
public readonly textareaListeners: Listeners;
|
||||||
public readonly components: CartaExtensionComponents;
|
public readonly cartaListeners: Listeners;
|
||||||
|
public readonly components: ExtensionComponents;
|
||||||
public readonly dispatcher = new EventTarget();
|
public readonly dispatcher = new EventTarget();
|
||||||
public readonly markedAsync = new Marked();
|
public readonly syncProcessor: Processor;
|
||||||
public readonly markedSync = new Marked();
|
public readonly asyncProcessor: Promise<Processor>;
|
||||||
|
|
||||||
|
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._element;
|
return this.mElement;
|
||||||
}
|
}
|
||||||
public get input() {
|
public get input() {
|
||||||
return this._input;
|
return this.mInput;
|
||||||
}
|
}
|
||||||
public get renderer() {
|
public get renderer() {
|
||||||
return this._renderer;
|
return this.mRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
@ -179,23 +239,31 @@ export class Carta {
|
||||||
callback: (() => void) | undefined;
|
callback: (() => void) | undefined;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
public constructor(public readonly options?: CartaOptions) {
|
public constructor(options?: Options) {
|
||||||
|
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.highlightRules = [];
|
this.grammarRules = [];
|
||||||
|
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.highlightRules.push(...(ext.highlightRules ?? []));
|
this.grammarRules.push(...(ext.grammarRules ?? []));
|
||||||
|
this.highlightingRules.push(...(ext.highlightingRules ?? []));
|
||||||
|
|
||||||
listeners.push(...(ext.listeners ?? []));
|
listeners.push(...(ext.listeners ?? []));
|
||||||
}
|
}
|
||||||
|
@ -232,41 +300,82 @@ export class Carta {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load marked extensions
|
// Load unified extensions
|
||||||
const markedExtensions = this.options?.extensions
|
this.mSyncTransformers = [];
|
||||||
?.flatMap((ext) => ext.markedExtensions)
|
this.mAsyncTransformers = [];
|
||||||
.filter((ext) => ext != null) as MarkedExtension[] | undefined;
|
|
||||||
if (markedExtensions)
|
|
||||||
markedExtensions.forEach((ext) => {
|
|
||||||
this.useMarkedExtension(ext);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load highlight custom language
|
for (const ext of options?.extensions ?? []) {
|
||||||
loadCustomMarkdown(this.options?.extensions);
|
for (const transformer of ext.transformers ?? []) {
|
||||||
|
if (transformer.execution === 'sync') {
|
||||||
|
this.mSyncTransformers.push(transformer);
|
||||||
|
} else {
|
||||||
|
this.mAsyncTransformers.push(transformer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const ext of this.options?.extensions ?? []) {
|
this.syncProcessor = this.setupSynchronousProcessor({ gfmOptions: options?.gfmOptions });
|
||||||
ext.cartaRef && ext.cartaRef(this);
|
this.asyncProcessor = this.setupAsynchronousProcessor({ gfmOptions: options?.gfmOptions });
|
||||||
ext.shjRef &&
|
|
||||||
ext.shjRef({
|
for (const ext of options?.extensions ?? []) {
|
||||||
highlight,
|
if (ext.onLoad) {
|
||||||
highlightAutodetect,
|
|
||||||
loadCustomLanguage
|
|
||||||
});
|
|
||||||
ext.onLoad &&
|
|
||||||
ext.onLoad({
|
ext.onLoad({
|
||||||
carta: this,
|
carta: this
|
||||||
highlight: {
|
|
||||||
highlight,
|
|
||||||
highlightAutodetect,
|
|
||||||
loadCustomLanguage
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private useMarkedExtension(exts: MarkedExtension) {
|
private setupSynchronousProcessor({ gfmOptions }: { gfmOptions?: GfmOptions }) {
|
||||||
this.markedAsync.use(exts);
|
const syncProcessor = unified();
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -275,12 +384,13 @@ export class Carta {
|
||||||
* @returns Rendered html.
|
* @returns Rendered html.
|
||||||
*/
|
*/
|
||||||
public async render(markdown: string): Promise<string> {
|
public async render(markdown: string): Promise<string> {
|
||||||
const dirty = await this.markedAsync.parse(markdown, { async: true });
|
const processor = await this.asyncProcessor;
|
||||||
|
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.options?.sanitizer && this.options?.sanitizer(dirty)) ?? dirty;
|
return (this.sanitizer && this.sanitizer(dirty)) ?? dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -289,12 +399,12 @@ export class Carta {
|
||||||
* @returns Rendered html.
|
* @returns Rendered html.
|
||||||
*/
|
*/
|
||||||
public renderSSR(markdown: string): string {
|
public renderSSR(markdown: string): string {
|
||||||
const dirty = this.markedSync.parse(markdown, { async: false });
|
const dirty = String(this.syncProcessor.processSync(markdown));
|
||||||
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.options?.sanitizer) return this.options.sanitizer(dirty);
|
if (this.sanitizer) return this.sanitizer(dirty);
|
||||||
return dirty;
|
return dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,7 +413,7 @@ export class Carta {
|
||||||
* @param element The editor element.
|
* @param element The editor element.
|
||||||
*/
|
*/
|
||||||
public $setElement(element: HTMLDivElement) {
|
public $setElement(element: HTMLDivElement) {
|
||||||
this._element = element;
|
this.mElement = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -315,19 +425,19 @@ export class Carta {
|
||||||
// Remove old listeners if any
|
// Remove old listeners if any
|
||||||
const previousInput = this.input;
|
const previousInput = this.input;
|
||||||
|
|
||||||
this._input = new CartaInput(textarea, container, {
|
this.mInput = new InputEnhancer(textarea, container, {
|
||||||
shortcuts: this.keyboardShortcuts,
|
shortcuts: this.keyboardShortcuts,
|
||||||
prefixes: this.prefixes,
|
prefixes: this.prefixes,
|
||||||
listeners: this.textareaListeners,
|
listeners: this.textareaListeners,
|
||||||
historyOpts: this.options?.historyOptions
|
historyOpts: this.historyOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
if (previousInput) {
|
if (previousInput) {
|
||||||
previousInput.events.removeEventListener('update', callback);
|
previousInput.events.removeEventListener('update', callback);
|
||||||
this._input.history = previousInput.history;
|
this.mInput.history = previousInput.history;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._input.events.addEventListener('update', callback);
|
this.mInput.events.addEventListener('update', callback);
|
||||||
|
|
||||||
// Bind elements
|
// Bind elements
|
||||||
this.elementsToBind.forEach((it) => {
|
this.elementsToBind.forEach((it) => {
|
||||||
|
@ -343,7 +453,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._renderer = new CartaRenderer(container);
|
this.mRenderer = new Renderer(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,14 +490,4 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
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 = '';
|
||||||
|
@ -11,7 +13,7 @@
|
||||||
export let props: TextAreaProps = {};
|
export let props: TextAreaProps = {};
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
let textarea: HTMLTextAreaElement;
|
||||||
let highlighElem: HTMLPreElement;
|
let highlighElem: HTMLDivElement;
|
||||||
let highlighted = value;
|
let highlighted = value;
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
|
@ -36,10 +38,44 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const highlight = async (text: string) => (highlighted = (await carta.highlight(text)) as string);
|
const highlight = async (text: string) => {
|
||||||
$: highlight(value).then(resize);
|
const highlighter = await carta.highlighter();
|
||||||
|
let html: string;
|
||||||
|
|
||||||
onMount(() => (mounted = true));
|
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);
|
||||||
|
$: highlightNestedLanguages(value);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mounted = true;
|
||||||
|
requestAnimationFrame(resize);
|
||||||
|
});
|
||||||
onMount(setInput);
|
onMount(setInput);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -56,11 +92,14 @@
|
||||||
bind:this={elem}
|
bind:this={elem}
|
||||||
>
|
>
|
||||||
<div class="carta-input-wrapper">
|
<div class="carta-input-wrapper">
|
||||||
<pre
|
<div
|
||||||
class="shj-lang-md carta-font-code"
|
class="carta-highlight carta-font-code"
|
||||||
bind:this={highlighElem}
|
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-hidden="true"><!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}</pre>
|
aria-hidden="true"
|
||||||
|
bind:this={highlighElem}
|
||||||
|
>
|
||||||
|
<!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}
|
||||||
|
</div>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
name="md"
|
name="md"
|
||||||
|
@ -110,12 +149,11 @@
|
||||||
color: transparent;
|
color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
font-size: inherit;
|
|
||||||
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
tab-size: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
.carta-highlight {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -133,6 +171,21 @@
|
||||||
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;
|
|
@ -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.options?.rendererDebounce ?? 300);
|
}, carta.rendererDebounce ?? 300);
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// On value updates
|
// On value updates
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { CartaLabels } from '../labels';
|
import type { Labels } 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: CartaLabels;
|
export let labels: Labels;
|
||||||
|
|
||||||
let toolbar: HTMLDivElement;
|
let toolbar: HTMLDivElement;
|
||||||
let menu: HTMLDivElement;
|
let menu: HTMLDivElement;
|
||||||
|
|
|
@ -1,87 +1,268 @@
|
||||||
import { detectLanguage } from '@speed-highlight/core/detect.js';
|
|
||||||
import {
|
import {
|
||||||
highlightText,
|
getHighlighter,
|
||||||
loadLanguage,
|
type BundledTheme,
|
||||||
type ShjLanguage,
|
type ThemeInput,
|
||||||
type ShjLanguageDefinition
|
type StringLiteralUnion,
|
||||||
} from '@speed-highlight/core';
|
type BundledLanguage,
|
||||||
import type { CartaExtension } from './carta';
|
type SpecialLanguage,
|
||||||
import cartaMarkdown from './shj';
|
type LanguageInput,
|
||||||
|
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];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlight text using Speed-Highlight. May return null on error(usually if requested
|
* Custom TextMate highlighting rule for the highlighter.
|
||||||
* 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 async function highlight(
|
export type HighlightingRule = {
|
||||||
text: string,
|
light: NonNullable<ThemeRegistration['tokenColors']>[number];
|
||||||
lang: Lang,
|
dark: NonNullable<ThemeRegistration['tokenColors']>[number];
|
||||||
hideLineNumbers?: boolean
|
};
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
/**
|
||||||
return await highlightText(text, lang, true, { hideLineNumbers: hideLineNumbers ?? true });
|
* Shiki options for the highlighter.
|
||||||
} catch (_) {
|
*/
|
||||||
return null;
|
export type ShikiOptions = {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlight text using Speed-Highlight with detected language.
|
* Checks if a language is a bundled language.
|
||||||
* @param text Text to highlight.
|
* @param lang The language to check.
|
||||||
* @param hideLineNumbers Whether to hide line numbering.
|
* @returns Whether the language is a bundled language.
|
||||||
* @returns Highlighted html text.
|
|
||||||
*/
|
*/
|
||||||
export async function highlightAutodetect(text: string, hideLineNumbers?: boolean) {
|
export const isBundleLanguage = (lang: string): lang is BundledLanguage =>
|
||||||
const lang = await detectLanguage(text);
|
Object.keys(bundledLanguages).includes(lang);
|
||||||
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a custom language for reference in highlight rules.
|
* Find all nested languages in the markdown text and load them into the highlighter.
|
||||||
* @param id Id of the language.
|
* @param text Markdown text to parse for nested languages.
|
||||||
* @param langModule A module that has the default export set to an array of HighlightRule.
|
* @returns The set of nested languages found in the text.
|
||||||
* @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));
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export function loadCustomLanguage(id: string, langModule: { default: ShjLanguageDefinition }) {
|
const findNestedLanguages = (text: string) => {
|
||||||
return loadLanguage(id, langModule);
|
const languages = new Set<string>();
|
||||||
}
|
|
||||||
|
|
||||||
export interface HighlightFunctions {
|
const regex = /```([a-z]+)\n([\s\S]+?)\n```/g;
|
||||||
highlight: typeof highlight;
|
let match: RegExpExecArray | null;
|
||||||
highlightAutodetect: typeof highlightAutodetect;
|
while ((match = regex.exec(text))) {
|
||||||
loadCustomLanguage: typeof loadCustomLanguage;
|
languages.add(match[1]);
|
||||||
}
|
}
|
||||||
|
return languages;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load custom markdown syntax highlighting rules.
|
* Load all nested languages found in the markdown text into the highlighter.
|
||||||
* Automatically called when a Carta instance is created.
|
* @param highlighter The highlighter instance.
|
||||||
* @param extensions Additional extensions used in Carta.
|
* @param text The text to parse for nested languages.
|
||||||
|
* @returns Whether the highlighter was updated with new languages.
|
||||||
*/
|
*/
|
||||||
export function loadCustomMarkdown(extensions: CartaExtension[] = []) {
|
export const loadNestedLanguages = async (highlighter: Highlighter, text: string) => {
|
||||||
const highlightRules = extensions.map((ext) => ext.highlightRules ?? []).flat();
|
text = text.replaceAll('\r\n', '\n'); // Normalize line endings
|
||||||
const lang = [];
|
|
||||||
lang.push(...cartaMarkdown, ...highlightRules);
|
const languages = findNestedLanguages(text);
|
||||||
loadCustomLanguage('cartamd', { default: lang });
|
const loadedLanguages = highlighter.getLoadedLanguages();
|
||||||
}
|
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 CartaHistoryOptions {
|
export interface TextAreaHistoryOptions {
|
||||||
/**
|
/**
|
||||||
* 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: CartaHistoryOptions = {
|
const defaultHistoryOptions: TextAreaHistoryOptions = {
|
||||||
minInterval: 300,
|
minInterval: 300,
|
||||||
maxSize: 1_000_000
|
maxSize: 1_000_000
|
||||||
};
|
};
|
||||||
|
@ -27,11 +27,11 @@ const defaultHistoryOptions: CartaHistoryOptions = {
|
||||||
/**
|
/**
|
||||||
* Input undo/redo functionality.
|
* Input undo/redo functionality.
|
||||||
*/
|
*/
|
||||||
export class CartaHistory {
|
export class TextAreaHistory {
|
||||||
private states: HistoryState[] = [];
|
private states: HistoryState[] = [];
|
||||||
private currentIndex = -1; // Only <= 0 numbers
|
private currentIndex = -1; // Only <= 0 numbers
|
||||||
private readonly options: CartaHistoryOptions;
|
private readonly options: TextAreaHistoryOptions;
|
||||||
constructor(options?: Partial<CartaHistoryOptions>) {
|
constructor(options?: Partial<TextAreaHistoryOptions>) {
|
||||||
this.options = mergeDefaultInterface(options, defaultHistoryOptions);
|
this.options = mergeDefaultInterface(options, defaultHistoryOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ export class CartaHistory {
|
||||||
}
|
}
|
||||||
this.currentIndex = -1;
|
this.currentIndex = -1;
|
||||||
|
|
||||||
if (latest && Date.now() - latest.timestamp.getTime() <= this.options.minInterval) {
|
if (latest && Date.now() - latest.timestamp.getTime() <= (this.options.minInterval ?? 300)) {
|
||||||
this.states.pop();
|
this.states.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ export class CartaHistory {
|
||||||
// every char is 2 bytes
|
// every char is 2 bytes
|
||||||
size += value.length * 2;
|
size += value.length * 2;
|
||||||
|
|
||||||
while (size > this.options.maxSize) {
|
while (size > (this.options.maxSize ?? 1_000_000)) {
|
||||||
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 { CartaInput } from './input';
|
import type { InputEnhancer } 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 CartaIcon {
|
export interface Icon {
|
||||||
/**
|
/**
|
||||||
* 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 CartaInput instance
|
* @param input InputEnhancer instance
|
||||||
*/
|
*/
|
||||||
action: (input: CartaInput) => void;
|
action: (input: InputEnhancer) => 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 CartaIcon[];
|
] as const satisfies readonly Icon[];
|
||||||
|
|
||||||
export type DefaultIconId = (typeof defaultIcons)[number]['id'] | 'menu';
|
export type DefaultIconId = (typeof defaultIcons)[number]['id'] | 'menu';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { CartaListener } from './carta';
|
import type { Listener } from './carta';
|
||||||
import type { Prefix } from './prefixes';
|
import type { Prefix } from './prefixes';
|
||||||
import type { KeyboardShortcut } from './shortcuts';
|
import type { KeyboardShortcut } from './shortcuts';
|
||||||
import { CartaHistory, type CartaHistoryOptions } from './history';
|
import { TextAreaHistory as TextAreaHistory, type TextAreaHistoryOptions } 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: CartaListener<any>[];
|
readonly listeners: Listener<any>[];
|
||||||
readonly historyOpts?: Partial<CartaHistoryOptions>;
|
readonly historyOpts?: Partial<TextAreaHistoryOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CartaInput {
|
export class InputEnhancer {
|
||||||
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: CartaHistory;
|
public history: TextAreaHistory;
|
||||||
public readonly events = new EventTarget();
|
public readonly events = new EventTarget();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -54,7 +54,7 @@ export class CartaInput {
|
||||||
|
|
||||||
textarea.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
textarea.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||||
|
|
||||||
this.history = new CartaHistory(settings.historyOpts);
|
this.history = new TextAreaHistory(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 CartaLabels {
|
export interface Labels {
|
||||||
writeTab: string;
|
writeTab: string;
|
||||||
previewTab: string;
|
previewTab: string;
|
||||||
iconsLabels: Partial<Record<IconId, string>>;
|
iconsLabels: Partial<Record<IconId, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultCartaLabels: CartaLabels = {
|
export const defaultLabels: Labels = {
|
||||||
writeTab: 'Write',
|
writeTab: 'Write',
|
||||||
previewTab: 'Preview',
|
previewTab: 'Preview',
|
||||||
iconsLabels: {}
|
iconsLabels: {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export class CartaRenderer {
|
export class Renderer {
|
||||||
constructor(public readonly container: HTMLDivElement) {}
|
constructor(public readonly container: HTMLDivElement) {}
|
||||||
// Reserved for future use
|
// Reserved for future use
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
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 { CartaInput } from './input';
|
import type { InputEnhancer } 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: CartaInput) => void;
|
action: (input: InputEnhancer) => void;
|
||||||
/**
|
/**
|
||||||
* Prevent saving the current state in history.
|
* Prevent saving the current state in history.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,6 +4,8 @@ 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.
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
.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 { CartaEditor } from '$lib';
|
import { MarkdownEditor } 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,7 +23,8 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<CartaEditor placeholder="Some text..." mode="tabs" {carta} />
|
<ToggleTheme class="toggle-theme" />
|
||||||
|
<MarkdownEditor value={sampleText} placeholder="Some text..." mode="tabs" {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -33,9 +34,10 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code, code) {
|
:global(.carta-font-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) {
|
||||||
|
@ -43,9 +45,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
max-width: 1536px;
|
max-width: 1536px;
|
||||||
margin: 0 auto 0 auto;
|
margin: 2rem auto 2rem 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 */
|
||||||
|
|
74
packages/carta-md/src/routes/ToggleTheme.svelte
Normal file
74
packages/carta-md/src/routes/ToggleTheme.svelte
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<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>
|
79
packages/carta-md/src/routes/sample.md
Normal file
79
packages/carta-md/src/routes/sample.md
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# 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 |
|
BIN
packages/carta-md/static/pic.jpg
Normal file
BIN
packages/carta-md/static/pic.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-anchor/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -32,11 +32,11 @@
|
||||||
"!dist/**/*.spec.*"
|
"!dist/**/*.spec.*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"slugify": "^1.6.6"
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
"rehype-slug": "^6.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.1.0",
|
"carta-md": "^4.0.0",
|
||||||
"marked": "^9.1.5",
|
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,13 +1,32 @@
|
||||||
.carta-renderer .anchor-link {
|
.carta-viewer h1,
|
||||||
visibility: hidden;
|
.carta-viewer h2,
|
||||||
opacity: 0.6;
|
.carta-viewer h3,
|
||||||
|
.carta-viewer h4,
|
||||||
|
.carta-viewer h5,
|
||||||
|
.carta-viewer h6 {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-renderer h1:hover .anchor-link,
|
.carta-viewer h1 .icon.icon-link,
|
||||||
.carta-renderer h2:hover .anchor-link,
|
.carta-viewer h2 .icon.icon-link,
|
||||||
.carta-renderer h3:hover .anchor-link,
|
.carta-viewer h3 .icon.icon-link,
|
||||||
.carta-renderer h4:hover .anchor-link,
|
.carta-viewer h4 .icon.icon-link,
|
||||||
.carta-renderer h5:hover .anchor-link,
|
.carta-viewer h5 .icon.icon-link,
|
||||||
.carta-renderer h6:hover .anchor-link {
|
.carta-viewer h6 .icon.icon-link {
|
||||||
visibility: visible;
|
opacity: 0;
|
||||||
|
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,47 +1,30 @@
|
||||||
import type { CartaExtension } from 'carta-md';
|
import rehypeSlug, { type Options as SlugOptions } from 'rehype-slug';
|
||||||
import { generateUniqueSlug } from './slug';
|
import rehypeAutolinkHeadings, { type Options as AutolinkOptions } from 'rehype-autolink-headings';
|
||||||
|
import type { Plugin } from 'carta-md';
|
||||||
export * from './default.css?inline';
|
export * from './default.css?inline';
|
||||||
|
|
||||||
export interface AnchorExtensionOptions {
|
export interface AnchorExtensionOptions {
|
||||||
/**
|
/**
|
||||||
* Maximum depth of headers to generate anchors for. Defaults to 6.
|
* rehype-slug options.
|
||||||
*/
|
*/
|
||||||
maxDepth?: number;
|
slug?: SlugOptions;
|
||||||
|
/**
|
||||||
|
* 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): CartaExtension => {
|
export const anchor = (options?: AnchorExtensionOptions): Plugin => {
|
||||||
let slugs: string[] = [];
|
|
||||||
|
|
||||||
const maxDepth = options?.maxDepth ?? 6;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Reset the slug history after rendering completes, so the links persist after re-rendering
|
transformers: [
|
||||||
listeners: [
|
|
||||||
['carta-render', () => (slugs = [])],
|
|
||||||
['carta-render-ssr', () => (slugs = [])]
|
|
||||||
],
|
|
||||||
markedExtensions: [
|
|
||||||
{
|
{
|
||||||
renderer: {
|
execution: 'sync',
|
||||||
heading(text, level, raw) {
|
type: 'rehype',
|
||||||
if (level > maxDepth) {
|
transform({ processor }) {
|
||||||
return false;
|
processor.use(rehypeSlug, options?.slug).use(rehypeAutolinkHeadings, options?.autolink);
|
||||||
}
|
|
||||||
|
|
||||||
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}>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
3
packages/plugin-anchor/src/lib/link.svg
Normal file
3
packages/plugin-anchor/src/lib/link.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 575 B |
|
@ -1,23 +0,0 @@
|
||||||
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,13 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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({
|
||||||
maxDepth: 2
|
autolink: {}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -28,7 +29,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<CartaEditor {carta} {value} />
|
<MarkdownEditor {carta} {value} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -38,9 +39,15 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code, code) {
|
:global(.carta-font-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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"!dist/**/*.spec.*"
|
"!dist/**/*.spec.*"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.4.0",
|
"carta-md": "^4.0.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, CartaExtension, CartaListener } from 'carta-md';
|
import type { Carta, Plugin, Listener } 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): CartaExtension => {
|
export const attachment = (options: AttachmentExtensionOptions): Plugin => {
|
||||||
let carta: Carta | undefined;
|
let carta: Carta | undefined;
|
||||||
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
|
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
|
||||||
|
|
||||||
|
@ -122,11 +122,11 @@ export const attachment = (options: AttachmentExtensionOptions): CartaExtension
|
||||||
carta = c;
|
carta = c;
|
||||||
},
|
},
|
||||||
listeners: [
|
listeners: [
|
||||||
['drop', handleDrop, false] satisfies CartaListener<'drop'>,
|
['drop', handleDrop, false] satisfies Listener<'drop'>,
|
||||||
['dragenter', () => draggingOverTextArea.set(true)] satisfies CartaListener<'dragenter'>,
|
['dragenter', () => draggingOverTextArea.set(true)] satisfies Listener<'dragenter'>,
|
||||||
['dragleave', () => draggingOverTextArea.set(false)] satisfies CartaListener<'dragleave'>,
|
['dragleave', () => draggingOverTextArea.set(false)] satisfies Listener<'dragleave'>,
|
||||||
['dragover', (e) => e.preventDefault()] satisfies CartaListener<'dragover'>,
|
['dragover', (e) => e.preventDefault()] satisfies Listener<'dragover'>,
|
||||||
['paste', handlePaste, false] satisfies CartaListener<'paste'>
|
['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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
# Carta Code Plugin
|
# Carta Code Plugin
|
||||||
|
|
||||||
This plugin adds support for code blocks **syntax highlighting**. Install it using:
|
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
||||||
|
@ -20,28 +18,31 @@ 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.
|
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`.
|
||||||
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.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
code({
|
const carta = new Carta({
|
||||||
customHighlight: {
|
// ...
|
||||||
highlighter: (code, lang) => myCustomHighlighter(code, lang),
|
extensions: [
|
||||||
langPrefix: 'my-highlighter-'
|
code({
|
||||||
|
theme: 'ayu-light'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
shikiOptions: {
|
||||||
|
themes: ['ayu-light']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { code } from '@cartamd/plugin-code';
|
import { code } from '@cartamd/plugin-code';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -49,7 +50,7 @@ code({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -16,21 +16,22 @@
|
||||||
"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": "^3.0.0"
|
"carta-md": "^4.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked-highlight": "^2.0.6"
|
"@shikijs/rehype": "^1.4.0",
|
||||||
|
"unified": "^11.0.4"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
|
|
|
@ -1,70 +1,52 @@
|
||||||
import type { CartaExtension, HighlightFunctions } from 'carta-md';
|
import type { DualTheme, Theme, Plugin } from 'carta-md';
|
||||||
import { markedHighlight } from 'marked-highlight';
|
import type { RehypeShikiOptions } from '@shikijs/rehype';
|
||||||
|
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
|
||||||
|
|
||||||
interface CodeExtensionOptions {
|
export type CodeExtensionOptions = Omit<RehypeShikiOptions, 'theme' | 'themes'> & {
|
||||||
/**
|
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.
|
||||||
* Options for custom syntax highlighting.
|
// However, when importing from carta-md, this causes a MODULE_NOT_FOUND error
|
||||||
*/
|
// for some reason.
|
||||||
customHighlight?: {
|
/**
|
||||||
/**
|
* Checks if a theme is a dual theme.
|
||||||
* Custom highlight function. Beware that you'll have to provide your own styles.
|
* @param theme The theme to check.
|
||||||
* This function needs to convert a string of code into html.
|
* @returns Whether the theme is a dual theme.
|
||||||
*/
|
*/
|
||||||
highlighter: (code: string, lang: string) => string | Promise<string>;
|
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme =>
|
||||||
/**
|
typeof theme == 'object' && 'light' in theme && 'dark' in theme;
|
||||||
* 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.
|
* Checks if a theme is a single theme.
|
||||||
*/
|
* @param theme The theme to check.
|
||||||
langPrefix: string;
|
* @returns Whether the theme is a single theme.
|
||||||
};
|
*/
|
||||||
}
|
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): CartaExtension => {
|
export const code = (options?: CodeExtensionOptions): Plugin => {
|
||||||
return {
|
return {
|
||||||
onLoad: ({ highlight }) => (shj = highlight),
|
transformers: [
|
||||||
markedExtensions: [
|
{
|
||||||
markedHighlight({
|
execution: 'async',
|
||||||
langPrefix: options?.customHighlight?.langPrefix ?? 'shj-lang-',
|
type: 'rehype',
|
||||||
async: true,
|
async transform({ processor, carta }) {
|
||||||
async highlight(code, lang) {
|
let theme = options?.theme;
|
||||||
if (options?.customHighlight) {
|
|
||||||
return await options.customHighlight.highlighter(code, lang);
|
const highlighter = await carta.highlighter();
|
||||||
|
if (!theme) {
|
||||||
|
theme = highlighter.theme; // Use the theme specified in the highlighter
|
||||||
}
|
}
|
||||||
|
|
||||||
const { highlight, highlightAutodetect } = shj;
|
if (isSingleTheme(theme)) {
|
||||||
|
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, theme });
|
||||||
lang ||= options?.defaultLanguage ?? '';
|
} else {
|
||||||
let highlighted: string | null = null;
|
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, themes: theme });
|
||||||
|
}
|
||||||
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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@cartamd/plugin-emoji",
|
"name": "@cartamd/plugin-emoji",
|
||||||
"version": "3.0.0",
|
"version": "4.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": "^3.1.0",
|
"carta-md": "^4.0.0",
|
||||||
"marked": "^9.1.5",
|
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { CartaExtension, CartaExtensionComponent } from 'carta-md';
|
import type { Plugin, ExtensionComponent, GrammarRule, HighlightingRule } from 'carta-md';
|
||||||
import type { TokenizerAndRendererExtension } from 'marked';
|
import remarkGemoji from 'remark-gemoji';
|
||||||
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';
|
||||||
|
@ -25,7 +24,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): CartaExtension => {
|
export const emoji = (options?: EmojiExtensionOptions): Plugin => {
|
||||||
const inTransition =
|
const inTransition =
|
||||||
options?.inTransition ??
|
options?.inTransition ??
|
||||||
((node: Element) =>
|
((node: Element) =>
|
||||||
|
@ -40,7 +39,7 @@ export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
|
||||||
duration: 100
|
duration: 100
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const emojiComponent: CartaExtensionComponent<ComponentProps> = {
|
const emojiComponent: ExtensionComponent<ComponentProps> = {
|
||||||
component: Emoji,
|
component: Emoji,
|
||||||
parent: 'input',
|
parent: 'input',
|
||||||
props: {
|
props: {
|
||||||
|
@ -49,39 +48,42 @@ export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
markedExtensions: [
|
transformers: [
|
||||||
{
|
{
|
||||||
extensions: [emojiTokenizerAndRenderer()]
|
execution: 'sync',
|
||||||
|
type: 'remark',
|
||||||
|
transform({ processor }) {
|
||||||
|
processor.use(remarkGemoji);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
components: [emojiComponent],
|
components: [emojiComponent],
|
||||||
highlightRules: [
|
grammarRules: [grammar],
|
||||||
{
|
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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -32,9 +32,10 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code, code) {
|
:global(.carta-font-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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
@ -17,23 +17,20 @@
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/katex": "^0.16.0",
|
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
"marked": "^9.1.5",
|
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.0.0",
|
"carta-md": "^4.0.0"
|
||||||
"katex": "^0.16.10",
|
|
||||||
"marked": "^9.1.5"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"katex": "^0.16.10"
|
"rehype-katex": "^7.0.0",
|
||||||
|
"remark-math": "^6.0.0"
|
||||||
},
|
},
|
||||||
"version": "3.0.0",
|
"version": "4.0.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import type { Carta, CartaExtension } from 'carta-md';
|
import type { Plugin } from 'carta-md';
|
||||||
import { TokenizerAndRendererExtension } from 'marked';
|
import remarkMath, { type Options as RemarkMathOptions } from 'remark-math';
|
||||||
import katex, { KatexOptions } from 'katex';
|
import rehypeKatex, { type Options as RehypeKatexOptions } from 'rehype-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
|
||||||
*/
|
*/
|
||||||
|
@ -20,51 +19,45 @@ 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): CartaExtension => {
|
export const math = (options?: MathExtensionOptions): Plugin => {
|
||||||
return {
|
return {
|
||||||
onLoad: ({ carta: c, highlight: shj }) => {
|
onLoad: async ({ carta }) => {
|
||||||
carta = c;
|
const highlighter = await carta.highlighter();
|
||||||
import('./latex.js')
|
await highlighter.loadLanguage('latex');
|
||||||
.then((module) => shj.loadCustomLanguage('latex', module))
|
carta.input?.update();
|
||||||
.then(() => carta.input?.update());
|
|
||||||
},
|
},
|
||||||
markedExtensions: [
|
transformers: [
|
||||||
{
|
{
|
||||||
extensions: [inlineKatex(options?.inline), blockKatex(options?.block)]
|
execution: 'sync',
|
||||||
|
type: 'remark',
|
||||||
|
transform({ processor }) {
|
||||||
|
processor.use(remarkMath, options?.remarkMath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
execution: 'sync',
|
||||||
|
type: 'rehype',
|
||||||
|
transform({ processor }) {
|
||||||
|
processor.use(rehypeKatex, options?.rehypeKatex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
|
@ -79,64 +72,57 @@ export const math = (options?: MathExtensionOptions): CartaExtension => {
|
||||||
action: (input) => input.toggleSelectionSurrounding(['$$\n', '\n$$'])
|
action: (input) => input.toggleSelectionSurrounding(['$$\n', '\n$$'])
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
highlightRules: [
|
grammarRules: [
|
||||||
{
|
{
|
||||||
match: /\$[{}[\]a-zA-Z0-9.+-_=*/\\ ]+\$/g,
|
name: 'inline_math',
|
||||||
sub: 'latex'
|
type: 'inline',
|
||||||
|
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' }
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /^\$\$+\n([^$]+?)\n\$\$+\n/gm,
|
name: 'block_math',
|
||||||
sub: 'latex'
|
type: 'block',
|
||||||
|
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}>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@cartamd/plugin-slash",
|
"name": "@cartamd/plugin-slash",
|
||||||
"version": "3.0.0",
|
"version": "4.0.1",
|
||||||
"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": "^3.1.0",
|
"carta-md": "^4.0.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 { CartaExtension, CartaExtensionComponent } from 'carta-md';
|
import type { Plugin, ExtensionComponent } 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): CartaExtension => {
|
export const slash = (options?: SlashExtensionOptions): Plugin => {
|
||||||
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): CartaExtension => {
|
||||||
fade(node, {
|
fade(node, {
|
||||||
duration: 100
|
duration: 100
|
||||||
}));
|
}));
|
||||||
const slashComponent: CartaExtensionComponent<ComponentProps> = {
|
const slashComponent: ExtensionComponent<ComponentProps> = {
|
||||||
component: SlashComponent,
|
component: SlashComponent,
|
||||||
props: {
|
props: {
|
||||||
snippets,
|
snippets,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { CartaInput } from 'carta-md';
|
import type { InputEnhancer } 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: CartaInput) => void;
|
action: (input: InputEnhancer) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertLine(input: CartaInput, string: string) {
|
function insertLine(input: InputEnhancer, 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, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -16,7 +16,7 @@ npm i @cartamd/plugin-tikz
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } 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>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {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": "^3.0.0",
|
"carta-md": "^4.0.0"
|
||||||
"marked": "^9.1.5"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
@ -46,5 +46,9 @@
|
||||||
"syntax highlighting",
|
"syntax highlighting",
|
||||||
"emoji",
|
"emoji",
|
||||||
"katex"
|
"katex"
|
||||||
]
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"hast-util-from-dom": "^5.0.0",
|
||||||
|
"unist-util-visit": "^5.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import type { Carta, CartaEvent, CartaExtension } from 'carta-md';
|
import type { Carta, Event, Plugin } from 'carta-md';
|
||||||
import { TokenizerAndRendererExtension } from 'marked';
|
import type { Plugin as UnifiedPlugin } from 'unified';
|
||||||
|
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 {
|
||||||
|
@ -18,102 +21,124 @@ interface TikzExtensionOptions {
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
/**
|
/**
|
||||||
* Post processing function for html.
|
* Post processing function for html.
|
||||||
* This also runs on stored html, differently
|
* This also runs on stored html.
|
||||||
* 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): CartaExtension => {
|
export const tikz = (options?: TikzExtensionOptions): Plugin => {
|
||||||
|
let carta: Carta;
|
||||||
return {
|
return {
|
||||||
cartaRef: (c) => (carta = c),
|
onLoad: async ({ carta: c }) => {
|
||||||
shjRef: (shj) => {
|
carta = c;
|
||||||
import('./tikz')
|
|
||||||
.then((module) => shj.loadCustomLanguage('tikz', module))
|
const highlighter = await carta.highlighter();
|
||||||
.then(() => carta.input?.update());
|
await highlighter.loadLanguage('latex');
|
||||||
|
carta.input?.update();
|
||||||
},
|
},
|
||||||
markedExtensions: [
|
transformers: [
|
||||||
{
|
{
|
||||||
async: true,
|
execution: 'async',
|
||||||
extensions: [tikzTokenizer(options)]
|
type: 'rehype',
|
||||||
|
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 tikzTokenizer = (options?: TikzExtensionOptions): TokenizerAndRendererExtension => {
|
const tikzTransformer: UnifiedPlugin<
|
||||||
return {
|
[{ carta: Carta; options: TikzExtensionOptions | undefined }],
|
||||||
name: 'tikz',
|
hast.Root
|
||||||
level: 'block',
|
> = ({ carta, options }) => {
|
||||||
start: (src) => src.indexOf('\n```tikz'),
|
return async function (tree) {
|
||||||
tokenizer: (src) => {
|
visit(tree, (pre, index, parent) => {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = document.createElement('div');
|
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 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);
|
||||||
|
|
||||||
// Try accessing cached HTML
|
const hash = md5(JSON.stringify(template.dataset) + text.nodeValue);
|
||||||
const hash = md5(JSON.stringify(template.dataset) + template.childNodes[0].nodeValue);
|
let savedSvg = window.localStorage.getItem(hash);
|
||||||
const savedSvg = window.localStorage.getItem(hash);
|
|
||||||
|
|
||||||
let html: string;
|
|
||||||
if (savedSvg) {
|
if (savedSvg) {
|
||||||
html = savedSvg;
|
if (options?.postProcessing) savedSvg = options.postProcessing(savedSvg);
|
||||||
if (options?.postProcessing) html = options.postProcessing(html);
|
container.innerHTML = savedSvg;
|
||||||
} else {
|
} else {
|
||||||
html = template.outerHTML;
|
container.appendChild(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizer = carta.options?.sanitizer;
|
if (carta.sanitizer) {
|
||||||
if (sanitizer) html = sanitizer(html);
|
container.innerHTML = carta.sanitizer(container.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
const hastNode = fromDom(container) as hast.Element;
|
||||||
<div
|
|
||||||
${center ? 'align="center"' : ''}
|
parent?.children.splice(index!, 1, hastNode);
|
||||||
class="tikz-generated ${options?.class ?? ''}"
|
|
||||||
tikz-generation="${currentGeneration}"
|
return [SKIP, index!];
|
||||||
>
|
});
|
||||||
${html}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,7 +148,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTikzImages(e: CartaEvent, options?: TikzExtensionOptions) {
|
function generateTikzImages(e: Event, 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) {
|
||||||
|
@ -150,7 +175,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 = `<script type="text/javascript" id="tikzjax">${tikzjax}</script>`;
|
const script = /* html */ `<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.
|
||||||
|
@ -161,9 +186,6 @@ 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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
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'
|
|
||||||
}
|
|
||||||
];
|
|
904
pnpm-lock.yaml
generated
904
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