Compare commits

..

No commits in common. "master" and "carta-md-v3.1.3" have entirely different histories.

126 changed files with 2279 additions and 7411 deletions

View file

@ -13,7 +13,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 6
- name: Install dependendencies - name: Install dependendencies
run: pnpm i run: pnpm i

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 6
- name: Install dependencies - name: Install dependencies
run: pnpm i run: pnpm i
@ -33,4 +33,4 @@ jobs:
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build publish_dir: ./demo/build

View file

@ -16,7 +16,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 6
- name: Install dependendencies - name: Install dependendencies
run: pnpm i run: pnpm i
@ -45,7 +45,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 6
- name: Install dependendencies - name: Install dependendencies
run: pnpm i run: pnpm i
@ -53,6 +53,9 @@ jobs:
- name: Build all packages - name: Build all packages
run: pnpm build run: pnpm build
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
- name: Release - name: Release
run: npm run publish run: npm run publish
env: env:

View file

@ -5,15 +5,11 @@
"coldark", "coldark",
"dompurify", "dompurify",
"flexsearch", "flexsearch",
"Gemoji",
"gruvbox", "gruvbox",
"iconify",
"Katex", "Katex",
"mdsvex", "mdsvex",
"oldschool", "oldschool",
"rehype", "rehype",
"shiki",
"shikijs",
"tikz", "tikz",
"tikzjax", "tikzjax",
"typeof" "typeof"
@ -21,6 +17,5 @@
"typescript.tsdk": "node_modules\\typescript\\lib", "typescript.tsdk": "node_modules\\typescript\\lib",
"[svelte]": { "[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode" "editor.defaultFormatter": "svelte.svelte-vscode"
}, }
"css.customData": [".vscode/tailwind.json"]
} }

55
.vscode/tailwind.json vendored
View file

@ -1,55 +0,0 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that youd 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"
}
]
}
]
}

View file

@ -1,25 +1,29 @@
<div align="right"> <div align="right">
<a href="https://www.npmjs.com/package/carta-md"> <a href="https://www.npmjs.com/package/carta-md">
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&labelColor=171d27&logo=npm&logoColor=white" alt="npm"> <img src="https://img.shields.io/npm/v/carta-md?color=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
</a> </a>
<a href="https://bundlephobia.com/package/carta-md"> <a href="https://bundlephobia.com/package/carta-md">
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle"> <img src="https://img.shields.io/bundlephobia/min/carta-md?color=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
</a> </a>
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE"> <a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&labelColor=171d27&logo=git&logoColor=white" alt="license"> <img src="https://img.shields.io/npm/l/carta-md?color=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
</a> </a>
<a href="http://beartocode.github.io/carta/"> <a href="http://beartocode.github.io/carta/">
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs"> <img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
</a> </a>
</div> </div>
[![Carta.png](https://i.postimg.cc/nV6DMXKM/Carta.png)](https://beartocode.github.io/carta/) <div align="center">
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
</div>
<h1 align="center"><strong>Carta</strong></h1> <br>
<div align="center">Modern, lightweight, powerful Markdown Editor.</div>
<div align="center"><strong>Carta</strong></div>
<div align="center">Swiftly edit and render Markdown, with no overhead.</div>
<br /> <br />
<div align="center"> <div align="center">
<a href="https://beartocode.github.io/carta/">📚 Documentation</a> <a href="https://beartocode.github.io/carta/">Documentation</a>
<span> · </span> <span> · </span>
<a href="https://github.com/BearToCode/carta">GitHub</a> <a href="https://github.com/BearToCode/carta">GitHub</a>
</div> </div>
@ -28,30 +32,22 @@
# Introduction # Introduction
> [!NOTE] Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer, based on [Marked](https://github.com/markedjs/marked). Check out the [demo](http://beartocode.github.io/carta/) to see it in action.
> Carta has recently been updated to `v4`, which features numerous major changes. Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
>
> Follow the [Migration Guide](http://beartocode.github.io/carta/migration) to update your project.
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer. It is powered by [unified](https://github.com/unifiedjs/unified), [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
Differently from most editors, Carta does not include a code editor, but it is _just_ a textarea with syntax highlighting, shortcuts and more.
## Features ## Features
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/)); - Keyboard **shortcuts** (extensible);
- 🛠️ Toolbar (extensible); - Toolbar (extensible);
- ⌨️ Keyboard **shortcuts** (extensible); - Markdown syntax highlighting;
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark; - Scroll sync;
- 🔀 Scroll sync; - **SSR** compatible;
- ✅ Accessibility friendly; - **Katex** support (plugin);
- 🖥️ **SSR** compatible; - **Slash** commands (plugin);
- ⚗️ **KaTeX** support (plugin); - **Emojis**, with included search (plugin);
- 🔨 **Slash** commands (plugin); - **Tikz** support(plugin);
- 😄 **Emojis**, with included search (plugin); - **Attachment** support(plugin);
- ✏️ **TikZ** support (plugin); - Code blocks **syntax highlighting** (plugin).
- 📂 **Attachment** support (plugin);
- ⚓ **Anchor** links in headings (plugin);
- 🌈 Code blocks **syntax highlighting** (plugin).
## Packages ## Packages
@ -64,23 +60,12 @@ Differently from most editors, Carta does not include a code editor, but it is _
| [plugin-slash](https://www.npmjs.com/package/@cartamd/plugin-slash) | ![plugin-slash](https://img.shields.io/npm/v/@cartamd/plugin-slash) | [/plugins/slash](https://beartocode.github.io/carta/plugins/slash) | | [plugin-slash](https://www.npmjs.com/package/@cartamd/plugin-slash) | ![plugin-slash](https://img.shields.io/npm/v/@cartamd/plugin-slash) | [/plugins/slash](https://beartocode.github.io/carta/plugins/slash) |
| [plugin-tikz](https://www.npmjs.com/package/@cartamd/plugin-tikz) | ![plugin-tikz](https://img.shields.io/npm/v/@cartamd/plugin-tikz) | [/plugins/tikz](https://beartocode.github.io/carta/plugins/tikz) | | [plugin-tikz](https://www.npmjs.com/package/@cartamd/plugin-tikz) | ![plugin-tikz](https://img.shields.io/npm/v/@cartamd/plugin-tikz) | [/plugins/tikz](https://beartocode.github.io/carta/plugins/tikz) |
| [plugin-attachment](https://www.npmjs.com/package/@cartamd/plugin-attachment) | ![plugin-attachment](https://img.shields.io/npm/v/@cartamd/plugin-attachment) | [/plugins/attachment](https://beartocode.github.io/carta/plugins/attachment) | | [plugin-attachment](https://www.npmjs.com/package/@cartamd/plugin-attachment) | ![plugin-attachment](https://img.shields.io/npm/v/@cartamd/plugin-attachment) | [/plugins/attachment](https://beartocode.github.io/carta/plugins/attachment) |
| [plugin-anchor](https://www.npmjs.com/package/@cartamd/plugin-anchor) | ![plugin-anchor](https://img.shields.io/npm/v/@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 ## Installation
@ -100,9 +85,11 @@ npm i @cartamd/plugin-name
```svelte ```svelte
<script lang="ts"> <script lang="ts">
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
// Component default theme // Component default theme
import 'carta-md/default.css'; import 'carta-md/default.css';
// Markdown input theme (Speed Highlight)
import 'carta-md/light.css';
const carta = new Carta({ const carta = new Carta({
// Remember to use a sanitizer to prevent XSS attacks // Remember to use a sanitizer to prevent XSS attacks
@ -110,14 +97,13 @@ npm i @cartamd/plugin-name
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
<style> <style>
/* Or in global stylesheet */ /* Or in global stylesheet */
/* Set your custom monospace font */ /* Set your custom monospace font */
:global(.carta-font-code) { :global(.carta-font-code) {
font-family: '...', monospace; font-family: '...', monospace;
font-size: 1.1rem;
} }
</style> </style>
``` ```
@ -137,7 +123,6 @@ 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)
@ -158,12 +143,3 @@ npm run commit
# or, if you have commitizen installed globally # or, if you have commitizen installed globally
git cz git cz
``` ```
### Running docs
If you want to preview the docs:
```
cd docs
npm run dev
```

View file

@ -15,11 +15,9 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1", "@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "3.0.1", "@sveltejs/adapter-static": "1.0.0-next.50",
"@sveltejs/kit": "^2.5.4", "@sveltejs/kit": "^1.5.0",
"@sveltejs/package": "^2.3.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/flexsearch": "^0.7.6", "@types/flexsearch": "^0.7.6",
"@types/katex": "^0.16.0", "@types/katex": "^0.16.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
@ -27,12 +25,12 @@
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"sass": "^1.69.5", "sass": "^1.69.5",
"svelte": "^4.2.12", "svelte": "^3.54.0 || ^4.0.0",
"svelte-check": "^3.6.7", "svelte-check": "^3.0.1",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^5.1.6" "vite": "^4.3.9"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@ -47,8 +45,8 @@
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk-sv": "^0.0.6", "cmdk-sv": "^0.0.6",
"flexsearch": "0.7.21", "flexsearch": "0.7.21",
"iconify-icon": "^2.0.0", "katex": "^0.16.7",
"katex": "^0.16.10", "radix-icons-svelte": "^1.2.1",
"tailwind-merge": "^2.0.0" "tailwind-merge": "^2.0.0"
} }
} }

View file

@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Copy } from 'radix-icons-svelte';
let elem: HTMLElement; let elem: HTMLElement;
</script> </script>
@ -11,10 +13,10 @@
navigator.clipboard.writeText(elem.innerText); navigator.clipboard.writeText(elem.innerText);
}} }}
class=" class="
absolute right-4 top-[min(50%_,_32px)] aspect-square -translate-y-1/2 transform absolute right-4 top-[min(50%_,_32px)] -translate-y-1/2 transform
rounded hover:bg-neutral-800 hover:text-neutral-300 active:text-sky-300 rounded p-2 hover:bg-neutral-800 hover:text-neutral-300 active:text-sky-300
" "
> >
<iconify-icon icon="octicon:copy-16" class="p-2 text-lg"></iconify-icon> <Copy class="h-5 w-5" />
</button> </button>
</div> </div>

View file

@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onNavigate } from '$app/navigation'; import { onDestroy, onMount } from 'svelte';
import { debounce, throttle } from '$lib/utils';
import { onMount } from 'svelte';
const PADDING = 80; const PADDING = 80;
@ -9,58 +7,49 @@
let className = ''; let className = '';
let headers: HTMLElement[] = []; let headers: HTMLElement[] = [];
let selectedHeaderIndex = 0; let scrollY = 0;
function retrieveHeaders() { function retrieveHeaders() {
headers = Array.from( const markdownContainer = document.querySelector('.markdown');
document.querySelectorAll('.markdown > h1, .markdown > h2, .markdown > h3') if (!markdownContainer) return;
) as HTMLElement[]; headers = Array.from(markdownContainer.querySelectorAll('h1, h2, h3')) as HTMLElement[];
} }
const highlightHeader = () => { function highlightHeader(header: HTMLElement, nextHeader: HTMLElement | null, index: number) {
for (let index = headers.length - 1; index >= 0; index--) { const headerHasReachedTop = header.getBoundingClientRect().top <= PADDING || index == 0;
const header = headers[index]; const nextHeaderReachedTop = nextHeader && nextHeader.getBoundingClientRect().top <= PADDING;
const rect = header.getBoundingClientRect(); return !nextHeaderReachedTop && headerHasReachedTop;
if (rect.top < PADDING) { }
selectedHeaderIndex = index;
return;
}
}
selectedHeaderIndex = 0;
};
const [throttledHighlightHeader] = throttle(highlightHeader, 100); let observer: MutationObserver;
const debouncedHighlightHeader = debounce(highlightHeader, 100);
onNavigate(() => { onMount(() => {
setTimeout(() => { observer = new MutationObserver(retrieveHeaders);
retrieveHeaders(); observer.observe(document.body, { childList: true, subtree: true });
highlightHeader(); retrieveHeaders();
}, 300); });
onDestroy(() => {
observer?.disconnect();
}); });
onMount(retrieveHeaders);
</script> </script>
<svelte:window <svelte:window bind:scrollY />
on:scroll={() => {
throttledHighlightHeader();
debouncedHighlightHeader(); // So it is called at the end of the scroll event
}}
/>
<div class="h-full space-y-3 {className}"> <div class="h-full space-y-3 {className}">
{#each headers as header, i} {#each headers as header, i}
{@const margin = Number(header.tagName.split('')[1]) - 1} {@const margin = Number(header.tagName.split('')[1]) - 1}
{#key selectedHeaderIndex} {@const nextHeader = headers[i + 1]}
{#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href} {#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href}
{#key scrollY}
<a <a
style="margin-left: {margin * 0.75}rem;" style="margin-left: {margin * 0.75}rem;"
class="block text-sm {selectedHeaderIndex === i class="block text-sm {highlightHeader(header, nextHeader, i)
? 'font-medium text-sky-300' ? 'font-medium text-sky-300'
: 'text-neutral-400'}" : 'text-neutral-400'}"
href={header.children[0].href}>{header.innerText}</a href={header.children[0].href}>{header.innerText}</a
> >
{/if} {/key}
{/key} {/if}
{/each} {/each}
</div> </div>

View file

@ -1,22 +0,0 @@
<script lang="ts">
export let npmLink: string;
export let githubLink: string;
</script>
<div class="plugin-link mb-2 mt-6 flex items-end space-x-3">
<slot />
<a href={githubLink} class="flex aspect-square">
<iconify-icon icon="mdi:github" class="text-3xl text-white hover:text-sky-300"></iconify-icon>
</a>
<a href={npmLink} class="flex aspect-square">
<iconify-icon icon="gg:npm" class="text-3xl text-white hover:text-sky-300"></iconify-icon>
</a>
</div>
<style>
:global(.markdown .plugin-link > h1, .markdown .plugin-link > h2, .markdown .plugin-link > h3) {
margin-top: 0;
margin-bottom: 0;
}
</style>

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Cross1, HamburgerMenu } from 'radix-icons-svelte';
import Sidebar from '../sidebar/Sidebar.svelte'; import Sidebar from '../sidebar/Sidebar.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -15,7 +16,7 @@
<div class="container mb-4 w-full border-b border-neutral-800 px-4 pb-1 sm:px-6 {className}"> <div class="container mb-4 w-full border-b border-neutral-800 px-4 pb-1 sm:px-6 {className}">
<button on:click={() => (enabled = !enabled)} class="text-neutral-500 hover:text-neutral-200"> <button on:click={() => (enabled = !enabled)} class="text-neutral-500 hover:text-neutral-200">
<iconify-icon icon="ci:hamburger-lg" class="text-3xl"></iconify-icon> <HamburgerMenu class="h-7 w-7" />
</button> </button>
</div> </div>
@ -28,7 +29,7 @@
on:click={() => (enabled = false)} on:click={() => (enabled = false)}
class="absolute right-4 top-4 text-neutral-500 hover:text-neutral-200" class="absolute right-4 top-4 text-neutral-500 hover:text-neutral-200"
> >
<iconify-icon icon="charm:cross" class="text-2xl"></iconify-icon> <Cross1 class="h-4 w-4" />
</button> </button>
</div> </div>
{/if} {/if}

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { GithubLogo, Star } from 'radix-icons-svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export { className as class }; export { className as class };
@ -18,15 +19,15 @@
href="https://github.com/BearToCode/carta" href="https://github.com/BearToCode/carta"
class="flex h-12 items-center space-x-2 p-2 {className}" class="flex h-12 items-center space-x-2 p-2 {className}"
> >
<iconify-icon icon="mdi:github" class="text-2xl"></iconify-icon> <GithubLogo class="h-5 w-5" />
<div class="hidden h-min flex-col justify-center space-y-1 md:flex"> <div class="hidden h-min flex-col justify-center space-y-1 md:flex">
<p class="text-[0.9rem] font-semibold leading-3">BearToCode/carta</p> <p class="text-[0.9rem] font-semibold leading-3">BearToCode/carta</p>
{#if loading} {#if loading}
<div class="pulse my-1.5 h-3 w-[80px] rounded-full bg-neutral-800" /> <div class="pulse my-1.5 h-3 w-[80px] rounded-full bg-neutral-800" />
{:else} {:else}
<div class="inline-flex items-center space-x-1"> <div class="inline-flex items-center space-x-1">
<iconify-icon icon="ic:round-star" class="h-3 w-3"></iconify-icon> <Star class="h-3 w-3" />
<span class="mt-1 text-[0.8rem] leading-3">{stars}</span> <span class="text-[0.8rem] leading-3">{stars}</span>
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as Command from '$lib/components/ui/command'; import * as Command from '$lib/components/ui/command';
import type { Document } from 'flexsearch'; import type { Document } from 'flexsearch';
import { Enter, MagnifyingGlass } from 'radix-icons-svelte';
import { import {
enrichResult, enrichResult,
initializeSearch, initializeSearch,
@ -9,7 +10,6 @@
} from '$lib/search'; } from '$lib/search';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { base } from '$app/paths';
export { className as class }; export { className as class };
@ -53,8 +53,8 @@
<svelte:window on:keydown={handleKeydown} /> <svelte:window on:keydown={handleKeydown} />
<button on:click={() => (open = !open)} class="mr-2 block aspect-square md:hidden"> <button on:click={() => (open = !open)} class="mr-2 block md:hidden">
<iconify-icon icon="ion:search" class="text-2xl text-neutral-200"></iconify-icon> <MagnifyingGlass class="h-7 w-7 text-neutral-200" />
</button> </button>
<button <button
@ -62,7 +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">
<iconify-icon icon="ion:search" class="text-xl text-neutral-500"></iconify-icon> <MagnifyingGlass class="h-5 w-5 text-neutral-500" />
<span class="text-neutral-500">Search...</span> <span class="text-neutral-500">Search...</span>
</div> </div>
<kbd <kbd
@ -81,8 +81,8 @@
{#each results as result} {#each results as result}
<Command.Item <Command.Item
onSelect={() => { onSelect={() => {
if (result.match?.heading) goto(`${base}/${result.path}#${result.match.heading.id}`); if (result.match?.heading) goto(`/${result.path}#${result.match.heading.id}`);
else goto(`${base}/${result.path}`); else goto(`/${result.path}`);
open = false; open = false;
}} }}
class="group" class="group"
@ -101,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">
<iconify-icon icon="mi:enter" class="text-xl text-neutral-400"></iconify-icon> <Enter class="h-5 w-5 text-neutral-400" />
</div> </div>
</Command.Item> </Command.Item>
{/each} {/each}

View file

@ -1,4 +1,16 @@
<script lang="ts"> <script lang="ts">
import {
Code,
CodesandboxLogo,
Cube,
Dashboard,
Download,
Face,
FontFamily,
Slash,
File,
FontStyle
} from 'radix-icons-svelte';
import SidebarLink from './SidebarLink.svelte'; import SidebarLink from './SidebarLink.svelte';
export { className as class }; export { className as class };
@ -11,90 +23,66 @@
<!-- Introduction --> <!-- Introduction -->
<SidebarLink href="/introduction"> <SidebarLink href="/introduction">
<iconify-icon icon="radix-icons:dashboard" class="text-xl"></iconify-icon> <Dashboard class="h-5 w-5" />
<span class="text-[0.95rem]">Introduction</span> <span class="text-[0.95rem]">Introduction</span>
</SidebarLink> </SidebarLink>
<!-- Examples --> <!-- Examples -->
<SidebarLink href="/examples"> <SidebarLink href="/examples">
<iconify-icon icon="ph:codesandbox-logo" class="text-xl"></iconify-icon> <CodesandboxLogo class="h-5 w-5" />
<span class="text-[0.95rem]">Examples</span> <span class="text-[0.95rem]">Examples</span>
</SidebarLink> </SidebarLink>
<!-- Getting Started --> <!-- Getting Started -->
<SidebarLink href="/getting-started"> <SidebarLink href="/getting-started">
<iconify-icon icon="ic:round-download" class="text-xl"></iconify-icon> <Download class="h-5 w-5" />
<span class="text-[0.95rem]">Getting Started</span> <span class="text-[0.95rem]">Getting Started</span>
</SidebarLink> </SidebarLink>
<!-- Editing Styles --> <!-- Editing Styles -->
<SidebarLink href="/editing-styles"> <SidebarLink href="/editing-styles">
<iconify-icon icon="lucide:palette" class="text-xl"></iconify-icon> <FontStyle class="h-5 w-5" />
<span class="text-[0.95rem]">Editing Styles</span> <span class="text-[0.95rem]">Editing Styles</span>
</SidebarLink> </SidebarLink>
<!-- Migration -->
<SidebarLink href="/migration">
<iconify-icon icon="material-symbols:upgrade" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Migration</span>
</SidebarLink>
<!-- Community Plugins -->
<SidebarLink href="/community-plugins">
<iconify-icon icon="ph:stack-fill" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Community Plugins</span>
</SidebarLink>
<!-- Using Svelte Components -->
<SidebarLink href="/using-components">
<iconify-icon icon="ri:svelte-fill" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Using Components</span>
</SidebarLink>
<h3 class="mb-3 ml-4 mt-6 text-sm font-medium first:mt-0 last:mb-0">Plugins</h3> <h3 class="mb-3 ml-4 mt-6 text-sm font-medium first:mt-0 last:mb-0">Plugins</h3>
<!-- Math --> <!-- Math -->
<SidebarLink href="/plugins/math"> <SidebarLink href="/plugins/math">
<iconify-icon icon="tabler:math" class="text-xl"></iconify-icon> <FontFamily class="h-5 w-5" />
<span class="text-[0.95rem]">Math</span> <span class="text-[0.95rem]">Math</span>
</SidebarLink> </SidebarLink>
<!-- Code --> <!-- Code -->
<SidebarLink href="/plugins/code"> <SidebarLink href="/plugins/code">
<iconify-icon icon="fluent:code-16-filled" class="text-xl"></iconify-icon> <Code class="h-5 w-5" />
<span class="text-[0.95rem]">Code</span> <span class="text-[0.95rem]">Code</span>
</SidebarLink> </SidebarLink>
<!-- Emoji --> <!-- Emoji -->
<SidebarLink href="/plugins/emoji"> <SidebarLink href="/plugins/emoji">
<iconify-icon icon="mingcute:emoji-line" class="text-xl"></iconify-icon> <Face class="h-5 w-5" />
<span class="text-[0.95rem]">Emoji</span> <span class="text-[0.95rem]">Emoji</span>
</SidebarLink> </SidebarLink>
<!-- Slash --> <!-- Slash -->
<SidebarLink href="/plugins/slash"> <SidebarLink href="/plugins/slash">
<iconify-icon icon="tabler:slash" class="text-xl"></iconify-icon> <Slash class="h-5 w-5" />
<span class="text-[0.95rem]">Slash</span> <span class="text-[0.95rem]">Slash</span>
</SidebarLink> </SidebarLink>
<!-- TikZ --> <!-- TikZ -->
<SidebarLink href="/plugins/tikz"> <SidebarLink href="/plugins/tikz">
<iconify-icon icon="mdi:draw-pen" class="text-xl"></iconify-icon> <Cube class="h-5 w-5" />
<span class="text-[0.95rem]">TikZ</span> <span class="text-[0.95rem]">TikZ</span>
</SidebarLink> </SidebarLink>
<!-- Attachment --> <!-- Attachment -->
<SidebarLink href="/plugins/attachment"> <SidebarLink href="/plugins/attachment">
<iconify-icon icon="tdesign:attach" class="text-xl"></iconify-icon> <File class="h-5 w-5" />
<span class="text-[0.95rem]">Attachment</span> <span class="text-[0.95rem]">Attachment</span>
</SidebarLink> </SidebarLink>
<!-- Anchor -->
<SidebarLink href="/plugins/anchor">
<iconify-icon icon="mingcute:link-fill" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Anchor</span>
</SidebarLink>
<h3 class="mb-3 ml-4 mt-6 text-sm font-medium first:mt-0 last:mb-0">API</h3> <h3 class="mb-3 ml-4 mt-6 text-sm font-medium first:mt-0 last:mb-0">API</h3>
<!-- Utilities --> <!-- Utilities -->

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Link from '../link/Link.svelte'; import Link from '../link/Link.svelte';
@ -12,7 +11,7 @@
<Link <Link
{href} {href}
class=" class="
{currentHref === `${base}${href}` {currentHref === href
? 'bg-sky-400 bg-opacity-10 font-medium text-sky-300' ? 'bg-sky-400 bg-opacity-10 font-medium text-sky-300'
: 'text-neutral-400 hover:bg-neutral-800 hover:text-neutral-300'} : 'text-neutral-400 hover:bg-neutral-800 hover:text-neutral-300'}
inline-flex w-full items-center space-x-2 rounded-lg px-4 py-1.5"><slot /></Link inline-flex w-full items-center space-x-2 rounded-lg px-4 py-1.5"><slot /></Link

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive } from 'cmdk-sv';
import { MagnifyingGlass } from 'radix-icons-svelte';
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
// type $$Props = CommandPrimitive.InputProps; // type $$Props = CommandPrimitive.InputProps;
@ -10,7 +11,7 @@
</script> </script>
<div class="flex items-center border-b px-3" data-cmdk-input-wrapper=""> <div class="flex items-center border-b px-3" data-cmdk-input-wrapper="">
<iconify-icon icon="ion:search" class="mr-2 shrink-0 text-xl opacity-50"></iconify-icon> <MagnifyingGlass class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input <CommandPrimitive.Input
bind:value bind:value
class={cn( class={cn(

View file

@ -2,6 +2,7 @@
import { Dialog as DialogPrimitive } from 'bits-ui'; import { Dialog as DialogPrimitive } from 'bits-ui';
import * as Dialog from '.'; import * as Dialog from '.';
import { cn, flyAndScale } from '$lib/utils'; import { cn, flyAndScale } from '$lib/utils';
import { Cross2 } from 'radix-icons-svelte';
type $$Props = DialogPrimitive.ContentProps; type $$Props = DialogPrimitive.ContentProps;
@ -26,9 +27,9 @@
> >
<slot /> <slot />
<DialogPrimitive.Close <DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-3 my-auto aspect-square rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none" class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
> >
<iconify-icon icon="basil:cross-solid" class="text-2xl"></iconify-icon> <Cross2 class="h-4 w-4" />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View file

@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import 'carta-md/dark.css';
import '$lib/styles/discord.scss';
import { emoji } from '@cartamd/plugin-emoji'; import { emoji } from '@cartamd/plugin-emoji';
import { code } from '@cartamd/plugin-code'; import { code } from '@cartamd/plugin-code';
import PlusCircled from './assets/PlusIcon.svelte'; import { PlusCircled } from 'radix-icons-svelte';
import '$lib/styles/discord.scss';
const carta = new Carta({ const carta = new Carta({
sanitizer: false,
disableIcons: true, disableIcons: true,
extensions: [ extensions: [
emoji(), emoji(),
@ -16,8 +15,8 @@
components: [ components: [
{ {
component: PlusCircled, component: PlusCircled,
parent: 'input', props: { class: 'discord-plus-icon' },
props: {} parent: 'input'
} }
] ]
} }
@ -25,4 +24,5 @@
}); });
</script> </script>
<MarkdownEditor placeholder="Send a message to @someone" mode="tabs" theme="discord" {carta} /> <CartaEditor placeholder="Send a message to @someone" mode="tabs" theme="discord" {carta}
></CartaEditor>

View file

@ -1,14 +1,13 @@
<script lang="ts"> <script lang="ts">
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { attachment } from '@cartamd/plugin-attachment'; import { attachment } from '@cartamd/plugin-attachment';
import { emoji } from '@cartamd/plugin-emoji'; import { emoji } from '@cartamd/plugin-emoji';
import { slash } from '@cartamd/plugin-slash'; import { slash } from '@cartamd/plugin-slash';
import { code } from '@cartamd/plugin-code'; import { code } from '@cartamd/plugin-code';
import 'carta-md/dark.css';
import '$lib/styles/github.scss'; import '$lib/styles/github.scss';
const carta = new Carta({ const carta = new Carta({
sanitizer: false,
extensions: [ extensions: [
attachment({ attachment({
async upload() { async upload() {
@ -21,10 +20,7 @@
] ]
}); });
export let value = `This is an example inspired by [GitHub](https://github.com) export let value = 'This is an example inspired by [GitHub](https://github.com)';
\`\`\`js
console.log('Hello, World!');
\`\`\``;
</script> </script>
<MarkdownEditor bind:value mode="tabs" theme="github" {carta} /> <CartaEditor bind:value mode="tabs" theme="github" {carta} />

View file

@ -1,14 +1,13 @@
<script lang="ts"> <script lang="ts">
import { Carta, MarkdownEditor, Markdown } from 'carta-md'; import { Carta, CartaEditor, CartaViewer } from 'carta-md';
import placeholder from './math-stack-exchange-placeholder.tex?raw'; import placeholder from './math-stack-exchange-placeholder.tex?raw';
import { math } from '@cartamd/plugin-math'; import { math } from '@cartamd/plugin-math';
import { tikz } from '@cartamd/plugin-tikz'; import { tikz } from '@cartamd/plugin-tikz';
import 'carta-md/dark.css';
import '$lib/styles/math-stack-exchange.scss'; import '$lib/styles/math-stack-exchange.scss';
import 'katex/dist/katex.min.css'; import 'katex/dist/katex.min.css';
const carta = new Carta({ const carta = new Carta({
sanitizer: false,
extensions: [ extensions: [
math(), math(),
tikz({ tikz({
@ -31,9 +30,9 @@
</script> </script>
<div class="math-stack-exchange-container"> <div class="math-stack-exchange-container">
<MarkdownEditor bind:value mode="tabs" theme="math-stack-exchange" {carta} /> <CartaEditor bind:value mode="tabs" theme="math-stack-exchange" {carta} />
{#key value} {#key value}
<Markdown theme="math-stack-exchange" {value} {carta} /> <CartaViewer theme="math-stack-exchange" {value} {carta} />
{/key} {/key}
</div> </div>

View file

@ -1,3 +0,0 @@
<div class="discord-plus-icon">
<iconify-icon icon="radix-icons:plus-circled" class="text-2xl"></iconify-icon>
</div>

View file

@ -1,4 +1,4 @@
import flexsearch from 'flexsearch'; import { Document } from 'flexsearch';
import type { SvelteComponent } from 'svelte'; import type { SvelteComponent } from 'svelte';
export interface SearchResult { export interface SearchResult {
@ -19,7 +19,7 @@ export type EnrichedSearchResult = SearchResult & {
}; };
export async function initializeSearch() { export async function initializeSearch() {
const indexedPages = new flexsearch.Document<SearchResult, true>({ const indexedPages = new Document<SearchResult, true>({
tokenize: 'full', tokenize: 'full',
cache: true, cache: true,
context: true, context: true,

View file

@ -27,7 +27,6 @@
.carta-font-code { .carta-font-code {
font-family: 'Fira Code', monospace; font-family: 'Fira Code', monospace;
caret-color: white; caret-color: white;
font-size: 1.1rem;
} }
.carta-toolbar { .carta-toolbar {
@ -42,6 +41,10 @@
height: 1.75rem; height: 1.75rem;
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
} }
[class*='shj-lang-'] {
background: transparent;
}
} }
// Plugin emoji // Plugin emoji
@ -76,8 +79,3 @@
background: $background-contrast; background: $background-contrast;
} }
} }
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}

View file

@ -28,14 +28,15 @@
.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 {
height: 2.5rem; height: 2.5rem;
background-color: $background-light; background-color: $background-light;
border-bottom: 1px solid $border;
padding-right: 12px;
border-top-left-radius: 0.5rem; border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem; border-top-right-radius: 0.5rem;
@ -50,12 +51,6 @@
} }
} }
.carta-toolbar-left button,
.carta-toolbar-right,
.carta-filler {
border-bottom: 1px solid $border;
}
.carta-toolbar-left { .carta-toolbar-left {
& > *:first-child { & > *:first-child {
border-top-left-radius: 0.5rem; border-top-left-radius: 0.5rem;
@ -67,12 +62,10 @@
font-size: 0.95rem; font-size: 0.95rem;
} }
button {
height: 100%;
}
.carta-active { .carta-active {
height: 100%;
background-color: $background; background-color: $background;
transform: translateY(1px);
color: white; color: white;
border-right: 1px solid $border; border-right: 1px solid $border;
@ -84,37 +77,8 @@
} }
} }
.carta-toolbar-right { [class*='shj-lang-'] {
padding-right: 12px; background: transparent;
}
.carta-icons-menu {
padding: 8px;
border: 1px solid $border;
border-radius: 6px;
min-width: 180px;
background: $background;
.carta-icon-full {
padding-left: 6px;
padding-right: 6px;
margin-top: 2px;
&:first-child {
margin-top: 0;
}
&:hover {
color: white;
background-color: $border;
}
span {
margin-left: 6px;
color: white;
font-size: 0.85rem;
}
}
} }
} }
@ -196,8 +160,3 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
} }
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}

View file

@ -43,21 +43,9 @@
} }
code { code {
@apply rounded bg-neutral-800 px-1 text-neutral-50; @apply rounded;
} &:not([class*='language-']) {
@apply bg-neutral-800 px-1 text-neutral-100;
p,
h1,
h2,
h3 {
code {
@apply px-1;
} }
} }
.carta-editor code {
font-family: 'Fira Code', monospace;
background: transparent;
padding: 0;
}
} }

View file

@ -34,7 +34,6 @@
.carta-font-code { .carta-font-code {
font-family: 'Fira Code', monospace; font-family: 'Fira Code', monospace;
caret-color: white; caret-color: white;
font-size: 1.1rem;
} }
.carta-toolbar { .carta-toolbar {
@ -65,34 +64,9 @@
.carta-toolbar-right { .carta-toolbar-right {
justify-content: flex-start; justify-content: flex-start;
} }
}
.carta-icons-menu { [class*='shj-lang-'] {
padding: 8px; background: transparent;
border: 1px solid $border;
border-radius: 6px;
min-width: 180px;
background: $background;
.carta-icon-full {
padding-left: 6px;
padding-right: 6px;
margin-top: 2px;
&:first-child {
margin-top: 0;
}
&:hover {
color: white;
background-color: $border;
}
span {
margin-left: 6px;
color: white;
font-size: 0.85rem;
}
} }
} }
@ -101,8 +75,3 @@
padding: 1rem; padding: 1rem;
} }
} }
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}

View file

@ -54,41 +54,3 @@ export const flyAndScale = (
easing: cubicOut easing: cubicOut
}; };
}; };
export const throttle = <R, A extends unknown[]>(
fn: (...args: A) => R,
delay: number
): [(...args: A) => R | undefined, () => void] => {
let wait = false;
let timeout: undefined | number;
let cancelled = false;
return [
(...args: A) => {
if (cancelled) return undefined;
if (wait) return undefined;
const val = fn(...args);
wait = true;
timeout = window.setTimeout(() => {
wait = false;
}, delay);
return val;
},
() => {
cancelled = true;
clearTimeout(timeout);
}
];
};
export function debounce<T extends unknown[]>(cb: (...args: T) => unknown, wait = 1000) {
let timeout: NodeJS.Timeout;
return (...args: T) => {
clearTimeout(timeout);
timeout = setTimeout(() => cb(...args), wait);
};
}

View file

@ -17,15 +17,9 @@ new Carta({
}); });
``` ```
### `gfmOptions`
Type: `GfmOptions`
GitHub Flavored Markdown options.
### `extensions` ### `extensions`
Type: `Extension[]` Type: `CartaExtension[]`
List of extensions(plugins) to use. List of extensions(plugins) to use.
@ -40,19 +34,13 @@ Defaults to 300ms.
Type: `DefaultShortcutId[] | true` Type: `DefaultShortcutId[] | true`
Remove default shortcuts by id. You can use `true` to disable all of them. Remove shortcuts by id. You can use `true` to disable all of them.
### `disableIcons`
Type: `DefaultIconId[] | true`
Remove default icons by id. You can use `true` to disable all of them.
### `disablePrefixes` ### `disablePrefixes`
Type: `DefaultPrefixId[] | true` Type: `DefaultPrefixId[] | true`
Remove default prefixes by id. You can use `true` to disable all of them. Remove prefixes by id. You can use `true` to disable all of them.
### `historyOptions` ### `historyOptions`
@ -78,21 +66,9 @@ Type: `(html: string) => void`
HTML sanitizer. See [here]({base}/getting-started#sanitization) for more details. HTML sanitizer. See [here]({base}/getting-started#sanitization) for more details.
### `shikiOptions` # `CartaEditor` options
Type: `ShikiOptions` List of options that can be used in the `<CartaEditor>` component.
Highlighter(Shiki) options.
### `theme`
Type: `Theme | DualTheme`
Shiki theme to use to highlight Markdown.
# `MarkdownEditor` options
List of options that can be used in the `<MarkdownEditor>` component.
### `carta` ### `carta`
@ -136,23 +112,9 @@ Type: `string`
Set the textarea placeholder. Set the textarea placeholder.
### `textarea` # `CartaViewer` options
Type: `TextAreaProps` (extends `Record<string, unknown>`) List of options that can be used in the `<CartaViewer>` component.
Additional properties that will be used in the textarea used under the hood in the editor.
`class`, `placeholder` and `value` are not allowed. Use the corresponding editor properties
instead.
### `labels`
Type: `Partial<Labels>`
Can be used to provide custom text for labels in the editor.
# `Markdown` options
List of options that can be used in the `<Markdown>` component.
### `carta` ### `carta`

View file

@ -7,14 +7,14 @@ title: Extension
import Code from '$lib/components/code/Code.svelte'; import Code from '$lib/components/code/Code.svelte';
</script> </script>
# `Plugin` properties # `CartaExtension` properties
You can easily extend Carta by creating custom plugins. You can easily extend Carta by creating custom plugins.
<Code> <Code>
```ts ```ts
const ext: Plugin = { const ext: CartaExtension = {
// ... // ...
}; };
@ -25,47 +25,11 @@ const carta = new Carta({
</Code> </Code>
Here are all the `Plugin` properties: Here are all the `CartaExtension` properties:
### `transformers` ### `markedExtensions`
Type: `UnifiedTransformer` List of marked extensions. For more information check out [Marked docs](https://marked.js.org/using_pro).
Remark or Rehype transformers.
#### `UnifiedTransformer.execution`
Type: `'sync' | 'async'`
If you specify async, this transformer won't be available for SSR.
#### `UnifiedTransformer.type`
Type: `'remark' | 'rehype'`
This determines at which step the transformer will operate, whether on Remark, on a Markdown-based syntax tree, or Rehype, on a HTML-based one.
#### `UnifiedTransformer.transform`
Type: `({ processor, carta }) => void`
The actual processor, can be async if the execution is specified as such.
<Code>
```ts
{
execution: 'sync',
type: 'rehype',
transform({ processor }) {
processor
.use(rehypeSlug)
.use(rehypeAutolinkHeadings);
}
}
```
</Code>
### `shortcuts` ### `shortcuts`
@ -99,7 +63,7 @@ Set of keys, corresponding to the `e.key` of `KeyboardEvent`s, but lowercase.
#### `KeyboardShortcut.action` #### `KeyboardShortcut.action`
Type: `(input: InputEnhancer) => void` Type: `(input: CartaInput) => void`
Shortcut callback. Shortcut callback.
@ -109,14 +73,14 @@ Prevent saving the current state in history.
### `icons` ### `icons`
Type: `Icon[]` Type: `CartaIcon[]`
Additional toolbar icons. For example: Additional toolbar icons. For example:
<Code> <Code>
```ts ```ts
const icon: Icon = { const icon: CartaIcon = {
id: 'heading', id: 'heading',
action: (input) => input.toggleLinePrefix('###'), action: (input) => input.toggleLinePrefix('###'),
component: HeadingIcon component: HeadingIcon
@ -125,19 +89,19 @@ const icon: Icon = {
</Code> </Code>
#### `Icon.id` #### `CartaIcon.id`
Type: `string` Type: `string`
Id of the icon. Id of the icon.
#### `Icon.action` #### `CartaIcon.action`
Type: `(input: InputEnhancer) => void` Type: `(input: CartaInput) => void`
Click callback. Click callback.
#### `Icon.component` #### `CartaIcon.component`
Type: `ComponentType` (SvelteComponent) Type: `ComponentType` (SvelteComponent)
@ -198,15 +162,15 @@ const prefix: Prefix = {
### `listeners` ### `listeners`
Type: `Listener[]` Type: `CartaListener[]`
Textarea event listeners. Has an additional `carta-render` and `carta-render-ssr` events keys. Textarea event listeners. Has an additional `carta-render` and `carta-render-ssr` events keys.
<Code> <Code>
```ts ```ts
const click: Listener = ['click', () => console.log('I was clicked!')]; const click: CartaListener = ['click', () => console.log('I was clicked!')];
const render: Listener = [ const render: CartaListener = [
'carta-render', 'carta-render',
(e) => { (e) => {
const carta = e.detail.carta; const carta = e.detail.carta;
@ -222,39 +186,33 @@ const render: Listener = [
### `components` ### `components`
Type: `ExtensionComponent[]` Type: `CartaExtensionComponent[]`
Additional components to be added to the editor or viewer. Additional components to be added to the editor or viewer.
#### `ExtensionComponent<T>.component` #### `CartaExtensionComponent<T>.component`
Type: `typeof SvelteComponentTyped<T & { carta: Carta }>` Type: `typeof SvelteComponentTyped<T & { carta: Carta }>`
Svelte components that exports `carta: Carta` and all the other properties specified as the generic parameter and in `props`. Svelte components that exports `carta: Carta` and all the other properties specified as the generic parameter and in `props`.
#### `ExtensionComponent<T>.props` #### `CartaExtensionComponent<T>.props`
Type: `T` Type: `T`
Properties that will be handed to the component. Properties that will be handed to the component.
#### `ExtensionComponent<T>.parent` #### `CartaExtensionComponent<T>.parent`
Type: `MaybeArray<'editor' | 'input' | 'renderer' | 'preview'>` Type: `MaybeArray<'editor' | 'input' | 'renderer' | 'preview'>`
Where the element will be placed. Where the element will be placed.
### `grammarRules`
Type: `GrammarRule[]`
Custom Markdown TextMate grammar rules for Shiki. They will be injected into the language.
### `highlightRules` ### `highlightRules`
Type: `HighlightingRule[]` Type: `ShjLanguageDefinition`
Custom highlighting rules for ShiKi. They will be injected into the selected theme. Custom markdown highlighting rules. See [Speed-Highlight Wiki](https://github.com/speed-highlight/core/wiki/Create-or-suggest-new-languages) for more info.
### `onLoad` ### `onLoad`

View file

@ -40,52 +40,3 @@ Svelte action that allows you to bind a specific element to the caret position.
<!-- ... --> <!-- ... -->
</div> </div>
``` ```
## `Carta.highlighter`
Get the Shiki highlighter.
```ts
const highlighter = await carta.highlighter();
const userTheme = carta.theme;
```
Here are some other highlight related utilities:
### `isBundleLanguage`
Checks if a language is a bundled language.
```ts
export const isBundleLanguage = (lang: string): lang is BundledLanguage;
```
### `isBundleTheme`
Checks if a theme is a bundled theme.
```ts
export const isBundleTheme = (theme: string): theme is BundledTheme;
```
### `isDualTheme`
Checks if a theme is a dual theme.
```ts
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme;
```
### `isSingleTheme`
```ts
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme;
```
### `isThemeRegistration`
Checks if a theme is a theme registration.
```ts
export const isThemeRegistration = (theme: Theme): theme is ThemeRegistration;
```

View file

@ -1,83 +0,0 @@
---
title: Community Plugins
section: Overview
---
<script>
import PluginLink from '$lib/components/link/PluginLink.svelte';
import Code from '$lib/components/code/Code.svelte';
</script>
Here are is a list of several plugins developed by the community:
<PluginLink
npmLink="https://www.npmjs.com/package/carta-plugin-video"
githubLink="https://github.com/maisonsmd/carta-plugin-video">
### `carta-plugin-video`
</PluginLink>
> Adds ability to render online video from Youtube or Vimeo.
<Code>
```
npm i carta-plugin-video
```
</Code>
<PluginLink
npmLink="https://www.npmjs.com/package/carta-plugin-imsize"
githubLink="https://github.com/maisonsmd/carta-plugin-imsize">
### `carta-plugin-imsize`
</PluginLink>
> Adds ability to render images in specific sizes.
<Code>
```
npm i carta-plugin-imsize
```
</Code>
<PluginLink
npmLink="https://www.npmjs.com/package/carta-plugin-ins-del"
githubLink="https://github.com/maisonsmd/carta-plugin-ins-del">
### `carta-plugin-ins-del`
</PluginLink>
> `<ins>` and `<del>` tags support
<Code>
```
npm i carta-plugin-ins-del
```
</Code>
<PluginLink
npmLink="https://www.npmjs.com/package/carta-plugin-subscript"
githubLink="https://github.com/maisonsmd/carta-plugin-subscript">
### `carta-plugin-subscript`
</PluginLink>
> Adds ability to render subscripts and superscripts.
<Code>
```
npm i carta-plugin-subscript
```
</Code>

View file

@ -43,68 +43,11 @@ While the core styles are embedded in the Svelte components, the others can be s
### Using multiple themes ### Using multiple themes
By using the `theme` property in `<MarkdownEditor>` you can differentiate the themes of multiple editors. By using the `theme` property in the editor you can differentiate the themes of multiple editors.
## Dark mode ## Changing Markdown color theme
When using dark mode, there are two different themes that have to be changed: the editor theme and the one used for syntax highlighting: Carta uses [Speed Highlight JS](https://github.com/speed-highlight/core) for syntax highlighting. Two default themes are included in the core package, `light.css` and `dark.css`, and others can be found on the Speed Highlight [GitHub](https://github.com/speed-highlight/core/tree/main/src/themes), but you can also easily create your own.
<Code>
```css
/* Editor dark mode */
/* Only if you are using the default theme */
html.dark .carta-theme__default {
--border-color: var(--border-color-dark);
--selection-color: var(--selection-color-dark);
--focus-outline: var(--focus-outline-dark);
--hover-color: var(--hover-color-dark);
--caret-color: var(--caret-color-dark);
--text-color: var(--text-color-dark);
}
/* Code dark mode */
/* Only if you didn't specify a custom code theme */
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}
```
</Code>
## Changing Markdown input color theme
Carta uses [Shiki](https://shiki.matsu.io/) for syntax highlighting. Two default themes are included in the core package, which are set as a [dual theme](https://shiki.matsu.io/guide/dual-themes) to support light and dark mode. If you plan to use a custom one with light/dark modes, make sure to use a dual theme as well.
You can change theme in the options:
<Code>
```ts
const carta = new Carta({
// ...
theme: 'github-dark'
});
```
</Code>
If you use a [custom theme](https://shiki.matsu.io/guide/load-theme)(or a custom language), you need to provide it inside the options, so that it gets loaded into the highlighter:
<Code>
```ts
const carta = new Carta({
// ...
shikiOptions: {
langs: // ...
themes: // ...
}
})
```
</Code>
## Markdown stylesheets ## Markdown stylesheets

View file

@ -37,8 +37,9 @@ Setup a basic editor:
```svelte ```svelte
<script> <script>
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import 'carta-md/default.css'; /* Default theme */ import 'carta-md/default.css'; /* Default theme */
import 'carta-md/light.css'; /* Markdown input theme */
const carta = new Carta({ const carta = new Carta({
// Remember to use a sanitizer to prevent XSS attacks! // Remember to use a sanitizer to prevent XSS attacks!
@ -49,13 +50,12 @@ Setup a basic editor:
let value = ''; let value = '';
</script> </script>
<MarkdownEditor {carta} bind:value /> <CartaEditor {carta} bind:value />
<style> <style>
/* Set your custom monospace font */ /* Set your custom monospace font */
:global(.carta-font-code) { :global(.carta-font-code) {
font-family: '...', monospace; font-family: '...', monospace;
font-size: 1.1rem;
} }
</style> </style>
``` ```
@ -68,7 +68,7 @@ Or, if you just want to render content:
```svelte ```svelte
<script> <script>
import { Carta, Markdown } from 'carta-md'; import { Carta, CartaViewer } from 'carta-md';
const carta = new Carta({ const carta = new Carta({
/* ... */ /* ... */
@ -77,7 +77,7 @@ Or, if you just want to render content:
let value = '...'; let value = '...';
</script> </script>
<Markdown {carta} {value} /> <CartaViewer {carta} {value} />
``` ```
</Code> </Code>
@ -93,16 +93,16 @@ Since Carta operates both on the server and the client, you'd need a sanitizer a
```svelte ```svelte
<script> <script>
// Your other stuff... // Your other stuff...
import DOMPurify from 'isomorphic-dompurify'; import { sanitize } from 'isomorphic-dompurify';
const carta = new Carta({ const carta = new Carta({
sanitizer: DOMPurify.sanitize sanitizer: sanitize
}); });
let value = ''; let value = '';
</script> </script>
<MarkdownEditor {carta} bind:value /> <CartaEditor {carta} bind:value />
``` ```
</Code> </Code>

View file

@ -5,21 +5,20 @@ section: Overview
<script> <script>
import * as Card from "$lib/components/ui/card"; import * as Card from "$lib/components/ui/card";
import * as Icon from "radix-icons-svelte";
</script> </script>
> Modern, lightweight, powerful Markdown Editor. > Swiftly edit and render Markdown, with no overhead.
Carta is a lightweight, fast and extensible Svelte Markdown editor and viewer, designed for flexibility. It works natively in SvelteKit, and supports Server Side Rendering. Carta is a lightweight, fast and extensible Svelte Markdown editor and viewer, designed for flexibility. It works natively in SvelteKit, and supports Server Side Rendering.
## Features ## Core Features
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/)); - **Lightweight**: no code editor is included, just a textarea with syntax highlighting, with Markdown related utilities.
- 🛠️ Toolbar (extensible); - **SSR compatible**: works great with SvelteKit.
- ⌨️ Keyboard **shortcuts** (extensible); - **Keyboard shortcuts**: extensible and configurable.
- 📦 Supports **[+150 plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark. - **Toolbar**: add or remove buttons according to your needs.
- 🔀 Scroll sync; - **Plugins friendly**: easily create your own extension.
- ✅ Accessibility friendly;
- 🖥️ **SSR** compatible;
## Official Plugins ## Official Plugins
@ -31,7 +30,7 @@ Carta comes with a set of official plugins for the most common use cases.
<Card.Root href="/plugins/math"> <Card.Root href="/plugins/math">
<Card.Header> <Card.Header>
<iconify-icon icon="tabler:math" class="text-3xl text-sky-300"></iconify-icon> <Icon.FontFamily class="w-8 h-8 text-sky-300" />
<Card.Title>Math</Card.Title> <Card.Title>Math</Card.Title>
<Card.Description>Support for KaTex expressions.</Card.Description> <Card.Description>Support for KaTex expressions.</Card.Description>
</Card.Header> </Card.Header>
@ -39,7 +38,7 @@ Carta comes with a set of official plugins for the most common use cases.
<Card.Root href="/plugins/code"> <Card.Root href="/plugins/code">
<Card.Header> <Card.Header>
<iconify-icon icon="fluent:code-16-filled" class="text-3xl text-sky-300"></iconify-icon> <Icon.Code class="w-8 h-8 text-sky-300" />
<Card.Title>Code</Card.Title> <Card.Title>Code</Card.Title>
<Card.Description>Code blocks syntax highlighting.</Card.Description> <Card.Description>Code blocks syntax highlighting.</Card.Description>
</Card.Header> </Card.Header>
@ -47,7 +46,7 @@ Carta comes with a set of official plugins for the most common use cases.
<Card.Root href="/plugins/emoji"> <Card.Root href="/plugins/emoji">
<Card.Header> <Card.Header>
<iconify-icon icon="mingcute:emoji-line" class="text-3xl text-sky-300"></iconify-icon> <Icon.Face class="w-8 h-8 text-sky-300" />
<Card.Title>Emoji</Card.Title> <Card.Title>Emoji</Card.Title>
<Card.Description>Embed emojis in Markdown.</Card.Description> <Card.Description>Embed emojis in Markdown.</Card.Description>
</Card.Header> </Card.Header>
@ -55,7 +54,7 @@ Carta comes with a set of official plugins for the most common use cases.
<Card.Root href="/plugins/slash"> <Card.Root href="/plugins/slash">
<Card.Header> <Card.Header>
<iconify-icon icon="tabler:slash" class="text-3xl text-sky-300"></iconify-icon> <Icon.Slash class="w-8 h-8 text-sky-300" />
<Card.Title>Slash</Card.Title> <Card.Title>Slash</Card.Title>
<Card.Description>Support for slash commands.</Card.Description> <Card.Description>Support for slash commands.</Card.Description>
</Card.Header> </Card.Header>
@ -63,7 +62,7 @@ Carta comes with a set of official plugins for the most common use cases.
<Card.Root href="/plugins/tikz"> <Card.Root href="/plugins/tikz">
<Card.Header> <Card.Header>
<iconify-icon icon="mdi:draw-pen" class="text-3xl text-sky-300"></iconify-icon> <Icon.Cube class="w-8 h-8 text-sky-300" />
<Card.Title>TikZ</Card.Title> <Card.Title>TikZ</Card.Title>
<Card.Description>Support for TikZ/PgfPlots diagrams.</Card.Description> <Card.Description>Support for TikZ/PgfPlots diagrams.</Card.Description>
</Card.Header> </Card.Header>
@ -71,28 +70,12 @@ Carta comes with a set of official plugins for the most common use cases.
<Card.Root href="/plugins/attachment"> <Card.Root href="/plugins/attachment">
<Card.Header> <Card.Header>
<iconify-icon icon="tdesign:attach" class="text-3xl text-sky-300"></iconify-icon> <Icon.File class="w-8 h-8 text-sky-300" />
<Card.Title>Attachment</Card.Title> <Card.Title>Attachment</Card.Title>
<Card.Description>Handle text attachments.</Card.Description> <Card.Description>Handle text attachments.</Card.Description>
</Card.Header> </Card.Header>
</Card.Root> </Card.Root>
<Card.Root href="/plugins/anchor">
<Card.Header>
<iconify-icon icon="mingcute:link-fill" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Anchor</Card.Title>
<Card.Description>Add anchor links to headings.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/community-plugins">
<Card.Header>
<iconify-icon icon="ph:stack-fill" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Community Plugins</Card.Title>
<Card.Description>Explore plugins from the community.</Card.Description>
</Card.Header>
</Card.Root>
</div> </div>
## Examples ## Examples
@ -105,7 +88,7 @@ A list of examples inspired by popular platforms.
<Card.Root href="/examples#github"> <Card.Root href="/examples#github">
<Card.Header> <Card.Header>
<iconify-icon icon="mdi:github" class="text-3xl text-sky-300" ></iconify-icon> <Icon.GithubLogo class="w-8 h-8 text-sky-300" />
<Card.Title>GitHub</Card.Title> <Card.Title>GitHub</Card.Title>
<Card.Description>Inspired by GitHub.</Card.Description> <Card.Description>Inspired by GitHub.</Card.Description>
</Card.Header> </Card.Header>
@ -113,7 +96,7 @@ A list of examples inspired by popular platforms.
<Card.Root href="/examples#discord"> <Card.Root href="/examples#discord">
<Card.Header> <Card.Header>
<iconify-icon icon="ic:baseline-discord" class="text-3xl text-sky-300" ></iconify-icon> <Icon.DiscordLogo class="w-8 h-8 text-sky-300" />
<Card.Title>Discord</Card.Title> <Card.Title>Discord</Card.Title>
<Card.Description>Inspired by Discord.</Card.Description> <Card.Description>Inspired by Discord.</Card.Description>
</Card.Header> </Card.Header>
@ -121,7 +104,7 @@ A list of examples inspired by popular platforms.
<Card.Root href="/examples#math-stack-exchange"> <Card.Root href="/examples#math-stack-exchange">
<Card.Header> <Card.Header>
<iconify-icon icon="fluent:math-formula-16-filled" class="text-3xl text-sky-300" ></iconify-icon> <Icon.Cube class="w-8 h-8 text-sky-300" />
<Card.Title>Math Stack Exchange</Card.Title> <Card.Title>Math Stack Exchange</Card.Title>
<Card.Description>Inspired by Math Stack Exchange.</Card.Description> <Card.Description>Inspired by Math Stack Exchange.</Card.Description>
</Card.Header> </Card.Header>

View file

@ -1,57 +0,0 @@
---
title: Migration Guide
section: Overview
---
# Major Changes
## Removal of Marked
Marked has been replaced with a combination of Unified, Remark and Rehype. If you previously used a custom plugin with it, you'll have to update it manually. Otherwise, all builtin plugins have already been updated. Make sure to **update** them!
Some plugins now have a different implementation and their options have changed. Those plugins are [plugin-math](https://beartocode.github.io/carta/plugins/math) and [plugin-anchor](https://beartocode.github.io/carta/plugins/anchor).
## Syntax highlighter update
SpeedHighlight has been replaced with [Shiki](https://shiki.matsu.io/). It now offers support for more languages, themes, and extensibility.
Make sure to remove previous themes imports, as Shiki uses JS based ones.
```ts
import 'carta-md/light.css'; // 👈 To be removed!
```
And also update the default theme. Previous based selectors should be removed:
```css
/* 👇 To be removed! */
[class*='shj-lang-'] {
/* ... */
}
```
## Removed verbose prefixes
Many exports have been renamed to make them less verbose:
- `CartaEditor` -> `MarkdownEditor` (old one still supported);
- `CartaRenderer` -> `Markdown` (old one still supported);
- `CartaEvent` -> `Event`;
- `CartaEventType` -> `EventType`;
- `CartaExtension` -> `Plugin`;
- `CartaExtensionComponent` -> `ExtensionComponent`;
- `CartaOptions` -> `Options`;
- `CartaHistory` -> `TextAreaHistory`;
- `CartaHistoryOptions` -> `TextAreaHistoryOptions`;
- `CartaIcon` -> `Icon`;
- `CartaListener` -> `Listener`;
- `CartaInput` -> `InputEnhancer`;
- `CartaRenderer` -> `Renderer`;
- `CartaLabels` -> `Labels`;
# Minor Changes
- If you don't use a sanitizer, you need to explicitly set it to `false`;
- Removed deprecated option `cartaRef` and `shjRef` for extensions;
- Removed deprecated options `postProcess` for `plugin-tikz`;
- `Carta.options` are no longer available.

View file

@ -1,70 +0,0 @@
---
section: Plugins
title: Anchor
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
This plugin adds `id` attributes and permalinks to headings.
## Installation
<Code>
```
npm i @cartamd/plugin-anchor
```
</Code>
## Setup
### Styles
Import the default theme, or create you own:
<Code>
```ts
import '@cartamd/plugin-anchor/default.css';
```
</Code>
### Extension
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { anchor } from '@cartamd/plugin-anchor';
const carta = new Carta({
extensions: [anchor()]
});
</script>
<MarkdownEditor {carta} />
```
</Code>
## Options
Here are the options you can pass to `anchor()`:
```ts
export interface AnchorExtensionOptions {
/**
* rehype-slug options.
*/
slug?: SlugOptions;
/**
* rehype-autolink-headings options.
*/
autolink?: AutolinkOptions;
}
```

View file

@ -35,7 +35,7 @@ import '@cartamd/plugin-attachment/default.css';
```svelte ```svelte
<script> <script>
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { attachment } from '@cartamd/plugin-attachment'; import { attachment } from '@cartamd/plugin-attachment';
const carta = new Carta({ const carta = new Carta({
@ -49,7 +49,7 @@ import '@cartamd/plugin-attachment/default.css';
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
``` ```
</Code> </Code>

View file

@ -7,7 +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**. It uses the same highlighter from the core package(Shiki). This plugin adds support for code blocks **syntax highlighting**.
## Installation ## Installation
@ -19,6 +19,8 @@ npm i @cartamd/plugin-code
</Code> </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
@ -35,34 +37,34 @@ import '@cartamd/plugin-code/default.css';
### Using the default highlighter ### Using the default highlighter
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options. Carta comes with a default highlighter that matches the one used to highlight Markdown in the editor and is used by default.
The theme is the same as the one used in the main Carta package (`carta-md/light.css` or `carta-md/dark.css`).
[Here](https://github.com/speed-highlight/core/tree/main/src/themes) you can find other themes.
### Using a custom highlighter
You can also provide a custom highlighter, that can be either sync or async.
<Code> <Code>
```ts ```ts
const carta = new Carta({ code({
// ... customHighlight: {
extensions: [ highlighter: (code, lang) => myCustomHighlighter(code, lang),
code({ langPrefix: 'my-highlighter-'
theme: 'ayu-light' }
})
]
}); });
``` ```
</Code> </Code>
### Using a custom highlighter
It is no longer possible to specify a custom highlighter in this plugin. However, there are many different [Remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) that provide syntax highlighting.
### Extension ### Extension
<Code> <Code>
```svelte ```svelte
<script> <script>
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { code } from '@cartamd/plugin-code'; import { code } from '@cartamd/plugin-code';
const carta = new Carta({ const carta = new Carta({
@ -70,11 +72,46 @@ It is no longer possible to specify a custom highlighter in this plugin. However
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
``` ```
</Code> </Code>
## Options ## Options
The options you can pass to `code()` extend the ones provided by [Shiki](https://shiki.matsu.io/guide/transformers). Here are the options you can pass to `code()`:
```ts
interface CodeExtensionOptions {
/**
* Default language when none is provided.
*/
defaultLanguage?: string;
/**
* Whether to autodetect a language when none is provided.
* Overwritten by `defaultLanguage`.
*/
autoDetect?: string;
/**
* Line numbering.
* @defaults false.
*/
lineNumbering?: boolean;
/**
* Options for custom syntax highlighting.
*/
customHighlight?: {
/**
* Custom highlight function. Beware that you'll have to provide your own styles.
* This function needs to convert a string of code into html.
*/
highlighter: (code: string, lang: string) => string | Promise<string>;
/**
* The language tag found immediately after the code block opening marker is
* appended to this to form the class attribute added to the `<code>` element.
*/
langPrefix: string;
};
}
```

View file

@ -39,7 +39,7 @@ import '@cartamd/plugin-emoji/default.css';
```svelte ```svelte
<script> <script>
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { emoji } from '@cartamd/plugin-emoji'; import { emoji } from '@cartamd/plugin-emoji';
const carta = new Carta({ const carta = new Carta({
@ -47,7 +47,7 @@ import '@cartamd/plugin-emoji/default.css';
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
``` ```
</Code> </Code>

View file

@ -5,7 +5,7 @@ title: Math
<script> <script>
import Code from '$lib/components/code/Code.svelte'; import Code from '$lib/components/code/Code.svelte';
import { Markdown, Carta } from 'carta-md'; import { CartaViewer, Carta } from 'carta-md';
import { math } from '@cartamd/plugin-math'; import { math } from '@cartamd/plugin-math';
import 'katex/dist/katex.css'; import 'katex/dist/katex.css';
@ -78,7 +78,7 @@ or by using a content delivery network:
```svelte ```svelte
<script> <script>
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { math } from '@cartamd/plugin-math'; import { math } from '@cartamd/plugin-math';
const carta = new Carta({ const carta = new Carta({
@ -86,7 +86,7 @@ or by using a content delivery network:
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
``` ```
</Code> </Code>
@ -103,7 +103,7 @@ Pythagorean theorem: $a^2+b^2=c^2$
</Code> </Code>
<Markdown {carta} value={inline} /> <CartaViewer {carta} value={inline} />
<br> <br>
@ -119,7 +119,7 @@ $$
</Code> </Code>
<Markdown {carta} value={block} /> <CartaViewer {carta} value={block} />
## Options ## Options
@ -131,6 +131,7 @@ interface MathExtensionOptions {
* Options for inline katex, eg: $a^2+b^2=c^2$ * Options for inline katex, eg: $a^2+b^2=c^2$
*/ */
inline?: { inline?: {
katexOptions?: KatexOptions;
/** /**
* @default control+m * @default control+m
*/ */
@ -143,18 +144,23 @@ interface MathExtensionOptions {
* $$ * $$
*/ */
block?: { block?: {
/**
* Tag the generated katex will be put into. Must have `display: block`.
*/
tag?: string;
/**
* Whether to center the generated expression.
* @default true
*/
center?: boolean;
/**
* Class for generated katex.
*/
class?: string;
/** /**
* @default ctrl+shift+m * @default ctrl+shift+m
*/ */
shortcut?: Set<string>; shortcut?: Set<string>;
katexOptions?: KatexOptions;
}; };
/**
* Options for remark-math
*/
remarkMath?: RemarkMathOptions;
/**
* Options for rehype-katex
*/
rehypeKatex?: RehypeKatexOptions;
}
``` ```

View file

@ -39,7 +39,7 @@ import '@cartamd/plugin-slash/default.css';
```svelte ```svelte
<script> <script>
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { slash } from '@cartamd/plugin-slash'; import { slash } from '@cartamd/plugin-slash';
const carta = new Carta({ const carta = new Carta({
@ -47,7 +47,7 @@ import '@cartamd/plugin-slash/default.css';
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
``` ```
</Code> </Code>

View file

@ -20,7 +20,7 @@ npm i @cartamd/plugin-tikz
## Important Notes ## Important Notes
1. This plugin requires the import of a **heavy** library (~7Mb), which is dynamically imported at runtime; 1. This plugin requires the import of a **heavy** library (~7Mb), which is dynamically imported at runtime;
2. Generated images are **not SSR compatible**, as they are rendered in the browser; 2. Generated images are **not ssr compatible**, as they are rendered in the browser;
3. You need to update your sanitizer to allow the specific tag: `<div type="text/tikz">`. 3. You need to update your sanitizer to allow the specific tag: `<div type="text/tikz">`.
## Setup ## Setup
@ -29,7 +29,7 @@ npm i @cartamd/plugin-tikz
```svelte ```svelte
<script> <script>
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { tikz } from '@cartamd/plugin-tikz'; import { tikz } from '@cartamd/plugin-tikz';
import '@cartamd/plugin-tikz/fonts.css'; import '@cartamd/plugin-tikz/fonts.css';
@ -38,7 +38,7 @@ npm i @cartamd/plugin-tikz
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
``` ```
</Code> </Code>

View file

@ -1,217 +0,0 @@
---
title: Using Svelte Components
section: Overview
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
Svelte components can be embedded into the rendered HTML to make certain elements interactive. However, they require a bit more work, as Remark is configured to only render static HTML. To get around this, the idea is to do the following:
1. Create a Unified plugin to isolate the targeted element;
2. Replace all the elements with the component, after every render.
## Example
Let's say we want to replace all hashtags, such as `#something`, with a custom component. Here is as example of how that could be achieved.
### Parsing the hashtags
First things first: we need to tell the parser that we want to parse hashtags as custom elements. To do this, it's useful to first install the following packages:
<Code>
```shell
npm i unist-util-visit
# Types
npm i -D unified hast
```
</Code>
Let's create a Unified plugin. The basic structure of a plugin is the following:
<Code>
```ts
import type { Plugin as UnifiedPlugin } from 'unified'
import { SKIP, visit } from 'unist-util-visit'
const unifiedPlugin: UnifiedPlugin<[], hast.Root> = () => {
return function (tree) {
// Visit every node in the syntax tree
visit(tree, (node, index, parent) => {
// Do something with the node
}
}
}
```
</Code>
We now want to parse text nodes, so that words such as `#pizza` and `#123` are separated from the rest. This is a possible implementation:
<Code>
```ts
const unifiedPlugin: UnifiedPlugin<[], hast.Root> = () => {
return function (tree) {
visit(tree, (node, index, parent) => {
// Skip code blocks and their children
if (node.type === 'element' && node.tagName === 'pre') return [SKIP];
// Skip non-text nodes
if (node.type !== 'text') return;
const text = node as hast.Text;
// Parse the text node and replace hashtags with spans
const regex = /#(\w+)/g;
const children: (hast.Element | hast.Text)[] = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(text.value))) {
const before = text.value.slice(lastIndex, match.index);
if (before) {
children.push({ type: 'text', value: before });
}
children.push({
type: 'element',
tagName: 'span',
properties: { type: 'hashtag', value: match[1] },
children: [{ type: 'text', value: match[0] }]
});
lastIndex = regex.lastIndex;
}
if (lastIndex < text.value.length) {
children.push({ type: 'text', value: text.value.slice(lastIndex) });
}
// Replace the text node with all the children
parent!.children.splice(index!, 1, ...children);
// Skip the children
return [SKIP, index! + children.length];
});
};
};
```
</Code>
If you want a more in-depth guide on writing Unified plugins, you can check out the official [documentation](https://unifiedjs.com/learn/guide/create-a-plugin/).
Notice that hashtags are now replaced with the following:
```html
<span type="hashtag" value="pizza"> #pizza </span>
```
### Configuring the transformer
Unified plugins need to be wrapped inside a `UnifiedTransformer` type, to be able to be used in Carta.
<Code>
```ts
import type { UnifiedTransformer } from 'carta-md';
const hashtagTransformer: UnifiedTransformer<'sync'> = {
execution: 'sync', // Sync, since the plugin is synchronous
type: 'rehype', // Rehype, since it operates on HTML
transform({ processor }) {
processor.use(unifiedPlugin);
}
};
```
</Code>
### Mounting the components
We now want to replace the generated hashtag placeholders with the following element:
<Code>
```svelte
<!-- Hashtag.svelte -->
<script>
export let value;
</script>
<button
on:click={() => {
console.log('Hashtag clicked!');
}}
>
#{value}
</button>
```
</Code>
To do that, we create a listener that:
1. Finds all the previous placeholders;
2. Mounts the component next to them;
3. Removes the placeholders.
<Code>
```ts
import type { Listener } from 'carta-md';
import Hashtag from './Hashtag.svelte';
const convertHashtags: Listener<'carta-render'> = [
'carta-render',
function onRender({ detail: { carta } }) {
const rendererContainer = carta.renderer?.container;
if (!rendererContainer) return;
// Find all hashtag spans and replace them with Svelte components
const hashtagSpans = rendererContainer.querySelectorAll('span[type="hashtag"]');
for (const span of hashtagSpans) {
const hashtag = span.getAttribute('value') ?? '';
new Hashtag({
target: span.parentElement!,
anchor: span,
props: { value: hashtag }
});
span.remove();
}
}
];
```
</Code>
### Using the plugin
Let's now create a Plugin with the transformer and the listener:
<Code>
```ts
import type { Plugin } from 'carta-md';
export const hashtag = (): Plugin => ({
transformers: [hashtagTransformer],
listeners: [convertHashtags]
});
```
</Code>
We can now use the plugin with the following:
```ts
import { Carta } from 'carta-md';
const carta = new Carta({
// ...
extensions: [hashtag()]
});
```
You can find the example source code [here](https://github.com/BearToCode/svelte-in-carta-example).

View file

@ -4,7 +4,6 @@
import Navbar from '$lib/components/navbar/Navbar.svelte'; import Navbar from '$lib/components/navbar/Navbar.svelte';
import Sidebar from '$lib/components/sidebar/Sidebar.svelte'; import Sidebar from '$lib/components/sidebar/Sidebar.svelte';
import Footer from '$lib/components/footer/Footer.svelte'; import Footer from '$lib/components/footer/Footer.svelte';
import { base } from '$app/paths';
import '../app.postcss'; import '../app.postcss';
</script> </script>
@ -13,7 +12,7 @@
<div class="filter-blur-xl min-h-screen w-full py-16 xl:py-24"> <div class="filter-blur-xl min-h-screen w-full py-16 xl:py-24">
<div <div
class="fixed bottom-0 left-0 right-0 top-0 z-[-1] backdrop-blur-2xl backdrop-filter" class="fixed bottom-0 left-0 right-0 top-0 z-[-1] backdrop-blur-2xl backdrop-filter"
style="background: url({base}/background.png) no-repeat center center; background-size: cover;" style="background: url(/background.png) no-repeat center center; background-size: cover;"
></div> ></div>
<MobileSidebar class="xl:hidden" /> <MobileSidebar class="xl:hidden" />
<div class="container relative mx-auto flex px-4 sm:px-6"> <div class="container relative mx-auto flex px-4 sm:px-6">
@ -24,6 +23,6 @@
<slot /> <slot />
<Footer /> <Footer />
</main> </main>
<HeaderTracker class="sticky top-24 hidden w-[15rem] flex-shrink-0 xl:block" /> <HeaderTracker class="sticky top-24 hidden w-[20rem] xl:block" />
</div> </div>
</div> </div>

View file

@ -1,3 +1 @@
import 'iconify-icon'; // Register iconify web components
export const prerender = true; export const prerender = true;

View file

@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
onMount(() => { onMount(() => {
goto(`${base}/introduction`); goto('./introduction');
}); });
</script> </script>

View file

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

View file

@ -1,7 +1,7 @@
import mdsvexConfig from './mdsvex.config.js'; import mdsvexConfig from './mdsvex.config.js';
import adapter from '@sveltejs/adapter-static'; import adapter from '@sveltejs/adapter-static';
import { mdsvex } from 'mdsvex'; import { mdsvex } from 'mdsvex';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {

View file

@ -31,7 +31,7 @@
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.2",
"ora": "^6.3.0", "ora": "^6.3.0",
"prettier": "3.1.0", "prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0", "prettier-plugin-svelte": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7", "prettier-plugin-tailwindcss": "^0.5.7",
"semantic-release": "^20.1.3", "semantic-release": "^20.1.3",
@ -53,5 +53,13 @@
"@semantic-release/npm", "@semantic-release/npm",
"@semantic-release/github" "@semantic-release/github"
] ]
},
"pnpm": {
"overrides": {
"@adobe/css-tools@<4.3.1": ">=4.3.1",
"semver@>=7.0.0 <7.5.2": ">=7.5.2",
"postcss@<8.4.31": ">=8.4.31",
"undici@<5.26.2": ">=5.26.2"
}
} }
} }

View file

@ -1,25 +1,29 @@
<div align="right"> <div align="right">
<a href="https://www.npmjs.com/package/carta-md"> <a href="https://www.npmjs.com/package/carta-md">
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&labelColor=171d27&logo=npm&logoColor=white" alt="npm"> <img src="https://img.shields.io/npm/v/carta-md?color=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
</a> </a>
<a href="https://bundlephobia.com/package/carta-md"> <a href="https://bundlephobia.com/package/carta-md">
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle"> <img src="https://img.shields.io/bundlephobia/min/carta-md?color=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
</a> </a>
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE"> <a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&labelColor=171d27&logo=git&logoColor=white" alt="license"> <img src="https://img.shields.io/npm/l/carta-md?color=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
</a> </a>
<a href="http://beartocode.github.io/carta/"> <a href="http://beartocode.github.io/carta/">
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs"> <img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
</a> </a>
</div> </div>
[![Carta.png](https://i.postimg.cc/nV6DMXKM/Carta.png)](https://beartocode.github.io/carta/) <div align="center">
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
</div>
<h1 align="center"><strong>Carta</strong></h1> <br>
<div align="center">Modern, lightweight, powerful Markdown Editor.</div>
<div align="center"><strong>Carta</strong></div>
<div align="center">Swiftly edit and render Markdown, with no overhead.</div>
<br /> <br />
<div align="center"> <div align="center">
<a href="https://beartocode.github.io/carta/">📚 Documentation</a> <a href="https://beartocode.github.io/carta/">Documentation</a>
<span> · </span> <span> · </span>
<a href="https://github.com/BearToCode/carta">GitHub</a> <a href="https://github.com/BearToCode/carta">GitHub</a>
</div> </div>
@ -28,81 +32,38 @@
# Introduction # Introduction
> **NOTE**: Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer, based on [Marked](https://github.com/markedjs/marked). Check out the [demo](http://beartocode.github.io/carta/) to see it in action.
> Carta has recently been updated to `v4`, which features numerous major changes. Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
>
> Follow the [Migration Guide](http://beartocode.github.io/carta/migration) to update your project.
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer. It is powered by [unified](https://github.com/unifiedjs/unified), [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
Differently from most editors, Carta does not include a code editor, but it is _just_ a textarea with syntax highlighting, shortcuts and more.
## Features ## Features
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/)); - Keyboard **shortcuts** (extensible);
- 🛠️ Toolbar (extensible); - Toolbar (extensible);
- ⌨️ Keyboard **shortcuts** (extensible); - Markdown syntax highlighting;
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark; - Scroll sync;
- 🔀 Scroll sync; - **SSR** compatible;
- ✅ Accessibility friendly; - **Katex** support (plugin);
- 🖥️ **SSR** compatible; - **Slash** commands (plugin);
- ⚗️ **KaTeX** support (plugin); - **Emojis**, with included search (plugin);
- 🔨 **Slash** commands (plugin); - **Tikz** support(plugin);
- 😄 **Emojis**, with included search (plugin); - **Attachment** support(plugin);
- ✏️ **TikZ** support (plugin); - Code blocks **syntax highlighting** (plugin).
- 📂 **Attachment** support (plugin);
- ⚓ **Anchor** links in headings (plugin);
- 🌈 Code blocks **syntax highlighting** (plugin).
## Packages
| Package | Status | Docs |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [carta-md](https://www.npmjs.com/package/carta-md) | ![carta-md](https://img.shields.io/npm/v/carta-md) | [/](https://beartocode.github.io/carta/introduction) |
| [plugin-math](https://www.npmjs.com/package/@cartamd/plugin-math) | ![plugin-math](https://img.shields.io/npm/v/@cartamd/plugin-math) | [/plugins/math](https://beartocode.github.io/carta/plugins/math) |
| [plugin-code](https://www.npmjs.com/package/@cartamd/plugin-code) | ![plugin-code](https://img.shields.io/npm/v/@cartamd/plugin-code) | [/plugins/code](https://beartocode.github.io/carta/plugins/code) |
| [plugin-emoji](https://www.npmjs.com/package/@cartamd/plugin-emoji) | ![plugin-emoji](https://img.shields.io/npm/v/@cartamd/plugin-emoji) | [/plugins/emoji](https://beartocode.github.io/carta/plugins/emoji) |
| [plugin-slash](https://www.npmjs.com/package/@cartamd/plugin-slash) | ![plugin-slash](https://img.shields.io/npm/v/@cartamd/plugin-slash) | [/plugins/slash](https://beartocode.github.io/carta/plugins/slash) |
| [plugin-tikz](https://www.npmjs.com/package/@cartamd/plugin-tikz) | ![plugin-tikz](https://img.shields.io/npm/v/@cartamd/plugin-tikz) | [/plugins/tikz](https://beartocode.github.io/carta/plugins/tikz) |
| [plugin-attachment](https://www.npmjs.com/package/@cartamd/plugin-attachment) | ![plugin-attachment](https://img.shields.io/npm/v/@cartamd/plugin-attachment) | [/plugins/attachment](https://beartocode.github.io/carta/plugins/attachment) |
| [plugin-anchor](https://www.npmjs.com/package/@cartamd/plugin-anchor) | ![plugin-anchor](https://img.shields.io/npm/v/@cartamd/plugin-anchor) | [/plugins/anchor](https://beartocode.github.io/carta/plugins/anchor) |
## Community plugins
| Plugin | Description |
| ----------------------------------------------------------------------------- | ---------------------------------- |
| [carta-plugin-video](https://github.com/maisonsmd/carta-plugin-video) | Render online videos |
| [carta-plugin-imsize](https://github.com/maisonsmd/carta-plugin-imsize) | Render images in specific sizes |
| [carta-plugin-subscript](https://github.com/maisonsmd/carta-plugin-subscript) | Render subscripts and superscripts |
| [carta-plugin-ins-del](https://github.com/maisonsmd/carta-plugin-ins-del) | `<ins>` and `<del>` tags support |
# Getting started # Getting started
> **WARNING** > **Warning**
> Sanitization is not dealt with by Carta. You need to provide a `sanitizer` in the options. > Sanitization is not dealt with by Carta. You need to provide a `sanitizer` in the options.
> Common sanitizers are [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) (suggested) and [sanitize-html](https://www.npmjs.com/package/sanitize-html). > Common sanitizers are [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) (suggested) and [sanitize-html](https://www.npmjs.com/package/sanitize-html).
> Checkout the documentation for an example.
## Installation
Core package:
```
npm i carta-md
```
Plugins:
```
npm i @cartamd/plugin-name
```
## Basic configuration ## Basic configuration
```svelte ```svelte
<script lang="ts"> <script lang="ts">
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
// Component default theme // Component default theme
import 'carta-md/default.css'; import 'carta-md/default.css';
// Markdown input theme (Speed Highlight)
import 'carta-md/light.css';
const carta = new Carta({ const carta = new Carta({
// Remember to use a sanitizer to prevent XSS attacks // Remember to use a sanitizer to prevent XSS attacks
@ -110,14 +71,13 @@ npm i @cartamd/plugin-name
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
<style> <style>
/* Or in global stylesheet */ /* Or in global stylesheet */
/* Set your custom monospace font */ /* Set your custom monospace font */
:global(.carta-font-code) { :global(.carta-font-code) {
font-family: '...', monospace; font-family: '...', monospace;
font-size: 1.1rem;
} }
</style> </style>
``` ```
@ -137,33 +97,7 @@ For the full documentation, examples, guides and more checkout the [website](htt
- [Slash](https://beartocode.github.io/carta/plugins/slash) - [Slash](https://beartocode.github.io/carta/plugins/slash)
- [TikZ](https://beartocode.github.io/carta/plugins/tikz) - [TikZ](https://beartocode.github.io/carta/plugins/tikz)
- [Attachment](https://beartocode.github.io/carta/plugins/attachment) - [Attachment](https://beartocode.github.io/carta/plugins/attachment)
- [Anchor](https://beartocode.github.io/carta/plugins/anchor)
- API: - API:
- [Utilities](https://beartocode.github.io/carta/api/utilities) - [Utilities](https://beartocode.github.io/carta/api/utilities)
- [Core](https://beartocode.github.io/carta/api/core) - [Core](https://beartocode.github.io/carta/api/core)
- [Extension](https://beartocode.github.io/carta/api/extension) - [Extension](https://beartocode.github.io/carta/api/extension)
# Contributing & Development
Every contribution is well accepted. If you have a feature request you can open a new issue.
This package uses a [pnpm workspace](https://pnpm.io/workspaces), so pnpm is required to download and put everything together properly.
### Committing
This repository is [commitizen](https://github.com/commitizen/cz-cli) friendly. To commit use:
```
npm run commit
# or, if you have commitizen installed globally
git cz
```
### Running docs
If you want to preview the docs:
```
cd docs
npm run dev
```

View file

@ -14,7 +14,10 @@
"svelte": "./dist/index.js", "svelte": "./dist/index.js",
"import": "./dist/index.js" "import": "./dist/index.js"
}, },
"./default.css": "./dist/default.css" "./default.css": "./dist/default.css",
"./default-theme.css": "./dist/default.css",
"./light.css": "./dist/light.css",
"./dark.css": "./dist/dark.css"
}, },
"version": "3.0.0", "version": "3.0.0",
"scripts": { "scripts": {
@ -24,23 +27,18 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1", "@sveltejs/adapter-auto": "^1.0.0-next.90",
"@sveltejs/kit": "^2.5.4", "@sveltejs/kit": "^1.0.0-next.587",
"@sveltejs/package": "^2.3.0", "@sveltejs/package": "^2.0.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "svelte-check": "^3.2.0",
"svelte-check": "^3.6.7",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^5.1.6" "typescript-plugin-css-modules": "^5.0.1"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"rehype-stringify": "^10.0.0", "@speed-highlight/core": "1.2.2",
"remark-gfm": "^4.0.0", "marked": "^9.1.5"
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"shiki": "^1.4.0",
"unified": "^11.0.4"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^3.54.0 || ^4.0.0" "svelte": "^3.54.0 || ^4.0.0"

View file

@ -1,12 +1,9 @@
<script lang="ts"> <script lang="ts">
import type { Carta } from './internal/carta'; import type { Carta } from './internal/carta';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Renderer from './internal/components/Renderer.svelte'; import CartaRenderer from './internal/components/CartaRenderer.svelte';
import Input from './internal/components/Input.svelte'; import MarkdownInput from './internal/components/MarkdownInput.svelte';
import { debounce } from './internal/utils'; import { debounce } from './internal/utils';
import type { TextAreaProps } from './internal/textarea-props';
import { defaultLabels, type Labels } from './internal/labels';
import Toolbar from './internal/components/Toolbar.svelte';
export let carta: Carta; export let carta: Carta;
export let theme = 'default'; export let theme = 'default';
@ -15,24 +12,18 @@
export let scroll: 'sync' | 'async' = 'sync'; export let scroll: 'sync' | 'async' = 'sync';
export let disableToolbar = false; export let disableToolbar = false;
export let placeholder = ''; export let placeholder = '';
export let textarea: TextAreaProps = {};
let userLabels: Partial<Labels> = {};
export { userLabels as labels };
const labels: Labels = {
...defaultLabels,
...userLabels
};
let width: number; let width: number;
let selectedTab: 'write' | 'preview' = 'write'; let selectedTab: 'write' | 'preview' = 'write';
let windowMode: 'tabs' | 'split'; let windowMode: 'tabs' | 'split';
let mounted = false; let mounted = false;
let hideIcons = false;
let resizeInput: () => void; let resizeInput: () => void;
onMount(() => (mounted = true)); onMount(() => (mounted = true));
$: { $: {
windowMode = mode === 'auto' ? (width > 768 ? 'split' : 'tabs') : mode; windowMode = mode === 'auto' ? (width > 768 ? 'split' : 'tabs') : mode;
hideIcons = width < 576;
} }
$: { $: {
@ -98,17 +89,49 @@
<div bind:this={editorElem} bind:clientWidth={width} class="carta-editor carta-theme__{theme}"> <div bind:this={editorElem} bind:clientWidth={width} class="carta-editor carta-theme__{theme}">
{#if !disableToolbar} {#if !disableToolbar}
<Toolbar {carta} {labels} mode={windowMode} bind:tab={selectedTab} /> <div class="carta-toolbar">
<div class="carta-toolbar-left">
{#if windowMode == 'tabs'}
<button
on:click={() => (selectedTab = 'write')}
class={selectedTab === 'write' ? 'carta-active' : ''}
>
Write
</button>
<button
on:click={() => (selectedTab = 'preview')}
class={selectedTab === 'preview' ? 'carta-active' : ''}
>
Preview
</button>
{/if}
</div>
<div class="carta-toolbar-right">
{#if !hideIcons}
{#each carta.icons as icon}
<button
on:click|preventDefault|stopPropagation={() => {
carta.input && icon.action(carta.input);
carta.input?.update();
carta.input?.textarea.focus();
}}
class="carta-icon"
>
<svelte:component this={icon.component} />
</button>
{/each}
{/if}
</div>
</div>
{/if} {/if}
<div class="carta-wrapper"> <div class="carta-wrapper">
<div class="carta-container mode-{windowMode}"> <div class="carta-container mode-{windowMode}">
{#if windowMode == 'split' || selectedTab == 'write'} {#if windowMode == 'split' || selectedTab == 'write'}
<Input <MarkdownInput
{carta} {carta}
{placeholder} {placeholder}
{handleScroll} {handleScroll}
props={textarea}
bind:value bind:value
bind:resize={resizeInput} bind:resize={resizeInput}
bind:elem={inputElem} bind:elem={inputElem}
@ -121,10 +144,10 @@
<svelte:component this={component} {carta} {...props} /> <svelte:component this={component} {carta} {...props} />
{/each} {/each}
{/if} {/if}
</Input> </MarkdownInput>
{/if} {/if}
{#if windowMode == 'split' || selectedTab == 'preview'} {#if windowMode == 'split' || selectedTab == 'preview'}
<Renderer {carta} {handleScroll} bind:value bind:elem={rendererElem}> <CartaRenderer {carta} {handleScroll} bind:value bind:elem={rendererElem}>
<!-- Renderer extensions components --> <!-- Renderer extensions components -->
{#if mounted} {#if mounted}
{#each carta.components.filter(({ parent }) => [parent] {#each carta.components.filter(({ parent }) => [parent]
@ -133,7 +156,7 @@
<svelte:component this={component} {carta} {...props} /> <svelte:component this={component} {carta} {...props} />
{/each} {/each}
{/if} {/if}
</Renderer> </CartaRenderer>
{/if} {/if}
</div> </div>
</div> </div>
@ -156,11 +179,17 @@
flex-direction: column; flex-direction: column;
} }
:global(.carta-container.mode-split > *) { .carta-toolbar {
height: 2rem;
display: flex;
flex-shrink: 0;
}
:global(.mode-split > *) {
width: 50%; width: 50%;
} }
:global(.carta-container.mode-tabs > *) { :global(.mode-tabs > *) {
width: 100%; width: 100%;
} }
@ -168,4 +197,27 @@
display: flex; display: flex;
position: relative; position: relative;
} }
.carta-toolbar-left {
height: 100%;
}
.carta-toolbar-right {
height: 100%;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.carta-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 3px;
cursor: pointer;
margin-left: 4px;
}
</style> </style>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { loadNestedLanguages, type Carta } from '.'; import type { Carta } from './';
export let carta: Carta; export let carta: Carta;
export let value: string; export let value: string;
@ -12,12 +12,7 @@
let rendered = carta.renderSSR(value); let rendered = carta.renderSSR(value);
onMount(async () => { onMount(async () => {
carta.$setRenderer(elem); carta.$setRenderer(elem);
// Add code syntax highlighting (if plugin is present) once loaded on the client.
// Load highlighting languages
const highlighter = await carta.highlighter();
await loadNestedLanguages(highlighter, value);
// Render using asynchronous renderer
rendered = await carta.render(value); rendered = await carta.render(value);
mounted = true; mounted = true;

View file

@ -0,0 +1,41 @@
@import 'light.css';
[class*='shj-lang-'] {
color: #f8f8f2;
background: #1a1a1c;
}
[class*='shj-lang-']:before {
color: #6f9aff;
}
.shj-syn-deleted,
.shj-syn-err,
.shj-syn-var {
color: #ff5261;
}
.shj-syn-section,
.shj-syn-kwd {
color: #ff7cc6;
}
.shj-syn-class {
color: #eab07c;
}
.shj-numbers,
.shj-syn-cmnt {
color: #7d828b;
}
.shj-syn-insert,
.shj-syn-type,
.shj-syn-func,
.shj-syn-bool {
color: #71d58a;
}
.shj-syn-num {
color: #b581fd;
}
.shj-syn-oper {
color: #80c6ff;
}
.shj-syn-str {
color: #4dacfa;
}

View file

@ -3,15 +3,6 @@
--selection-color: #b5f0ff3d; --selection-color: #b5f0ff3d;
--focus-outline: #76bbf3; --focus-outline: #76bbf3;
--hover-color: #e9e9e9; --hover-color: #e9e9e9;
--caret-color: #161616;
--text-color: #1a1a1a;
--border-color-dark: #4d4d4c;
--selection-color-dark: #b5f0ff3d;
--focus-outline-dark: #76bbf3;
--hover-color-dark: #4d4d4c;
--caret-color-dark: #ffffff;
--text-color-dark: #f1f1f1;
} }
.carta-theme__default.carta-editor { .carta-theme__default.carta-editor {
@ -38,14 +29,10 @@
/* Text settings */ /* Text settings */
.carta-theme__default .carta-input { .carta-theme__default .carta-input {
caret-color: var(--caret-color); caret-color: #4d4d4c;
font-size: 0.95rem; font-size: 0.95rem;
} }
.carta-theme__default .carta-input ::placeholder {
color: var(--text-color);
}
/* Splitter */ /* Splitter */
.carta-theme__default .mode-split.carta-container::after { .carta-theme__default .mode-split.carta-container::after {
content: ''; content: '';
@ -75,10 +62,6 @@
align-items: flex-end; align-items: flex-end;
} }
.carta-theme__default button {
color: var(--text-color);
}
/* Markdown input and renderer */ /* Markdown input and renderer */
.carta-theme__default .carta-input, .carta-theme__default .carta-input,
.carta-theme__default .carta-renderer { .carta-theme__default .carta-renderer {
@ -87,22 +70,12 @@
} }
/* Icons */ /* Icons */
.carta-theme__default .carta-icon, .carta-theme__default .carta-icon {
.carta-theme__default .carta-icon-full {
border: 0; border: 0;
background: transparent; background: transparent;
} }
.carta-theme__default .carta-icon-full { .carta-theme__default .carta-icon:hover {
padding: 6px 4px;
}
.carta-theme__default .carta-icon-full span {
margin-left: 6px;
}
.carta-theme__default .carta-icon:hover,
.carta-theme__default .carta-icon-full:hover {
background: var(--hover-color); background: var(--hover-color);
} }
@ -110,21 +83,6 @@
background: inherit; background: inherit;
} }
.carta-theme__default .carta-icons-menu {
padding: 6px;
border: 1px solid var(--border-color);
border-radius: 6px;
min-width: 180px;
}
.carta-theme__default .carta-icons-menu .carta-icon-full {
margin-top: 2px;
}
.carta-theme__default .carta-icons-menu .carta-icon-full:first-child {
margin-top: 0;
}
/* Buttons */ /* Buttons */
.carta-theme__default .carta-toolbar-left button { .carta-theme__default .carta-toolbar-left button {
background: none; background: none;

View file

@ -1,15 +1,10 @@
export { default as MarkdownEditor } from '$lib/MarkdownEditor.svelte'; export { default as CartaEditor } from '$lib/CartaEditor.svelte';
export { default as Markdown } from '$lib/Markdown.svelte'; export { default as CartaViewer } from '$lib/CartaViewer.svelte';
export type { InputEnhancer, TextSelection } from '$lib/internal/input'; export type { CartaInput, TextSelection } from '$lib/internal/input';
export type { Icon } from '$lib/internal/icons'; export type { CartaIcon } from '$lib/internal/icons';
export type { KeyboardShortcut } from '$lib/internal/shortcuts'; export type { KeyboardShortcut } from '$lib/internal/shortcuts';
export type { Prefix } from '$lib/internal/prefixes'; export type { Prefix } from '$lib/internal/prefixes';
export * from '$lib/internal/carta'; export * from '$lib/internal/carta';
export * from '$lib/internal/highlight'; export * from '$lib/internal/highlight';
export * from '$lib/internal/textarea-props';
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';

View file

@ -1,21 +0,0 @@
/**
* Handles arrow key navigation for a list of elements.
* @param e The event to handle.
*/
export function handleArrowKeysNavigation(e: KeyboardEvent & { currentTarget: HTMLElement }) {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
e.preventDefault();
const siblings = e.currentTarget.parentElement?.children;
if (!siblings) return;
const next = (e.currentTarget.nextElementSibling ?? siblings[0]) as HTMLElement;
const prev = (e.currentTarget.previousElementSibling ??
siblings[siblings.length - 1]) as HTMLElement;
if (e.key === 'ArrowRight') {
next.focus();
} else {
prev.focus();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,330 +0,0 @@
import { type ThemeInput } from 'shiki';
const theme = {
displayName: 'Carta Dark' as const,
name: 'carta-dark' as const,
semanticHighlighting: true,
fg: '#f8f8f2',
bg: 'transparent',
tokenColors: [
{
scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
settings: {
foreground: '#6a737d'
}
},
{
scope: ['variable.other.constant', 'variable.other.enummember', 'variable.language'],
settings: {
foreground: '#fff'
}
},
{
scope: ['constant', 'entity.name.constant'],
settings: {
foreground: '#71d58a'
}
},
{
scope: ['entity', 'entity.name'],
settings: {
foreground: '#b392f0'
}
},
{
scope: 'variable.parameter.function',
settings: {
foreground: '#e1e4e8'
}
},
{
scope: 'entity.name.tag',
settings: {
foreground: '#85e89d'
}
},
{
scope: ['keyword', 'punctuation.definition.template-expression'],
settings: {
foreground: '#ff7cc6'
}
},
{
scope: ['storage', 'storage.type'],
settings: {
foreground: '#ff7cc6'
}
},
{
scope: ['storage.modifier.package', 'storage.modifier.import', 'storage.type.java'],
settings: {
foreground: '#e1e4e8'
}
},
{
scope: [
'string',
'punctuation.definition.string',
'string punctuation.section.embedded source'
],
settings: {
foreground: '#4dacfa'
}
},
{
scope: 'support',
settings: {
foreground: '#71d58a'
}
},
{
scope: 'meta.property-name',
settings: {
foreground: '#71d58a'
}
},
{
scope: 'variable',
settings: {
foreground: '#b581fd'
}
},
{
scope: 'variable.other',
settings: {
foreground: '#e1e4e8'
}
},
{
scope: 'invalid.broken',
settings: {
fontStyle: 'italic',
foreground: '#fdaeb7'
}
},
{
scope: 'invalid.deprecated',
settings: {
fontStyle: 'italic',
foreground: '#fdaeb7'
}
},
{
scope: 'invalid.illegal',
settings: {
fontStyle: 'italic',
foreground: '#fdaeb7'
}
},
{
scope: 'invalid.unimplemented',
settings: {
fontStyle: 'italic',
foreground: '#fdaeb7'
}
},
{
scope: 'carriage-return',
settings: {
background: '#ff7cc6',
fontStyle: 'italic underline',
foreground: '#24292e'
}
},
{
scope: 'message.error',
settings: {
foreground: '#fdaeb7'
}
},
{
scope: 'string variable',
settings: {
foreground: '#71d58a'
}
},
{
scope: ['source.regexp', 'string.regexp'],
settings: {
foreground: '#4dacfa'
}
},
{
scope: [
'string.regexp.character-class',
'string.regexp constant.character.escape',
'string.regexp source.ruby.embedded',
'string.regexp string.regexp.arbitrary-repitition'
],
settings: {
foreground: '#4dacfa'
}
},
{
scope: 'string.regexp constant.character.escape',
settings: {
fontStyle: 'bold',
foreground: '#85e89d'
}
},
{
scope: 'support.constant',
settings: {
foreground: '#71d58a'
}
},
{
scope: 'support.variable',
settings: {
foreground: '#71d58a'
}
},
{
scope: 'meta.module-reference',
settings: {
foreground: '#71d58a'
}
},
{
scope: 'punctuation.definition.list.begin.markdown',
settings: {
foreground: '#ff7cc6'
}
},
{
scope: ['markup.heading', 'markup.heading entity.name'],
settings: {
fontStyle: 'bold',
foreground: '#e8e8e8'
}
},
{
scope: 'markup.quote',
settings: {
foreground: '#7d828b'
}
},
{
scope: 'markup.italic',
settings: {
fontStyle: 'italic',
foreground: '#ff7cc6'
}
},
{
scope: 'markup.bold',
settings: {
foreground: '#b581fd'
}
},
{
scope: ['markup.underline'],
settings: {
foreground: '#71d58a',
fontStyle: 'underline'
}
},
{
scope: ['markup.strikethrough'],
settings: {
foreground: '#ff5261',
fontStyle: 'strikethrough'
}
},
{
scope: 'markup.inline.raw',
settings: {
foreground: '#4dacfa'
}
},
{
scope: ['markup.deleted', 'meta.diff.header.from-file', 'punctuation.definition.deleted'],
settings: {
background: '#86181d',
foreground: '#fdaeb7'
}
},
{
scope: ['markup.inserted', 'meta.diff.header.to-file', 'punctuation.definition.inserted'],
settings: {
background: '#144620',
foreground: '#85e89d'
}
},
{
scope: ['markup.changed', 'punctuation.definition.changed'],
settings: {
background: '#c24e00',
foreground: '#b581fd'
}
},
{
scope: ['markup.ignored', 'markup.untracked'],
settings: {
background: '#71d58a',
foreground: '#2f363d'
}
},
{
scope: 'meta.diff.range',
settings: {
fontStyle: 'bold',
foreground: '#b392f0'
}
},
{
scope: 'meta.diff.header',
settings: {
foreground: '#71d58a'
}
},
{
scope: 'meta.separator',
settings: {
fontStyle: 'bold',
foreground: '#71d58a'
}
},
{
scope: 'meta.output',
settings: {
foreground: '#71d58a'
}
},
{
scope: [
'brackethighlighter.tag',
'brackethighlighter.curly',
'brackethighlighter.round',
'brackethighlighter.square',
'brackethighlighter.angle',
'brackethighlighter.quote'
],
settings: {
foreground: '#d1d5da'
}
},
{
scope: 'brackethighlighter.unmatched',
settings: {
foreground: '#fdaeb7'
}
},
{
scope: ['constant.other.reference.link', 'string.other.link'],
settings: {
fontStyle: 'underline',
foreground: '#4dacfa'
}
},
{
scope: ['punctuation.definition.markdown', 'fenced_code.block.language'],
settings: {
foreground: '#ff7cc6'
}
}
],
type: 'light'
} satisfies ThemeInput;
export default theme;

View file

@ -1,329 +0,0 @@
import { type ThemeInput } from 'shiki';
const theme = {
displayName: 'Carta Light' as const,
name: 'carta-light' as const,
semanticHighlighting: true,
fg: '#333333',
bg: 'transparent',
tokenColors: [
{
scope: ['comment', 'punctuation.definition.comment', 'string.comment'],
settings: {
foreground: '#6a737d'
}
},
{
scope: ['variable.other.constant', 'variable.other.enummember', 'variable.language'],
settings: {
foreground: '#000'
}
},
{
scope: ['constant', 'entity.name.constant'],
settings: {
foreground: '#3bf'
}
},
{
scope: ['entity', 'entity.name'],
settings: {
foreground: '#6f42c1'
}
},
{
scope: 'variable.parameter.function',
settings: {
foreground: '#24292e'
}
},
{
scope: 'entity.name.tag',
settings: {
foreground: '#22863a'
}
},
{
scope: ['keyword', 'punctuation.definition.template-expression'],
settings: {
foreground: '#e16'
}
},
{
scope: ['storage', 'storage.type'],
settings: {
foreground: '#e16'
}
},
{
scope: ['storage.modifier.package', 'storage.modifier.import', 'storage.type.java'],
settings: {
foreground: '#24292e'
}
},
{
scope: [
'string',
'punctuation.definition.string',
'string punctuation.section.embedded source'
],
settings: {
foreground: '#7d8'
}
},
{
scope: 'support',
settings: {
foreground: '#3bf'
}
},
{
scope: 'meta.property-name',
settings: {
foreground: '#3bf'
}
},
{
scope: 'variable',
settings: {
foreground: '#f60'
}
},
{
scope: 'variable.other',
settings: {
foreground: '#24292e'
}
},
{
scope: 'invalid.broken',
settings: {
fontStyle: 'italic',
foreground: '#b31d28'
}
},
{
scope: 'invalid.deprecated',
settings: {
fontStyle: 'italic',
foreground: '#b31d28'
}
},
{
scope: 'invalid.illegal',
settings: {
fontStyle: 'italic',
foreground: '#b31d28'
}
},
{
scope: 'invalid.unimplemented',
settings: {
fontStyle: 'italic',
foreground: '#b31d28'
}
},
{
scope: 'carriage-return',
settings: {
background: '#e16',
fontStyle: 'italic underline',
foreground: '#fafbfc'
}
},
{
scope: 'message.error',
settings: {
foreground: '#b31d28'
}
},
{
scope: 'string variable',
settings: {
foreground: '#3bf'
}
},
{
scope: ['source.regexp', 'string.regexp'],
settings: {
foreground: '#7d8'
}
},
{
scope: [
'string.regexp.character-class',
'string.regexp constant.character.escape',
'string.regexp source.ruby.embedded',
'string.regexp string.regexp.arbitrary-repitition'
],
settings: {
foreground: '#7d8'
}
},
{
scope: 'string.regexp constant.character.escape',
settings: {
fontStyle: 'bold',
foreground: '#22863a'
}
},
{
scope: 'support.constant',
settings: {
foreground: '#3bf'
}
},
{
scope: 'support.variable',
settings: {
foreground: '#3bf'
}
},
{
scope: 'meta.module-reference',
settings: {
foreground: '#3bf'
}
},
{
scope: 'punctuation.definition.list.begin.markdown',
settings: {
foreground: '#e16'
}
},
{
scope: ['markup.heading', 'markup.heading entity.name'],
settings: {
fontStyle: 'bold',
foreground: '#212121'
}
},
{
scope: 'markup.quote',
settings: {
foreground: '#999'
}
},
{
scope: 'markup.italic',
settings: {
foreground: '#e16'
}
},
{
scope: 'markup.bold',
settings: {
foreground: '#f60'
}
},
{
scope: ['markup.underline'],
settings: {
foreground: '#84f',
fontStyle: 'underline'
}
},
{
scope: ['markup.strikethrough'],
settings: {
foreground: '#f44',
fontStyle: 'strikethrough'
}
},
{
scope: 'markup.inline.raw',
settings: {
foreground: '#5af'
}
},
{
scope: ['markup.deleted', 'meta.diff.header.from-file', 'punctuation.definition.deleted'],
settings: {
background: '#ffeef0',
foreground: '#b31d28'
}
},
{
scope: ['markup.inserted', 'meta.diff.header.to-file', 'punctuation.definition.inserted'],
settings: {
background: '#f0fff4',
foreground: '#22863a'
}
},
{
scope: ['markup.changed', 'punctuation.definition.changed'],
settings: {
background: '#ffebda',
foreground: '#f60'
}
},
{
scope: ['markup.ignored', 'markup.untracked'],
settings: {
background: '#3bf',
foreground: '#f6f8fa'
}
},
{
scope: 'meta.diff.range',
settings: {
fontStyle: 'bold',
foreground: '#6f42c1'
}
},
{
scope: 'meta.diff.header',
settings: {
foreground: '#3bf'
}
},
{
scope: 'meta.separator',
settings: {
fontStyle: 'bold',
foreground: '#3bf'
}
},
{
scope: 'meta.output',
settings: {
foreground: '#3bf'
}
},
{
scope: [
'brackethighlighter.tag',
'brackethighlighter.curly',
'brackethighlighter.round',
'brackethighlighter.square',
'brackethighlighter.angle',
'brackethighlighter.quote'
],
settings: {
foreground: '#586069'
}
},
{
scope: 'brackethighlighter.unmatched',
settings: {
foreground: '#b31d28'
}
},
{
scope: ['constant.other.reference.link', 'string.other.link'],
settings: {
fontStyle: 'underline',
foreground: '#5af'
}
},
{
scope: ['punctuation.definition.markdown', 'fenced_code.block.language'],
settings: {
foreground: '#e16'
}
}
],
type: 'light'
} satisfies ThemeInput;
export default theme;

View file

@ -1,61 +1,53 @@
import type { SvelteComponent } from 'svelte'; import type { CartaHistoryOptions } from './history';
import { unified, type Processor } from 'unified'; import type { SvelteComponentTyped } from 'svelte';
import remarkParse from 'remark-parse'; import type { ShjLanguageDefinition } from '@speed-highlight/core/index';
import remarkGfm, { type Options as GfmOptions } from 'remark-gfm'; import { Marked, type MarkedExtension } from 'marked';
import remarkRehype from 'remark-rehype'; import { CartaInput } from './input';
import rehypeStringify from 'rehype-stringify';
import type { TextAreaHistoryOptions } from './history';
import { InputEnhancer } from './input';
import { import {
type DefaultShortcutId, type DefaultShortcutId,
type KeyboardShortcut, type KeyboardShortcut,
defaultKeyboardShortcuts defaultKeyboardShortcuts
} from './shortcuts'; } from './shortcuts';
import { defaultIcons, type Icon, type DefaultIconId } from './icons'; import { defaultIcons, type CartaIcon, type DefaultIconId } from './icons';
import { defaultPrefixes, type DefaultPrefixId, type Prefix } from './prefixes'; import { defaultPrefixes, type DefaultPrefixId, type Prefix } from './prefixes';
import { Renderer } from './renderer'; import { CartaRenderer } from './renderer';
import { CustomEvent, type MaybeArray } from './utils';
import { import {
loadHighlighter, type HighlightFunctions,
loadDefaultTheme, loadCustomLanguage,
type Highlighter, highlight,
type GrammarRule, highlightAutodetect,
type ShikiOptions, loadCustomMarkdown
type DualTheme, } from './highlight.js';
type Theme, import { CustomEvent } from './utils';
type HighlightingRule
} from './highlight';
/** /**
* Carta-specific event with extra payload. * Carta-specific event with extra payload.
*/ */
export type Event = CustomEvent<{ carta: Carta }>; export type CartaEvent = CustomEvent<{ carta: Carta }>;
const cartaEvents = ['carta-render', 'carta-render-ssr'] as const; const cartaEvents = ['carta-render', 'carta-render-ssr'] as const;
type CartaEventType = (typeof cartaEvents)[number]; type CartaEventType = (typeof cartaEvents)[number];
/** export type CartaListener<K extends CartaEventType | keyof HTMLElementEventMap> = [
* Custom listeners for the textarea element.
*/
export type Listener<K extends CartaEventType | keyof HTMLElementEventMap> = [
type: K, type: K,
listener: ( listener: (
this: HTMLTextAreaElement, this: HTMLTextAreaElement,
ev: K extends CartaEventType ev: K extends CartaEventType
? Event ? CartaEvent
: K extends keyof HTMLElementEventMap : K extends keyof HTMLElementEventMap
? HTMLElementEventMap[K] ? HTMLElementEventMap[K]
: Event : Event
) => unknown, ) => unknown,
options?: boolean | AddEventListenerOptions options?: boolean | AddEventListenerOptions
]; ];
/** // eslint-disable-next-line @typescript-eslint/no-explicit-any
* Custom Svelte component for extensions. type CartaListeners = CartaListener<any>[];
*/
export interface ExtensionComponent<T extends object | undefined> { type MaybeArray<T> = T | Array<T>;
export interface CartaExtensionComponent<T extends object> {
/** /**
* Svelte components that exports `carta: Carta` and all the other properties specified in `props`. * Svelte components that exports `carta: Carta` and all the other properties specified in `props`.
*/ */
component: typeof SvelteComponent<T & { carta: Carta }>; component: typeof SvelteComponentTyped<T & { carta: Carta }>;
/** /**
* Properties that will be handed to the component. * Properties that will be handed to the component.
*/ */
@ -67,22 +59,16 @@ export interface ExtensionComponent<T extends object | undefined> {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type Listeners = Listener<any>[]; type CartaExtensionComponents = Array<CartaExtensionComponent<any>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExtensionComponents = Array<ExtensionComponent<any>>;
/** /**
* Carta editor options. * Carta editor options.
*/ */
export interface Options { export interface CartaOptions {
/**
* GitHub Flavored Markdown options.
*/
gfmOptions?: GfmOptions;
/** /**
* Editor/viewer extensions. * Editor/viewer extensions.
*/ */
extensions?: Plugin[]; extensions?: CartaExtension[];
/** /**
* Renderer debouncing timeout, in ms. * Renderer debouncing timeout, in ms.
* @defaults 300ms * @defaults 300ms
@ -103,47 +89,21 @@ export interface Options {
/** /**
* History (Undo/Redo) options. * History (Undo/Redo) options.
*/ */
historyOptions?: TextAreaHistoryOptions; historyOptions?: Partial<CartaHistoryOptions>;
/** /**
* HTML sanitizer. * HTML sanitizer.
*/ */
sanitizer: ((html: string) => string) | false; sanitizer?: (html: string) => string;
/**
* Highlighter options.
*/
shikiOptions?: ShikiOptions;
/**
* ShikiJS theme
* @default 'carta-light' for light mode and 'carta-dark' for dark mode.
*/
theme?: Theme | DualTheme;
} }
/**
* Unified transformers plugins.
*/
export type UnifiedTransformer<E extends 'sync' | 'async'> = {
execution: 'sync' | 'async';
type: 'remark' | 'rehype';
transform: ({
processor,
carta
}: {
processor: Processor;
carta: Carta;
}) => E extends 'sync' ? void : Promise<void>;
};
/** /**
* Carta editor extensions. * Carta editor extensions.
*/ */
export interface Plugin { export interface CartaExtension {
/** /**
* Unified transformers plugins. * Marked extensions, more on that [here](https://marked.js.org/using_advanced).
* @important If the plugin is async, it will not run in SSR rendering.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any markedExtensions?: MarkedExtension[];
transformers?: UnifiedTransformer<'sync' | 'async'>[];
/** /**
* Additional keyboard shortcuts. * Additional keyboard shortcuts.
*/ */
@ -151,7 +111,7 @@ export interface Plugin {
/** /**
* Additional icons. * Additional icons.
*/ */
icons?: Icon[]; icons?: CartaIcon[];
/** /**
* Additional prefixes. * Additional prefixes.
*/ */
@ -159,78 +119,58 @@ export interface Plugin {
/** /**
* Textarea event listeners. * Textarea event listeners.
*/ */
listeners?: Listeners; listeners?: CartaListeners;
/** /**
* Additional components, that will be put after the editor. * Additional components, that will be put after the editor.
* All components are given a `carta: Carta` prop. * All components are given a `carta: Carta` prop.
* The editor has a `relative` position, so you can position * The editor has a `relative` position, so you can position
* elements absolutely. * elements absolutely.
*/ */
components?: ExtensionComponents; components?: CartaExtensionComponents;
/** /**
* Custom markdown grammar highlight rules for ShiKi. * Custom markdown highlight rules. See [Speed-Highlight Wiki](https://github.com/speed-highlight/core/wiki/Create-or-suggest-new-languages).
*/ */
grammarRules?: GrammarRule[]; highlightRules?: ShjLanguageDefinition;
/**
* Custom markdown highlighting rules for ShiKi.
*/
highlightingRules?: HighlightingRule[];
/** /**
* Use this callback to execute code when one Carta instance loads the extension. * Use this callback to execute code when one Carta instance loads the extension.
* @param data General Carta related data. * @param data General Carta related data.
*/ */
onLoad?: (data: { carta: Carta }) => void; onLoad?: (data: { carta: Carta; highlight: HighlightFunctions }) => void;
/**
* This function can be used to access a reference to the `Carta` class immediately after initialization.
* @deprecated Use `onLoad` instead.
*/
cartaRef?: (carta: Carta) => void;
/**
* This function can be used to access a reference to all highlight functions immediately after initialization.
* @deprecated Use `onLoad` instead.
*/
shjRef?: (functions: HighlightFunctions) => void;
} }
export class Carta { export class Carta {
public readonly sanitizer?: (html: string) => string;
public readonly historyOptions?: TextAreaHistoryOptions;
public readonly theme?: Theme | DualTheme;
public readonly shikiOptions?: ShikiOptions;
public readonly rendererDebounce: number;
public readonly keyboardShortcuts: KeyboardShortcut[]; public readonly keyboardShortcuts: KeyboardShortcut[];
public readonly icons: Icon[]; public readonly icons: CartaIcon[];
public readonly prefixes: Prefix[]; public readonly prefixes: Prefix[];
public readonly grammarRules: GrammarRule[]; public readonly highlightRules: ShjLanguageDefinition;
public readonly highlightingRules: HighlightingRule[]; public readonly textareaListeners: CartaListeners;
public readonly textareaListeners: Listeners; public readonly cartaListeners: CartaListeners;
public readonly cartaListeners: Listeners; public readonly components: CartaExtensionComponents;
public readonly components: ExtensionComponents;
public readonly dispatcher = new EventTarget(); public readonly dispatcher = new EventTarget();
public readonly syncProcessor: Processor; public readonly markedAsync = new Marked();
public readonly asyncProcessor: Promise<Processor>; public readonly markedSync = new Marked();
private mElement: HTMLDivElement | undefined;
private mInput: InputEnhancer | undefined;
private mRenderer: Renderer | undefined;
private mHighlighter: Highlighter | Promise<Highlighter> | undefined;
private mSyncTransformers: UnifiedTransformer<'sync'>[] = [];
private mAsyncTransformers: UnifiedTransformer<'async'>[] = [];
private _element: HTMLDivElement | undefined;
private _input: CartaInput | undefined;
private _renderer: CartaRenderer | undefined;
public get element() { public get element() {
return this.mElement; return this._element;
} }
public get input() { public get input() {
return this.mInput; return this._input;
} }
public get renderer() { public get renderer() {
return this.mRenderer; return this._renderer;
}
public async highlighter(): Promise<Highlighter> {
if (!this.mHighlighter) {
const promise = async () => {
return loadHighlighter({
theme: this.theme ?? (await loadDefaultTheme()),
grammarRules: this.grammarRules,
highlightingRules: this.highlightingRules,
shiki: this.shikiOptions
});
};
this.mHighlighter = promise();
this.mHighlighter = await this.mHighlighter;
}
return this.mHighlighter;
} }
private elementsToBind: { private elementsToBind: {
@ -239,31 +179,23 @@ export class Carta {
callback: (() => void) | undefined; callback: (() => void) | undefined;
}[] = []; }[] = [];
public constructor(options?: Options) { public constructor(public readonly options?: CartaOptions) {
this.sanitizer = options?.sanitizer || undefined;
this.historyOptions = options?.historyOptions;
this.theme = options?.theme;
this.shikiOptions = options?.shikiOptions;
this.rendererDebounce = options?.rendererDebounce ?? 300;
// Load plugins
this.keyboardShortcuts = []; this.keyboardShortcuts = [];
this.icons = []; this.icons = [];
this.prefixes = []; this.prefixes = [];
this.textareaListeners = []; this.textareaListeners = [];
this.cartaListeners = []; this.cartaListeners = [];
this.components = []; this.components = [];
this.grammarRules = []; this.highlightRules = [];
this.highlightingRules = [];
const listeners = []; const listeners = [];
for (const ext of options?.extensions ?? []) { for (const ext of options?.extensions ?? []) {
this.keyboardShortcuts.push(...(ext.shortcuts ?? [])); this.keyboardShortcuts.push(...(ext.shortcuts ?? []));
this.icons.push(...(ext.icons ?? [])); this.icons.push(...(ext.icons ?? []));
this.prefixes.push(...(ext.prefixes ?? [])); this.prefixes.push(...(ext.prefixes ?? []));
this.components.push(...(ext.components ?? [])); this.components.push(...(ext.components ?? []));
this.grammarRules.push(...(ext.grammarRules ?? [])); this.highlightRules.push(...(ext.highlightRules ?? []));
this.highlightingRules.push(...(ext.highlightingRules ?? []));
listeners.push(...(ext.listeners ?? [])); listeners.push(...(ext.listeners ?? []));
} }
@ -300,82 +232,41 @@ export class Carta {
) )
); );
// Load unified extensions // Load marked extensions
this.mSyncTransformers = []; const markedExtensions = this.options?.extensions
this.mAsyncTransformers = []; ?.flatMap((ext) => ext.markedExtensions)
.filter((ext) => ext != null) as MarkedExtension[] | undefined;
if (markedExtensions)
markedExtensions.forEach((ext) => {
this.useMarkedExtension(ext);
});
for (const ext of options?.extensions ?? []) { // Load highlight custom language
for (const transformer of ext.transformers ?? []) { loadCustomMarkdown(this.options?.extensions);
if (transformer.execution === 'sync') {
this.mSyncTransformers.push(transformer);
} else {
this.mAsyncTransformers.push(transformer);
}
}
}
this.syncProcessor = this.setupSynchronousProcessor({ gfmOptions: options?.gfmOptions }); for (const ext of this.options?.extensions ?? []) {
this.asyncProcessor = this.setupAsynchronousProcessor({ gfmOptions: options?.gfmOptions }); ext.cartaRef && ext.cartaRef(this);
ext.shjRef &&
for (const ext of options?.extensions ?? []) { ext.shjRef({
if (ext.onLoad) { highlight,
ext.onLoad({ highlightAutodetect,
carta: this loadCustomLanguage
});
ext.onLoad &&
ext.onLoad({
carta: this,
highlight: {
highlight,
highlightAutodetect,
loadCustomLanguage
}
}); });
}
} }
} }
private setupSynchronousProcessor({ gfmOptions }: { gfmOptions?: GfmOptions }) { private useMarkedExtension(exts: MarkedExtension) {
const syncProcessor = unified(); this.markedAsync.use(exts);
if (!exts.async) this.markedSync.use(exts);
const remarkPlugins = this.mSyncTransformers.filter((it) => it.type === 'remark');
const rehypePlugins = this.mSyncTransformers.filter((it) => it.type === 'rehype');
syncProcessor.use(remarkParse);
syncProcessor.use(remarkGfm, gfmOptions);
for (const plugin of remarkPlugins) {
plugin.transform({ processor: syncProcessor, carta: this });
}
syncProcessor.use(remarkRehype);
for (const plugin of rehypePlugins) {
plugin.transform({ processor: syncProcessor, carta: this });
}
syncProcessor.use(rehypeStringify);
return syncProcessor;
}
private async setupAsynchronousProcessor({ gfmOptions }: { gfmOptions?: GfmOptions }) {
const asyncProcessor = unified();
const remarkPlugins = [...this.mSyncTransformers, ...this.mAsyncTransformers].filter(
(it) => it.type === 'remark'
);
const rehypePlugins = [...this.mSyncTransformers, ...this.mAsyncTransformers].filter(
(it) => it.type === 'rehype'
);
asyncProcessor.use(remarkParse);
asyncProcessor.use(remarkGfm, gfmOptions);
for (const plugin of remarkPlugins) {
await plugin.transform({ processor: asyncProcessor, carta: this });
}
asyncProcessor.use(remarkRehype);
for (const plugin of rehypePlugins) {
await plugin.transform({ processor: asyncProcessor, carta: this });
}
asyncProcessor.use(rehypeStringify);
return asyncProcessor;
} }
/** /**
@ -384,13 +275,12 @@ export class Carta {
* @returns Rendered html. * @returns Rendered html.
*/ */
public async render(markdown: string): Promise<string> { public async render(markdown: string): Promise<string> {
const processor = await this.asyncProcessor; const dirty = await this.markedAsync.parse(markdown, { async: true });
const dirty = String(await processor.process(markdown));
if (!dirty) return ''; if (!dirty) return '';
this.dispatcher.dispatchEvent( this.dispatcher.dispatchEvent(
new CustomEvent<{ carta: Carta }>('carta-render', { detail: { carta: this } }) new CustomEvent<{ carta: Carta }>('carta-render', { detail: { carta: this } })
); );
return (this.sanitizer && this.sanitizer(dirty)) ?? dirty; return (this.options?.sanitizer && this.options?.sanitizer(dirty)) ?? dirty;
} }
/** /**
@ -399,12 +289,12 @@ export class Carta {
* @returns Rendered html. * @returns Rendered html.
*/ */
public renderSSR(markdown: string): string { public renderSSR(markdown: string): string {
const dirty = String(this.syncProcessor.processSync(markdown)); const dirty = this.markedSync.parse(markdown, { async: false });
if (typeof dirty != 'string') return ''; if (typeof dirty != 'string') return '';
this.dispatcher.dispatchEvent( this.dispatcher.dispatchEvent(
new CustomEvent<{ carta: Carta }>('carta-render-ssr', { detail: { carta: this } }) new CustomEvent<{ carta: Carta }>('carta-render-ssr', { detail: { carta: this } })
); );
if (this.sanitizer) return this.sanitizer(dirty); if (this.options?.sanitizer) return this.options.sanitizer(dirty);
return dirty; return dirty;
} }
@ -413,7 +303,7 @@ export class Carta {
* @param element The editor element. * @param element The editor element.
*/ */
public $setElement(element: HTMLDivElement) { public $setElement(element: HTMLDivElement) {
this.mElement = element; this._element = element;
} }
/** /**
@ -425,19 +315,19 @@ export class Carta {
// Remove old listeners if any // Remove old listeners if any
const previousInput = this.input; const previousInput = this.input;
this.mInput = new InputEnhancer(textarea, container, { this._input = new CartaInput(textarea, container, {
shortcuts: this.keyboardShortcuts, shortcuts: this.keyboardShortcuts,
prefixes: this.prefixes, prefixes: this.prefixes,
listeners: this.textareaListeners, listeners: this.textareaListeners,
historyOpts: this.historyOptions historyOpts: this.options?.historyOptions
}); });
if (previousInput) { if (previousInput) {
previousInput.events.removeEventListener('update', callback); previousInput.events.removeEventListener('update', callback);
this.mInput.history = previousInput.history; this._input.history = previousInput.history;
} }
this.mInput.events.addEventListener('update', callback); this._input.events.addEventListener('update', callback);
// Bind elements // Bind elements
this.elementsToBind.forEach((it) => { this.elementsToBind.forEach((it) => {
@ -453,7 +343,7 @@ export class Carta {
* @param container Div container of the rendered element. * @param container Div container of the rendered element.
*/ */
public $setRenderer(container: HTMLDivElement) { public $setRenderer(container: HTMLDivElement) {
this.mRenderer = new Renderer(container); this._renderer = new CartaRenderer(container);
} }
/** /**
@ -490,4 +380,14 @@ export class Carta {
} }
}; };
} }
/**
* Highlight Markdown using Speed-Highlight and this Carta instance highlighting rules.
* @param text Text to highlight.
* @returns Highlighted html text.
*/
public async highlight(text: string) {
loadCustomMarkdown(this.options?.extensions);
return highlight(text, 'cartamd', true);
}
} }

View file

@ -13,7 +13,7 @@
const debouncedRenderer = debounce(() => { const debouncedRenderer = debounce(() => {
carta.render(value).then((rendered) => (renderedHtml = rendered)); carta.render(value).then((rendered) => (renderedHtml = rendered));
}, carta.rendererDebounce ?? 300); }, carta.options?.rendererDebounce ?? 300);
$: { $: {
// On value updates // On value updates

View file

@ -1,199 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Carta } from '../carta';
import type { TextAreaProps } from '../textarea-props';
import { debounce } from '../utils';
import { isSingleTheme, loadNestedLanguages } from '../highlight';
export let carta: Carta;
export let value = '';
export let placeholder = '';
export let elem: HTMLDivElement;
export let handleScroll: (e: UIEvent) => void;
export let props: TextAreaProps = {};
let textarea: HTMLTextAreaElement;
let highlighElem: HTMLDivElement;
let highlighted = value;
let mounted = false;
export const resize = () => {
if (!mounted || !textarea) return;
textarea.style.height = highlighElem.scrollHeight + 'px';
textarea.scrollTop = 0;
};
const focus = () => {
// Allow text selection
const selectedText = window.getSelection()?.toString();
if (selectedText) return;
textarea?.focus();
};
const setInput = () => {
carta.$setInput(textarea, elem, () => {
value = textarea.value;
highlight(value);
});
};
const highlight = async (text: string) => {
const highlighter = await carta.highlighter();
let html: string;
if (isSingleTheme(highlighter.theme)) {
// Single theme
html = highlighter.codeToHtml(text, {
lang: highlighter.lang,
theme: highlighter.theme
});
} else {
// Dual theme
html = highlighter.codeToHtml(text, {
lang: highlighter.lang,
themes: highlighter.theme
});
}
if (carta.sanitizer) {
highlighted = carta.sanitizer(html);
} else {
highlighted = html;
}
};
const highlightNestedLanguages = debounce(async (text: string) => {
const highlighter = await carta.highlighter();
const { updated } = await loadNestedLanguages(highlighter, text);
if (updated) highlight(text);
}, 300);
$: highlight(value).then(resize);
$: highlightNestedLanguages(value);
onMount(() => {
mounted = true;
requestAnimationFrame(resize);
});
onMount(setInput);
</script>
<div role="tooltip" id="editor-unfocus-suggestion">
Press ESC then TAB to move the focus off the field
</div>
<div
on:click={focus}
on:keydown={focus}
on:scroll={handleScroll}
role="textbox"
tabindex="-1"
class="carta-input"
bind:this={elem}
>
<div class="carta-input-wrapper">
<div
class="carta-highlight carta-font-code"
tabindex="-1"
aria-hidden="true"
bind:this={highlighElem}
>
<!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}
</div>
<textarea
name="md"
id="md"
spellcheck="false"
class="carta-font-code"
aria-multiline="true"
aria-describedby="editor-unfocus-suggestion"
tabindex="0"
{placeholder}
{...props}
bind:value
bind:this={textarea}
on:scroll={() => (textarea.scrollTop = 0)}
/>
</div>
{#if mounted}
<slot />
{/if}
</div>
<style>
.carta-input {
position: relative;
}
.carta-input-wrapper {
height: 100%;
position: relative;
font-family: monospace;
}
textarea {
position: relative;
width: 100%;
max-width: 100%;
min-height: 100%;
overflow-y: hidden;
resize: none;
padding: 0;
margin: 0;
border: 0;
color: transparent;
background: transparent;
outline: none;
tab-size: 4;
}
.carta-highlight {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 0;
user-select: none;
height: fit-content;
padding: inherit;
margin: inherit;
word-wrap: break-word;
white-space: pre-wrap;
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 {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>

View file

@ -0,0 +1,125 @@
<script lang="ts">
import type { Carta } from '../carta';
import { onMount } from 'svelte';
export let carta: Carta;
export let value = '';
export let placeholder = '';
export let elem: HTMLDivElement;
export let handleScroll: (e: UIEvent) => void;
let textarea: HTMLTextAreaElement;
let highlighElem: HTMLPreElement;
let highlighted = value;
let mounted = false;
export const resize = () => {
if (!mounted || !textarea) return;
textarea.style.height = highlighElem.scrollHeight + 'px';
textarea.scrollTop = 0;
};
const focus = () => {
// Allow text selection
const selectedText = window.getSelection()?.toString();
if (selectedText) return;
textarea?.focus();
};
const setInput = () => {
carta.$setInput(textarea, elem, () => {
value = textarea.value;
highlight(value);
});
};
const highlight = async (text: string) => (highlighted = (await carta.highlight(text)) as string);
$: highlight(value).then(resize);
onMount(() => (mounted = true));
onMount(setInput);
</script>
<div
on:click={focus}
on:keydown={focus}
on:scroll={handleScroll}
role="textbox"
tabindex="0"
class="carta-input"
bind:this={elem}
>
<div class="carta-input-wrapper">
<pre
class="shj-lang-md carta-font-code"
bind:this={highlighElem}
aria-hidden="true"><!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}</pre>
<textarea
name="md"
id="md"
spellcheck="false"
class="carta-font-code"
{placeholder}
bind:value
bind:this={textarea}
on:scroll={() => (textarea.scrollTop = 0)}
/>
</div>
{#if mounted}
<slot />
{/if}
</div>
<style>
.carta-input {
position: relative;
}
.carta-input-wrapper {
height: 100%;
position: relative;
font-family: monospace;
}
textarea#md {
position: relative;
width: 100%;
max-width: 100%;
min-height: 100%;
overflow-y: hidden;
resize: none;
padding: 0;
margin: 0;
border: 0;
color: transparent;
background: transparent;
font-size: inherit;
outline: none;
}
pre {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 0;
user-select: none;
height: fit-content;
padding: inherit;
margin: inherit;
word-wrap: break-word;
white-space: pre-wrap;
word-break: break-word;
}
</style>

View file

@ -1,197 +0,0 @@
<script lang="ts">
import type { Labels } from '../labels';
import { handleArrowKeysNavigation } from '../accessibility';
import type { Carta } from '../carta';
import MenuIcon from './icons/MenuIcon.svelte';
import { onMount } from 'svelte';
import { debounce } from '../utils';
export let carta: Carta;
export let mode: 'tabs' | 'split';
export let tab: 'write' | 'preview';
export let labels: Labels;
let toolbar: HTMLDivElement;
let menu: HTMLDivElement;
let iconsContainer: HTMLDivElement;
let visibleIcons = [...carta.icons];
let availableWidth = 0;
let iconWidth = 0;
let toolbarHeight = 0;
let iconsHidden = false;
let showMenu = false;
const IconPadding = 8;
const waitForDOMUpdate = () => new Promise(requestAnimationFrame);
const onResize = debounce(async () => {
const overflowing = () => toolbar.scrollWidth - toolbar.clientWidth > 0;
while (overflowing()) {
visibleIcons.pop();
visibleIcons = visibleIcons;
await waitForDOMUpdate();
}
const fitting = () => availableWidth > 2 * iconWidth + IconPadding;
while (visibleIcons.length < carta.icons.length && fitting()) {
visibleIcons.push(carta.icons[visibleIcons.length]);
visibleIcons = visibleIcons;
await waitForDOMUpdate();
}
}, 100);
function onClick(event: MouseEvent) {
const target = event.target as HTMLElement;
if (menu && !menu.contains(target)) {
showMenu = false;
}
}
onMount(onResize);
$: iconsHidden = visibleIcons.length !== carta.icons.length;
</script>
<svelte:window on:resize={onResize} on:click={onClick} />
<div class="carta-toolbar" role="toolbar" bind:clientHeight={toolbarHeight} bind:this={toolbar}>
<div class="carta-toolbar-left">
{#if mode == 'tabs'}
<button
type="button"
tabindex={0}
class={tab === 'write' ? 'carta-active' : ''}
on:click={() => (tab = 'write')}
on:keydown={handleArrowKeysNavigation}
>
{labels.writeTab}
</button>
<button
type="button"
tabindex={-1}
class={tab === 'preview' ? 'carta-active' : ''}
on:click={() => (tab = 'preview')}
on:keydown={handleArrowKeysNavigation}
>
{labels.previewTab}
</button>
{/if}
</div>
<div class="carta-filler" bind:clientWidth={availableWidth} />
<div class="carta-toolbar-right" bind:this={iconsContainer}>
{#each visibleIcons as icon, index}
{@const label = labels.iconsLabels[icon.id] ?? icon.label}
<button
class="carta-icon"
tabindex={index == 0 ? 0 : -1}
title={label}
aria-label={label}
bind:clientWidth={iconWidth}
on:click|preventDefault|stopPropagation={() => {
carta.input && icon.action(carta.input);
carta.input?.update();
carta.input?.textarea.focus();
}}
on:keydown={handleArrowKeysNavigation}
>
<svelte:component this={icon.component} />
</button>
{/each}
{#if iconsHidden}
{@const label = labels.iconsLabels['menu'] ?? 'Menu'}
<button
class="carta-icon"
tabindex={-1}
title={label}
aria-label={label}
on:keydown={handleArrowKeysNavigation}
on:click|preventDefault|stopPropagation={() => (showMenu = !showMenu)}
>
<MenuIcon />
</button>
{/if}
</div>
</div>
{#if showMenu && iconsHidden}
<div class="carta-icons-menu" style="top: {toolbarHeight}px;" bind:this={menu}>
{#each carta.icons.filter((icon) => !visibleIcons.includes(icon)) as icon}
{@const label = labels.iconsLabels[icon.id] ?? icon.label}
<button
class="carta-icon-full"
aria-label={label}
on:click|preventDefault|stopPropagation={() => {
carta.input && icon.action(carta.input);
carta.input?.update();
carta.input?.textarea.focus();
showMenu = false;
}}
on:keydown={handleArrowKeysNavigation}
>
<svelte:component this={icon.component} />
<span>{label}</span>
</button>
{/each}
</div>
{/if}
<style>
.carta-toolbar {
height: 2rem;
display: flex;
flex-shrink: 0;
overflow-x: auto;
overflow-y: hidden;
}
.carta-toolbar-left {
display: flex;
align-items: center;
flex-wrap: nowrap;
height: 100%;
}
.carta-filler {
flex: 1;
}
.carta-toolbar-right {
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
}
.carta-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 3px;
cursor: pointer;
margin-left: 4px;
}
.carta-icon-full {
display: flex;
align-items: center;
border-radius: 3px;
cursor: pointer;
}
.carta-icons-menu {
position: absolute;
top: 100%;
right: 0;
display: flex;
flex-direction: column;
margin-right: 0.5rem;
z-index: 1;
}
</style>

View file

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
<path
fill="currentColor"
d="M4 8a2 2 0 1 1-3.999.001A2 2 0 0 1 4 8m6 0a2 2 0 1 1-3.999.001A2 2 0 0 1 10 8m6 0a2 2 0 1 1-3.999.001A2 2 0 0 1 16 8"
/>
</svg>

Before

Width:  |  Height:  |  Size: 249 B

View file

@ -1,268 +1,91 @@
import { detectLanguage } from '@speed-highlight/core/detect.js';
import { import {
getHighlighter, highlightText,
type BundledTheme, loadLanguage,
type ThemeInput, type ShjLanguage,
type StringLiteralUnion, type ShjLanguageDefinition
type BundledLanguage, } from '@speed-highlight/core';
type SpecialLanguage, import type { CartaExtension } from './carta';
type LanguageInput, import cartaMarkdown from './shj';
type LanguageRegistration,
type HighlighterGeneric, // Workaround to add intellisense
bundledLanguages, // eslint-disable-next-line @typescript-eslint/no-empty-interface
bundledThemes, interface Nothing {}
type ThemeRegistration type Union<T, U> = T | (U & Nothing);
} from 'shiki';
import type { Intellisense } from './utils'; type Lang = Union<ShjLanguage, string>;
/** /**
* Custom TextMate grammar rule for the highlighter. * Highlight text using Speed-Highlight. May return null on error(usually if requested
* language is not supported).
* @param text Text to highlight.
* @param lang Language to use, for example "js" or "c"
* @param hideLineNumbers Whether to hide line numbering.
* @returns Highlighted html text.
*/ */
export type GrammarRule = { export async function highlight(
name: string; text: string,
type: 'block' | 'inline'; lang: Lang,
definition: LanguageRegistration['repository'][string]; hideLineNumbers?: boolean
}; ): Promise<string | null> {
try {
/** return await highlightText(text, lang, true, { hideLineNumbers: hideLineNumbers ?? true });
* Custom TextMate highlighting rule for the highlighter. } catch (_) {
*/ return null;
export type HighlightingRule = {
light: NonNullable<ThemeRegistration['tokenColors']>[number];
dark: NonNullable<ThemeRegistration['tokenColors']>[number];
};
/**
* Shiki options for the highlighter.
*/
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;
} }
/** /**
* Checks if a language is a bundled language. * Highlight text using Speed-Highlight with detected language.
* @param lang The language to check. * @param text Text to highlight.
* @returns Whether the language is a bundled language. * @param hideLineNumbers Whether to hide line numbering.
* @returns Highlighted html text.
*/ */
export const isBundleLanguage = (lang: string): lang is BundledLanguage => export async function highlightAutodetect(text: string, hideLineNumbers?: boolean) {
Object.keys(bundledLanguages).includes(lang); const lang = await detectLanguage(text);
/** return await highlightText(text, lang, true, { hideLineNumbers: hideLineNumbers ?? true });
* Checks if a theme is a bundled theme. }
* @param theme The theme to check.
* @returns Whether the theme is a bundled theme.
*/
export const isBundleTheme = (theme: string): theme is BundledTheme =>
Object.keys(bundledThemes).includes(theme);
/**
* Checks if a theme is a dual theme.
* @param theme The theme to check.
* @returns Whether the theme is a dual theme.
*/
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme =>
typeof theme == 'object' && 'light' in theme && 'dark' in theme;
/**
* Checks if a theme is a single theme.
* @param theme The theme to check.
* @returns Whether the theme is a single theme.
*/
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme);
/**
* Checks if a theme is a theme registration.
* @param theme The theme to check.
* @returns Whether the theme is a theme registration.
*/
export const isThemeRegistration = (theme: Theme): theme is ThemeRegistration =>
typeof theme == 'object';
/** /**
* Find all nested languages in the markdown text and load them into the highlighter. * Load a custom language for reference in highlight rules.
* @param text Markdown text to parse for nested languages. * @param id Id of the language.
* @returns The set of nested languages found in the text. * @param langModule A module that has the default export set to an array of HighlightRule.
* @example
* ```
* // language.ts
* import type { HighlightLanguage } from 'carta-md';
*
* export default [
* {
* match: /helloworld/g,
* type: 'kwd'
* }
* ] satisfies HighlightLanguage;
* ```
* And in another file:
* ```
* import("./path/to/language")
* .then(module => Carta.loadCustomLanguage("lang-name", module));
* ```
*/ */
const findNestedLanguages = (text: string) => { export function loadCustomLanguage(id: string, langModule: { default: ShjLanguageDefinition }) {
const languages = new Set<string>(); return loadLanguage(id, langModule);
}
const regex = /```([a-z]+)\n([\s\S]+?)\n```/g; export interface HighlightFunctions {
let match: RegExpExecArray | null; highlight: typeof highlight;
while ((match = regex.exec(text))) { highlightAutodetect: typeof highlightAutodetect;
languages.add(match[1]); loadCustomLanguage: typeof loadCustomLanguage;
} }
return languages;
};
/** /**
* Load all nested languages found in the markdown text into the highlighter. * Load custom markdown syntax highlighting rules.
* @param highlighter The highlighter instance. * Automatically called when a Carta instance is created.
* @param text The text to parse for nested languages. * @param extensions Additional extensions used in Carta.
* @returns Whether the highlighter was updated with new languages.
*/ */
export const loadNestedLanguages = async (highlighter: Highlighter, text: string) => { export function loadCustomMarkdown(extensions: CartaExtension[] = []) {
text = text.replaceAll('\r\n', '\n'); // Normalize line endings const highlightRules = extensions.map((ext) => ext.highlightRules ?? []).flat();
const lang = [];
const languages = findNestedLanguages(text); lang.push(...cartaMarkdown, ...highlightRules);
const loadedLanguages = highlighter.getLoadedLanguages(); loadCustomLanguage('cartamd', { default: lang });
let updated = false; }
for (const lang of languages) {
if (isBundleLanguage(lang) && !loadedLanguages.includes(lang)) {
await highlighter.loadLanguage(lang);
loadedLanguages.push(lang);
updated = true;
}
}
return {
updated
};
};

View file

@ -6,20 +6,20 @@ interface HistoryState {
cursor: number; cursor: number;
} }
export interface TextAreaHistoryOptions { export interface CartaHistoryOptions {
/** /**
* Minimum interval between save states in ms. * Minimum interval between save states in ms.
* @default 300ms * @default 300ms
*/ */
minInterval?: number; minInterval: number;
/** /**
* Maximum history size in bytes. * Maximum history size in bytes.
* @default 1MB * @default 1MB
*/ */
maxSize?: number; maxSize: number;
} }
const defaultHistoryOptions: TextAreaHistoryOptions = { const defaultHistoryOptions: CartaHistoryOptions = {
minInterval: 300, minInterval: 300,
maxSize: 1_000_000 maxSize: 1_000_000
}; };
@ -27,11 +27,11 @@ const defaultHistoryOptions: TextAreaHistoryOptions = {
/** /**
* Input undo/redo functionality. * Input undo/redo functionality.
*/ */
export class TextAreaHistory { export class CartaHistory {
private states: HistoryState[] = []; private states: HistoryState[] = [];
private currentIndex = -1; // Only <= 0 numbers private currentIndex = -1; // Only <= 0 numbers
private readonly options: TextAreaHistoryOptions; private readonly options: CartaHistoryOptions;
constructor(options?: Partial<TextAreaHistoryOptions>) { constructor(options?: Partial<CartaHistoryOptions>) {
this.options = mergeDefaultInterface(options, defaultHistoryOptions); this.options = mergeDefaultInterface(options, defaultHistoryOptions);
} }
@ -79,7 +79,7 @@ export class TextAreaHistory {
} }
this.currentIndex = -1; this.currentIndex = -1;
if (latest && Date.now() - latest.timestamp.getTime() <= (this.options.minInterval ?? 300)) { if (latest && Date.now() - latest.timestamp.getTime() <= this.options.minInterval) {
this.states.pop(); this.states.pop();
} }
@ -94,7 +94,7 @@ export class TextAreaHistory {
// every char is 2 bytes // every char is 2 bytes
size += value.length * 2; size += value.length * 2;
while (size > (this.options.maxSize ?? 1_000_000)) { while (size > this.options.maxSize) {
const removed = this.states.shift(); const removed = this.states.shift();
if (!removed) break; // This should never happen if (!removed) break; // This should never happen
size -= removed.value.length * 2; size -= removed.value.length * 2;

View file

@ -1,5 +1,5 @@
import type { ComponentType } from 'svelte'; import type { ComponentType } from 'svelte';
import type { InputEnhancer } from './input'; import type { CartaInput } from './input';
import HeadingIcon from './components/icons/HeadingIcon.svelte'; import HeadingIcon from './components/icons/HeadingIcon.svelte';
import ItalicIcon from './components/icons/ItalicIcon.svelte'; import ItalicIcon from './components/icons/ItalicIcon.svelte';
import BoldIcon from './components/icons/BoldIcon.svelte'; import BoldIcon from './components/icons/BoldIcon.svelte';
@ -14,62 +14,42 @@ import StrikethroughIcon from './components/icons/StrikethroughIcon.svelte';
/** /**
* Editor toolbar icon information. * Editor toolbar icon information.
*/ */
export interface Icon { export interface CartaIcon {
/**
* The icon's unique identifier.
*/
id: string; id: string;
/** action: (input: CartaInput) => void;
* Callback function to execute when the icon is clicked.
* @param input InputEnhancer instance
*/
action: (input: InputEnhancer) => void;
/**
* The icon's component.
*/
component: ComponentType; component: ComponentType;
/**
* The icon's label (used as aria-label).
*/
label?: string;
} }
export const defaultIcons = [ export const defaultIcons = [
{ {
id: 'heading', id: 'heading',
action: (input) => input.toggleLinePrefix('###'), action: (input) => input.toggleLinePrefix('###'),
component: HeadingIcon, component: HeadingIcon
label: 'Heading'
}, },
{ {
id: 'bold', id: 'bold',
action: (input) => input.toggleSelectionSurrounding('**'), action: (input) => input.toggleSelectionSurrounding('**'),
component: BoldIcon, component: BoldIcon
label: 'Bold'
}, },
{ {
id: 'italic', id: 'italic',
action: (input) => input.toggleSelectionSurrounding('_'), action: (input) => input.toggleSelectionSurrounding('_'),
component: ItalicIcon, component: ItalicIcon
label: 'Italic'
}, },
{ {
id: 'strikethrough', id: 'strikethrough',
action: (input) => input.toggleSelectionSurrounding('~~'), action: (input) => input.toggleSelectionSurrounding('~~'),
component: StrikethroughIcon, component: StrikethroughIcon
label: 'Strikethrough'
}, },
{ {
id: 'quote', id: 'quote',
action: (input) => input.toggleLinePrefix('>'), action: (input) => input.toggleLinePrefix('>'),
component: QuoteIcon, component: QuoteIcon
label: 'Quote'
}, },
{ {
id: 'code', id: 'code',
action: (input) => input.toggleSelectionSurrounding('`'), action: (input) => input.toggleSelectionSurrounding('`'),
component: CodeIcon, component: CodeIcon
label: 'Code'
}, },
{ {
id: 'link', id: 'link',
@ -79,27 +59,23 @@ export const defaultIcons = [
input.insertAt(position, '(url)'); input.insertAt(position, '(url)');
input.textarea.setSelectionRange(position + 1, position + 4); input.textarea.setSelectionRange(position + 1, position + 4);
}, },
component: LinkIcon, component: LinkIcon
label: 'Link'
}, },
{ {
id: 'bulletedList', id: 'bulletedList',
action: (input) => input.toggleLinePrefix('- ', 'detach'), action: (input) => input.toggleLinePrefix('- ', 'detach'),
component: ListBulletedIcon, component: ListBulletedIcon
label: 'Bulleted list'
}, },
{ {
id: 'numberedList', id: 'numberedList',
action: (input) => input.toggleLinePrefix('1. ', 'detach'), action: (input) => input.toggleLinePrefix('1. ', 'detach'),
component: ListNumberedIcon, component: ListNumberedIcon
label: 'Numbered list'
}, },
{ {
id: 'taskList', id: 'taskList',
action: (input) => input.toggleLinePrefix('- [ ] ', 'detach'), action: (input) => input.toggleLinePrefix('- [ ] ', 'detach'),
component: ListTaskIcon, component: ListTaskIcon
label: 'Task list'
} }
] as const satisfies readonly Icon[]; ] as const satisfies readonly CartaIcon[];
export type DefaultIconId = (typeof defaultIcons)[number]['id'] | 'menu'; export type DefaultIconId = (typeof defaultIcons)[number]['id'];

View file

@ -1,7 +1,7 @@
import type { Listener } from './carta'; import type { CartaListener } from './carta';
import type { Prefix } from './prefixes'; import type { Prefix } from './prefixes';
import type { KeyboardShortcut } from './shortcuts'; import type { KeyboardShortcut } from './shortcuts';
import { TextAreaHistory as TextAreaHistory, type TextAreaHistoryOptions } from './history'; import { CartaHistory, type CartaHistoryOptions } from './history';
import { areEqualSets } from './utils'; import { areEqualSets } from './utils';
/** /**
@ -21,17 +21,16 @@ export interface InputSettings {
readonly shortcuts: KeyboardShortcut[]; readonly shortcuts: KeyboardShortcut[];
readonly prefixes: Prefix[]; readonly prefixes: Prefix[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly listeners: Listener<any>[]; readonly listeners: CartaListener<any>[];
readonly historyOpts?: Partial<TextAreaHistoryOptions>; readonly historyOpts?: Partial<CartaHistoryOptions>;
} }
export class InputEnhancer { export class CartaInput {
private pressedKeys: Set<string>; private pressedKeys: Set<string>;
private escapePressed = false;
// Used to detect keys that actually changed the textarea value // Used to detect keys that actually changed the textarea value
private onKeyDownValue: string | undefined; private onKeyDownValue: string | undefined;
public history: TextAreaHistory; public history: CartaHistory;
public readonly events = new EventTarget(); public readonly events = new EventTarget();
constructor( constructor(
@ -46,7 +45,6 @@ export class InputEnhancer {
textarea.addEventListener('focus', () => { textarea.addEventListener('focus', () => {
this.pressedKeys.clear(); this.pressedKeys.clear();
this.escapePressed = false;
}); });
textarea.addEventListener('blur', () => { textarea.addEventListener('blur', () => {
this.pressedKeys.clear(); this.pressedKeys.clear();
@ -54,7 +52,7 @@ export class InputEnhancer {
textarea.addEventListener('mousedown', this.handleMouseDown.bind(this)); textarea.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.history = new TextAreaHistory(settings.historyOpts); this.history = new CartaHistory(settings.historyOpts);
// Save initial value // Save initial value
this.history.saveState(this.textarea.value, this.textarea.selectionStart); this.history.saveState(this.textarea.value, this.textarea.selectionStart);
@ -125,33 +123,13 @@ export class InputEnhancer {
if (key === 'enter') { if (key === 'enter') {
// Check prefixes // Check prefixes
this.handleNewLine(e); this.handleNewLine(e);
} else if (key == 'tab' && !this.escapePressed) { } else if (key == 'tab') {
e.preventDefault(); // Don't select other stuff e.preventDefault(); // Don't select other stuff
const position = this.textarea.selectionStart;
if (e.shiftKey) { this.insertAt(this.textarea.selectionStart, '\t');
// Unindent this.textarea.selectionStart = position + 1;
const line = this.getLine(); this.textarea.selectionEnd = position + 1;
const lineStart = line.start;
const lineContent = line.value;
const position = this.textarea.selectionStart;
// Check if the line starts with a tab
if (lineContent.startsWith('\t')) {
// Remove the tab
this.removeAt(lineStart, 1);
this.textarea.selectionStart = position - 1;
this.textarea.selectionEnd = position - 1;
}
} else {
const position = this.textarea.selectionStart;
this.insertAt(this.textarea.selectionStart, '\t');
this.textarea.selectionStart = position + 1;
this.textarea.selectionEnd = position + 1;
}
this.update(); this.update();
} else if (key === 'escape') {
this.escapePressed = true;
} }
this.onKeyDownValue = this.textarea.value; this.onKeyDownValue = this.textarea.value;
} }
@ -241,6 +219,9 @@ export class InputEnhancer {
start: lineStartingIndex, start: lineStartingIndex,
end: lineEndingIndex, end: lineEndingIndex,
value: this.textarea.value.slice(lineStartingIndex, lineEndingIndex) value: this.textarea.value.slice(lineStartingIndex, lineEndingIndex)
/**
* Position of the cursor relative to the line.
*/
}; };
} }

View file

@ -1,19 +0,0 @@
import type { DefaultIconId } from './icons';
import type { Intellisense } from './utils';
type IconId = Intellisense<DefaultIconId>;
/**
* Labels that may appear in the editor.
*/
export interface Labels {
writeTab: string;
previewTab: string;
iconsLabels: Partial<Record<IconId, string>>;
}
export const defaultLabels: Labels = {
writeTab: 'Write',
previewTab: 'Preview',
iconsLabels: {}
};

View file

@ -1,4 +1,4 @@
export class Renderer { export class CartaRenderer {
constructor(public readonly container: HTMLDivElement) {} constructor(public readonly container: HTMLDivElement) {}
// Reserved for future use // Reserved for future use
} }

View file

@ -0,0 +1,54 @@
import { detectLanguage } from '@speed-highlight/core/detect.js';
import type { ShjLanguageDefinition } from '@speed-highlight/core';
/**
* Markdown syntax highlighting rules.
*/
const cartaMarkdown: ShjLanguageDefinition = [
{
type: 'cmnt',
match: /^>.*|(=|-)\1+/gm
},
{
type: 'class',
match: /\*\*((?!\*\*).)*\*\*/g
},
{
match: /```((?!```)[^])*\n```/g,
sub: (code) => ({
type: 'kwd',
sub: [
{
match: /\n[^]*(?=```)/g,
sub: code.split('\n')[0].slice(3) || detectLanguage(code)
}
]
})
},
{
type: 'str',
match: /`[^`]*`/g
},
{
type: 'var',
match: /~~((?!~~).)*~~/g
},
{
type: 'kwd',
match: /_[^_]*_|\*[^*]*\*/g
},
{
type: 'kwd',
match: /^\s*(\*|\d+\.)\s/gm
},
{
type: 'oper',
match: /\[[^\]]*]/g
},
{
type: 'func',
match: /\([^)]*\)/g
}
];
export default cartaMarkdown;

View file

@ -1,4 +1,4 @@
import type { InputEnhancer } from './input'; import type { CartaInput } from './input';
/** /**
* Keyboard shortcut data. * Keyboard shortcut data.
@ -13,7 +13,7 @@ export interface KeyboardShortcut {
* Callback action. * Callback action.
* @param input Input helper. * @param input Input helper.
*/ */
action: (input: InputEnhancer) => void; action: (input: CartaInput) => void;
/** /**
* Prevent saving the current state in history. * Prevent saving the current state in history.
*/ */

View file

@ -1,36 +0,0 @@
/**
* Base props for HTML textarea element.
*/
interface BaseTextAreaProps {
id?: string;
name?: string;
spellCheck?: boolean;
autoCapitalize?: string;
autoComplete?: string;
autoFocus?: boolean;
dirname?: string;
disabled?: boolean;
form?: string;
maxLength?: number;
minLength?: number;
required?: boolean;
spellcheck?: boolean;
// Props handled by Carta
/**
* Bind the value to the Editor instead.
*/
value?: never;
/**
* Use the placeholder property of the Editor instead.
*/
placeholder?: never;
class?: never;
}
/**
* Props for HTML textarea element.
*/
export type TextAreaProps<T extends Record<string, unknown> = Record<string, unknown>> =
BaseTextAreaProps & T;

View file

@ -1,12 +1,3 @@
// Workaround to add intellisense
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Nothing {}
type Union<T, U> = T | (U & Nothing);
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.
* @param cb Callback function. * @param cb Callback function.

View file

@ -0,0 +1,82 @@
.shj-inline {
margin: 0;
padding: 2px 5px;
display: inline-table;
border-radius: 5px;
}
.shj-numbers {
padding-left: 5px;
counter-reset: line;
}
.shj-numbers div {
padding-right: 5px;
}
.shj-numbers div::before {
color: #999;
display: block;
content: counter(line);
opacity: 0.5;
text-align: right;
margin-right: 5px;
counter-increment: line;
}
.shj-syn-cmnt {
font-style: italic;
}
.shj-syn-err,
.shj-syn-kwd {
color: #e16;
}
.shj-syn-num,
.shj-syn-class {
color: #f60;
}
.shj-numbers,
.shj-syn-cmnt {
color: #999;
}
.shj-syn-insert,
.shj-syn-str {
color: #7d8;
}
.shj-syn-bool {
color: #3bf;
}
.shj-syn-type,
.shj-syn-oper {
color: #5af;
}
.shj-syn-section,
.shj-syn-func {
color: #84f;
}
.shj-syn-deleted,
.shj-syn-var {
color: #f44;
}
.shj-oneline {
padding: 12px 10px;
}
.shj-lang-http.shj-oneline .shj-syn-kwd {
background: #25f;
color: #fff;
padding: 5px 7px;
border-radius: 5px;
}
.shj-multiline.shj-mode-header {
padding: 20px;
}
.shj-multiline.shj-mode-header:before {
content: attr(data-lang);
color: #58f;
display: block;
padding: 10px 20px;
background: #58f3;
border-radius: 5px;
margin-bottom: 20px;
}

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { MarkdownEditor } from '$lib'; import { CartaEditor } from '$lib';
import { Carta } from '$lib/internal/carta'; import { Carta } from '$lib/internal/carta';
import ToggleTheme from './ToggleTheme.svelte';
import sampleText from './sample.md?raw';
import '$lib/default.css'; import '$lib/default.css';
import '$lib/light.css';
const carta = new Carta(); const carta = new Carta();
</script> </script>
@ -23,8 +23,7 @@
</svelte:head> </svelte:head>
<main> <main>
<ToggleTheme class="toggle-theme" /> <CartaEditor placeholder="Some text..." mode="split" {carta} />
<MarkdownEditor value={sampleText} placeholder="Some text..." mode="tabs" {carta} />
</main> </main>
<style> <style>
@ -34,10 +33,9 @@
min-height: 100vh; min-height: 100vh;
} }
:global(.carta-font-code) { :global(.carta-font-code, code) {
font-family: 'Fira Code', monospace; font-family: 'Fira Code', monospace;
font-variant-ligatures: normal; font-variant-ligatures: normal;
font-size: 1.1rem;
} }
:global(input, textarea, button) { :global(input, textarea, button) {
@ -45,21 +43,9 @@
} }
main { main {
position: relative;
max-width: 1536px; max-width: 1536px;
margin: 2rem auto 2rem auto; margin: 0 auto 0 auto;
} padding: 2rem 0 2rem 0;
:global(img) {
max-width: 50%;
}
:global(.toggle-theme) {
position: absolute;
top: 0;
left: -6px;
transform: translateX(-100%);
} }
/* Responsive main */ /* Responsive main */

View file

@ -1,74 +0,0 @@
<script lang="ts">
let className = '';
let theme: 'light' | 'dark' = 'light';
export { className as class };
</script>
<button
class="{className} {theme}"
on:click={() => {
if (theme === 'light') {
document.documentElement.classList.add('dark');
theme = 'dark';
} else {
document.documentElement.classList.remove('dark');
theme = 'light';
}
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<path
fill="currentColor"
d="M235.54 150.21a104.84 104.84 0 0 1-37 52.91A104 104 0 0 1 32 120a103.09 103.09 0 0 1 20.88-62.52a104.84 104.84 0 0 1 52.91-37a8 8 0 0 1 10 10a88.08 88.08 0 0 0 109.8 109.8a8 8 0 0 1 10 10Z"
/>
</svg>
</button>
<style>
button {
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
background: none;
border: 1px solid #b9b9b9;
border-radius: 4px;
font-size: 1.5rem;
cursor: pointer;
}
button.dark {
color: #fff;
border-color: #4d4d4c;
}
:global(html.dark) {
background: #1b1b1f;
}
:global(html.dark .markdown-body) {
color: #fff;
}
/* Editor dark mode */
:global(html.dark .carta-theme__default) {
--border-color: var(--border-color-dark);
--selection-color: var(--selection-color-dark);
--focus-outline: var(--focus-outline-dark);
--hover-color: var(--hover-color-dark);
--caret-color: var(--caret-color-dark);
--text-color: var(--text-color-dark);
}
/* Code dark mode */
:global(html.dark .shiki),
:global(html.dark .shiki span) {
color: var(--shiki-dark) !important;
}
</style>

View file

@ -1,79 +0,0 @@
# Heading
## Sub-heading
Paragraphs are separated
by a blank line.
Two spaces at the end of a line
produce a line break.
Text attributes _italic_,
**bold**, `monospace`. Some `console.log(lst.filter(e => e == true))` implementations may use _single-asterisks_ for italic text.
Horizontal rule:
---
```js
function resolveAfter2Seconds(x) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
// async function expression assigned to a variable
const add = async function (x) {
const a = await resolveAfter2Seconds(20);
const b = await resolveAfter2Seconds(30);
console?.log(`http://localhost:${PORT}/`.match(/:[0-9]{2,4}^/g));
return x + a + b;
};
add(10).then((v) => {
console.log(v); // prints 60 after 4 seconds.
});
// async function expression used as an IIFE
(async function (x) {
const p1 = resolveAfter2Seconds(20);
const p2 = resolveAfter2Seconds(30);
return x + (await p1) + (await p2);
})(10).then((v) => {
console.log(v); // prints 60 after 2 seconds.
});
```
```beurihiuerh
}
```
Strikethrough:
~~strikethrough~~
Bullet list:
- apples
- oranges
- pears
Numbered list:
1. lather
2. rinse
3. repeat
An [example](http://example.com).
![pic](pic.jpg)
> Markdown uses email-style
> characters for blockquoting.
> Multiple paragraphs need to be prepended individually.
| Item | Price | In stock |
| ------------ | -------- | -------- |
| Juicy Apples | 1.99 | _7_ |
| Bananas | **1.89** | 5234 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {

View file

@ -13,7 +13,12 @@
"strict": true, "strict": true,
"composite": true, "composite": true,
"ignoreDeprecations": "5.0", "ignoreDeprecations": "5.0",
"plugins": [] "plugins": [],
"baseUrl": "./",
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
}, },
"include": ["./src"] "include": ["./src"]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias

View file

@ -1,11 +0,0 @@
.DS_Store
node_modules
/build
/dist
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View file

@ -1,36 +0,0 @@
# Carta Anchor Plugin
This plugin adds `id` attributes and permalinks to headings. Install it using:
```
npm i @cartamd/plugin-anchor
```
## Setup
### Styles
Import the default theme, or create you own:
```ts
import '@cartamd/plugin-anchor/default.css';
```
### Extension
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { anchor } from '@cartamd/plugin-anchor';
const carta = new Carta({
extensions: [anchor()]
});
</script>
<MarkdownEditor {carta} />
```
## Documentation
Checkout the [docs](https://beartocode.github.io/carta/plugins/anchor) for examples, options and more.

View file

@ -1,69 +0,0 @@
{
"name": "@cartamd/plugin-anchor",
"version": "3.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "MIT",
"scripts": {
"dev": "vite dev",
"build": "vite build && npm run package",
"preview": "vite preview",
"package": "svelte-kit sync && svelte-package && publint",
"prepublishOnly": "npm run package",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/BearToCode/carta.git"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
},
"./default.css": "./dist/default.css",
"./default-theme.css": "./dist/default.css"
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"dependencies": {
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0"
},
"peerDependencies": {
"carta-md": "^4.0.0",
"svelte": "^3.54.0 || ^4.0.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/package": "^2.3.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"carta-md": "workspace:*",
"marked": "^9.1.5",
"publint": "^0.1.9",
"svelte": "^3.54.0 || ^4.0.0",
"svelte-check": "^3.6.7",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.1.6"
},
"svelte": "./dist/index.js",
"keywords": [
"carta",
"markdown",
"editor",
"marked",
"text editor",
"marked editor",
"slash",
"syntax highlighting",
"emoji",
"katex"
]
}

View file

@ -1,12 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View file

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body>
</html>

View file

@ -1,32 +0,0 @@
.carta-viewer h1,
.carta-viewer h2,
.carta-viewer h3,
.carta-viewer h4,
.carta-viewer h5,
.carta-viewer h6 {
position: relative;
}
.carta-viewer h1 .icon.icon-link,
.carta-viewer h2 .icon.icon-link,
.carta-viewer h3 .icon.icon-link,
.carta-viewer h4 .icon.icon-link,
.carta-viewer h5 .icon.icon-link,
.carta-viewer h6 .icon.icon-link {
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;
}

View file

@ -1,32 +0,0 @@
import rehypeSlug, { type Options as SlugOptions } from 'rehype-slug';
import rehypeAutolinkHeadings, { type Options as AutolinkOptions } from 'rehype-autolink-headings';
import type { Plugin } from 'carta-md';
export * from './default.css?inline';
export interface AnchorExtensionOptions {
/**
* rehype-slug options.
*/
slug?: SlugOptions;
/**
* rehype-autolink-headings options.
*/
autolink?: AutolinkOptions;
}
/**
* Carta anchor plugin. Adds support to render anchor links in header tags.
*/
export const anchor = (options?: AnchorExtensionOptions): Plugin => {
return {
transformers: [
{
execution: 'sync',
type: 'rehype',
transform({ processor }) {
processor.use(rehypeSlug, options?.slug).use(rehypeAutolinkHeadings, options?.autolink);
}
}
]
};
};

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
<path fill="currentColor" fill-rule="evenodd" d="M9.929 3.132a2.078 2.078 0 1 1 2.94 2.94l-.65.648a.75.75 0 0 0 1.061 1.06l.649-.648a3.579 3.579 0 0 0-5.06-5.06L6.218 4.72a3.58 3.58 0 0 0 0 5.06a.75.75 0 0 0 1.061-1.06a2.08 2.08 0 0 1 0-2.94zm-.15 3.086a.75.75 0 0 0-1.057 1.064c.816.81.818 2.13.004 2.942l-2.654 2.647a2.08 2.08 0 0 1-2.94-2.944l.647-.647a.75.75 0 0 0-1.06-1.06l-.648.647a3.58 3.58 0 0 0 5.06 5.066l2.654-2.647a3.575 3.575 0 0 0-.007-5.068Z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 575 B

View file

@ -1,94 +0,0 @@
<script lang="ts">
import { Carta, MarkdownEditor } from 'carta-md';
import { anchor } from '$lib';
import 'carta-md/default.css';
import '$lib/default.css';
const carta = new Carta({
sanitizer: false,
extensions: [
anchor({
autolink: {}
})
]
});
let value = '# Heading\n## Heading 1\n### Heading 2';
</script>
<svelte:head>
<!-- Custom font -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
</svelte:head>
<main>
<MarkdownEditor {carta} {value} />
</main>
<style>
:global(body) {
margin: 0;
font-family: 'Inter var', sans-serif;
min-height: 100vh;
}
:global(.carta-font-code) {
font-family: 'Fira Code', monospace;
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) {
font-family: inherit;
}
main {
max-width: 1536px;
margin: 0 auto 0 auto;
padding: 2rem 0 2rem 0;
}
/* Responsive main */
@media screen and (max-width: 640px) {
main {
width: 95%;
}
}
@media screen and (min-width: 640px) and (max-width: 767px) {
main {
width: 640px;
}
}
@media screen and (min-width: 767px) and (max-width: 1023px) {
main {
width: 768px;
}
}
@media screen and (min-width: 1023px) and (max-width: 1279px) {
main {
width: 1024px;
}
}
@media screen and (min-width: 1279px) and (max-width: 1535px) {
main {
width: 1280px;
}
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,18 +0,0 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

View file

@ -1,16 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"composite": true,
"ignoreDeprecations": "5.0",
"plugins": []
}
}

View file

@ -1,6 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});

View file

@ -20,7 +20,7 @@ import '@cartamd/plugin-attachment/default.css';
```svelte ```svelte
<script lang="ts"> <script lang="ts">
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import { attachment } from '@cartamd/plugin-attachment'; import { attachment } from '@cartamd/plugin-attachment';
const carta = new Carta({ const carta = new Carta({
@ -34,7 +34,7 @@ import '@cartamd/plugin-attachment/default.css';
}); });
</script> </script>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
``` ```
## Documentation ## Documentation

View file

@ -34,24 +34,23 @@
"!dist/**/*.spec.*" "!dist/**/*.spec.*"
], ],
"peerDependencies": { "peerDependencies": {
"carta-md": "^4.0.0", "carta-md": "^3.0.0",
"marked": "^9.1.5", "marked": "^9.1.5",
"svelte": "^3.54.0 || ^4.0.0" "svelte": "^3.54.0 || ^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1", "@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^2.5.4", "@sveltejs/kit": "^1.27.1",
"@sveltejs/package": "^2.3.0", "@sveltejs/package": "^2.2.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/node-emoji": "^1.8.2", "@types/node-emoji": "^1.8.2",
"carta-md": "workspace:*", "carta-md": "workspace:*",
"marked": "^9.1.5", "marked": "^9.1.5",
"publint": "^0.1.9", "publint": "^0.1.9",
"svelte": "^4.2.12", "svelte": "^4.2.2",
"svelte-check": "^3.6.7", "svelte-check": "^3.5.2",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^5.1.6" "vite": "^4.3.9"
}, },
"svelte": "./dist/index.js", "svelte": "./dist/index.js",
"keywords": [ "keywords": [

View file

@ -1,4 +1,4 @@
import type { Carta, Plugin, Listener } from 'carta-md'; import type { Carta, CartaExtension, CartaListener } from 'carta-md';
import { get, writable, type Writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
import type { SvelteComponent } from 'svelte'; import type { SvelteComponent } from 'svelte';
import DropOverlay from './DropOverlay.svelte'; import DropOverlay from './DropOverlay.svelte';
@ -40,7 +40,7 @@ const ImageMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml']
/** /**
* Carta attachment plugin. * Carta attachment plugin.
*/ */
export const attachment = (options: AttachmentExtensionOptions): Plugin => { export const attachment = (options: AttachmentExtensionOptions): CartaExtension => {
let carta: Carta | undefined; let carta: Carta | undefined;
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes; const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
@ -102,31 +102,15 @@ export const attachment = (options: AttachmentExtensionOptions): Plugin => {
for (const file of files) handleFile(file); for (const file of files) handleFile(file);
} }
function handlePaste(this: HTMLTextAreaElement, e: ClipboardEvent) {
const items = e.clipboardData?.items;
if (!items) return;
const itemsArray = Array.from(items);
for (const item of itemsArray) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (!file) continue;
e.preventDefault();
handleFile(file);
}
}
}
return { return {
onLoad: ({ carta: c }) => { onLoad: ({ carta: c }) => {
carta = c; carta = c;
}, },
listeners: [ listeners: [
['drop', handleDrop, false] satisfies Listener<'drop'>, ['drop', handleDrop, false] satisfies CartaListener<'drop'>,
['dragenter', () => draggingOverTextArea.set(true)] satisfies Listener<'dragenter'>, ['dragenter', () => draggingOverTextArea.set(true)] satisfies CartaListener<'dragenter'>,
['dragleave', () => draggingOverTextArea.set(false)] satisfies Listener<'dragleave'>, ['dragleave', () => draggingOverTextArea.set(false)] satisfies CartaListener<'dragleave'>,
['dragover', (e) => e.preventDefault()] satisfies Listener<'dragover'>, ['dragover', (e) => e.preventDefault()] satisfies CartaListener<'dragover'>
['paste', handlePaste, false] satisfies Listener<'paste'>
], ],
components: [ components: [
{ {
@ -167,8 +151,7 @@ export const attachment = (options: AttachmentExtensionOptions): Plugin => {
input.click(); input.click();
}, },
id: 'attach', id: 'attach'
label: 'Attach file'
} }
] ]
}; };

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { attachment } from '$lib'; import { attachment } from '$lib';
import { Carta, MarkdownEditor } from 'carta-md'; import { Carta, CartaEditor } from 'carta-md';
import 'carta-md/default.css'; import 'carta-md/default.css';
import '$lib/default.css'; import '$lib/default.css';
@ -30,7 +30,7 @@
</svelte:head> </svelte:head>
<main> <main>
<MarkdownEditor {carta} /> <CartaEditor {carta} />
</main> </main>
<style> <style>

Some files were not shown because too many files have changed in this diff Show more