Compare commits

..

63 commits

Author SHA1 Message Date
BearToCode
2177545454 docs: fix grammar 2024-05-02 19:28:12 +02:00
BearToCode
a303834127 build: fix shiki version difference 2024-05-02 19:26:54 +02:00
BearToCode
3fd18a6796 fix: update shiki, fix latex highlighting issues 2024-05-02 17:26:17 +02:00
BearToCode
486a885704 docs: specify to load the language into the highlighter 2024-05-02 17:11:26 +02:00
BearToCode
8e5803a1c4 docs: fix style check issue 2024-05-02 11:30:35 +02:00
BearToCode
0ff9e5b124 docs: add guide on using Svelte components 2024-05-02 11:26:12 +02:00
BearToCode
eb114a7311 docs: small fix in style 2024-05-01 18:21:40 +02:00
BearToCode
34d4f400e6 docs: fix wrong name for Plugins 2024-05-01 18:15:42 +02:00
BearToCode
cc79b25288 docs: add missing copy button 2024-05-01 16:35:47 +02:00
BearToCode
a44b8e971d docs: add dark mode chapter 2024-05-01 16:30:13 +02:00
BearToCode
bafc86d8d3 fix: handle caret endings in Markdown component
fix: #56
2024-04-28 15:46:51 +02:00
BearToCode
a086ece088 docs: update old plugin code readme 2024-04-20 20:05:27 +02:00
BearToCode
dddaad1f1b fix(plugin-anchor): wrong class in default stylesheet 2024-04-20 19:46:00 +02:00
BearToCode
7261d295e2 docs: fix editor code having padding 2024-04-18 12:49:11 +02:00
BearToCode
482fd4b8af fix(plugin-tikz): remove wrapping pre 2024-04-15 18:16:43 +02:00
BearToCode
dfc8812123 fix(plugin-tikz): prevent crash when className property is not set 2024-04-14 18:37:48 +02:00
BearToCode
54358b649b fix(plugin-code): do not import isSingleTheme for main package
fix #50
2024-04-14 18:24:18 +02:00
BearToCode
7ad874e6aa fix(plugin-code): module not found error 2024-04-14 18:08:33 +02:00
BearToCode
545864a202 docs: fix grammar/style 2024-04-13 19:59:43 +02:00
BearToCode
0cef0b94ed docs: fix laggy header tracker 2024-04-12 20:14:11 +02:00
BearToCode
20d58a856b docs: fix code background 2024-04-12 16:34:34 +02:00
BearToCode
1a61f58e7d fix(plugin-tikz): invalid version 2024-04-12 16:22:49 +02:00
BearToCode
7d4034c316 fix(plugin-code): invalid version 2024-04-12 16:18:06 +02:00
BearToCode
26241b5bcd fix(plugin-emoji): invalid version 2024-04-12 16:12:56 +02:00
BearToCode
3785fd01c0 fix(plugin-slash): invalid version 2024-04-12 16:07:48 +02:00
BearToCode
c9be45a1ee fix(plugin-math): invalid version 2024-04-12 16:02:45 +02:00
BearToCode
c4428b8150 docs: fix relative links 2024-04-12 16:00:39 +02:00
Davide
036af273f9
v4 (#46) 2024-04-12 15:55:06 +02:00
BearToCode
58a7a8848f chore: migrate plugin-attachment 2024-04-12 08:29:41 +02:00
BearToCode
255913bd35 docs: minor fixes 2024-04-12 08:24:47 +02:00
BearToCode
e175e87bcc style: fix formatting 2024-04-12 08:24:46 +02:00
BearToCode
7e4a387523 docs: update docs, add migration guide 2024-04-12 08:24:46 +02:00
BearToCode
342c8d24d0 feat: use unified+rehype for parsing markdown
`marked` has been replaced with a more modern setup involving Unified JS, Rehype and various
plugins.

BREAKING CHANGE: Replaced `marked` with `unified` and `rehype`
2024-04-12 08:24:46 +02:00
BearToCode
9e1b0d1850 docs: update README 2024-04-12 08:24:46 +02:00
BearToCode
b30b2af6d9 fix: default theme placeholder color 2024-04-12 08:24:46 +02:00
BearToCode
c6a81acedd fix: default theme caret color 2024-04-12 08:24:46 +02:00
BearToCode
d1590ea384 build: move plugins peer-deps to v4 2024-04-12 08:24:46 +02:00
BearToCode
24be57a10f refactor: require explicit disabling of sanitizer
BREAKING CHANGE: Require `sanitizer` to be provided in the options. To disable it, set it explicitly
to `false`.
2024-04-12 08:24:46 +02:00
BearToCode
eb647b416f refactor: do not store options inside Carta
BREAKING CHANGE: Remove `Carta.options`. Update API
2024-04-12 08:24:46 +02:00
BearToCode
ae15b5b590 feat: use shiki for syntax highlighting
Remove Speed-Syntax Highlight, as the newer versions stopped working, and use ShikiJS instead. Also
has better syntax and more languages supported.

BREAKING CHANGE: Replace SHJ with ShikiJS, removed old themes and added new ones.
2024-04-12 08:24:46 +02:00
BearToCode
6e9cb68141 wip: use shiki for highlighting 2024-04-12 08:24:45 +02:00
BearToCode
f0abf195b8 refactor: remove verbose prefixes
Remove "Carta" prefixes from various interface/components.

BREAKING CHANGE: Different objects have been renamed
2024-04-12 08:24:37 +02:00
Davide
8f4188936e
feat(plugin-attachment): add file pasting functionality (#47) 2024-04-12 07:48:03 +02:00
ev
bed8ba5d47 feat(plugin-attachment): add file pasting functionality
allows pasting of files using a paste listener similar to github's readme editor

re #45
2024-04-12 01:28:54 +01:00
BearToCode
b7fecea206 fix: update katex to latest version 2024-03-28 20:31:16 +01:00
BearToCode
aeac8073b3 fix: icons oveflows 2024-03-19 18:37:42 +01:00
BearToCode
e430bf035a feat: add icons menu
Add a collapsable icons menu to the toolbar

fix #42
2024-03-19 15:16:54 +01:00
BearToCode
f0aaaed536 ci: remove signatures audit step
The step is currently failing due to vite 5.1.6 invalid signatures
2024-03-19 14:50:47 +01:00
BearToCode
3e8e7417d4 build: update dev dependencies 2024-03-18 19:41:22 +01:00
BearToCode
a36b57f0c4 docs: fix labels in wrong section 2024-03-06 19:00:51 +01:00
BearToCode
46a58adbec docs: update sanitizer example 2024-03-05 14:28:53 +01:00
Davide
b0deb50a25
docs: add missing disableIcons option 2024-02-11 14:15:00 +01:00
BearToCode
db3770cece docs: fix links style 2024-01-23 21:25:07 +01:00
BearToCode
00d67f3cbf docs: fix styles and links for community plugins 2024-01-23 21:16:16 +01:00
BearToCode
0af2e23f25 docs: fix margin styles in community plugins 2024-01-22 18:51:15 +01:00
BearToCode
893742d90e docs: add community plugins in README and docs 2024-01-22 18:45:57 +01:00
Davide
d33285251a
feat(cartaeditor): add tooltip on quick toolbar icon buttons (#34) 2024-01-19 09:34:24 +01:00
maisonsmd
4693658089 feat(cartaeditor): add tooltip on quick toolbar icon buttons
add tooltip on quick toolbar icon buttons
2024-01-18 10:39:51 +07:00
Davide
d3f3b4cc09
Add heading anchor link plugin (#31) 2024-01-16 19:27:27 +01:00
BearToCode
4843c27c67 chore: add plugin-achor to list of packages 2024-01-16 19:24:48 +01:00
maisonsmd
8f98e2ad36 docs(add docs for anchor plugin): add docs for anchor plugin
Add docs for anchor plugin
2024-01-16 23:22:26 +07:00
maisonsmd
2884ceeff1 feat(add new plugin: anchor): add new plugin: anchor
Add ability to render anchor links after headings
2024-01-16 23:17:32 +07:00
BearToCode
5ddddbdc2e fix(plugin-math): automatically enable displayMode for blocks 2024-01-06 12:31:12 +01:00
119 changed files with 7066 additions and 2115 deletions

View file

@ -53,9 +53,6 @@ jobs:
- name: Build all packages
run: pnpm build
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
- name: Release
run: npm run publish
env:

View file

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

55
.vscode/tailwind.json vendored Normal file
View file

@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that 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,31 +1,25 @@
<div align="right">
<a href="https://www.npmjs.com/package/carta-md">
<img src="https://img.shields.io/npm/v/carta-md?color=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
</a>
<a href="https://bundlephobia.com/package/carta-md">
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
</a>
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/carta-md?color=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&labelColor=171d27&logo=git&logoColor=white" alt="license">
</a>
<a href="http://beartocode.github.io/carta/">
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs">
</a>
</div>
<div align="center">
<a href="https://beartocode.github.io/carta/">
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
</a>
</div>
[![Carta.png](https://i.postimg.cc/nV6DMXKM/Carta.png)](https://beartocode.github.io/carta/)
<br>
<div align="center"><strong>Carta</strong></div>
<div align="center">Swiftly edit and render Markdown, with no overhead.</div>
<h1 align="center"><strong>Carta</strong></h1>
<div align="center">Modern, lightweight, powerful Markdown Editor.</div>
<br />
<div align="center">
<a href="https://beartocode.github.io/carta/">Documentation</a>
<a href="https://beartocode.github.io/carta/">📚 Documentation</a>
<span> · </span>
<a href="https://github.com/BearToCode/carta">GitHub</a>
</div>
@ -34,23 +28,30 @@
# Introduction
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer, based on [Marked](https://github.com/markedjs/marked). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
> [!NOTE]
> Carta has recently been updated to `v4`, which features numerous major changes.
>
> Follow the [Migration Guide](http://beartocode.github.io/carta/migration) to update your project.
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer. It is powered by [unified](https://github.com/unifiedjs/unified), [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
Differently from most editors, Carta does not include a code editor, but it is _just_ a textarea with syntax highlighting, shortcuts and more.
## Features
- Keyboard **shortcuts** (extensible);
- Toolbar (extensible);
- Markdown syntax highlighting;
- Scroll sync;
- Accessibility friendly;
- **SSR** compatible;
- **Katex** support (plugin);
- **Slash** commands (plugin);
- **Emojis**, with included search (plugin);
- **Tikz** support (plugin);
- **Attachment** support (plugin);
- Code blocks **syntax highlighting** (plugin).
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
- 🛠️ Toolbar (extensible);
- ⌨️ Keyboard **shortcuts** (extensible);
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark;
- 🔀 Scroll sync;
- ✅ Accessibility friendly;
- 🖥️ **SSR** compatible;
- ⚗️ **KaTeX** support (plugin);
- 🔨 **Slash** commands (plugin);
- 😄 **Emojis**, with included search (plugin);
- ✏️ **TikZ** support (plugin);
- 📂 **Attachment** support (plugin);
- ⚓ **Anchor** links in headings (plugin);
- 🌈 Code blocks **syntax highlighting** (plugin).
## Packages
@ -63,12 +64,23 @@ Differently from most editors, Carta includes neither ProseMirror nor CodeMirror
| [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
> [!WARNING]
> 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).
> Checkout the documentation for an example.
## Installation
@ -88,11 +100,9 @@ npm i @cartamd/plugin-name
```svelte
<script lang="ts">
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
// Component default theme
import 'carta-md/default.css';
// Markdown input theme (Speed Highlight)
import 'carta-md/light.css';
const carta = new Carta({
// Remember to use a sanitizer to prevent XSS attacks
@ -100,13 +110,14 @@ npm i @cartamd/plugin-name
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
<style>
/* Or in global stylesheet */
/* Set your custom monospace font */
:global(.carta-font-code) {
font-family: '...', monospace;
font-size: 1.1rem;
}
</style>
```
@ -126,6 +137,7 @@ For the full documentation, examples, guides and more checkout the [website](htt
- [Slash](https://beartocode.github.io/carta/plugins/slash)
- [TikZ](https://beartocode.github.io/carta/plugins/tikz)
- [Attachment](https://beartocode.github.io/carta/plugins/attachment)
- [Anchor](https://beartocode.github.io/carta/plugins/anchor)
- API:
- [Utilities](https://beartocode.github.io/carta/api/utilities)
- [Core](https://beartocode.github.io/carta/api/core)
@ -146,3 +158,12 @@ 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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
<script lang="ts">
import * as Command from '$lib/components/ui/command';
import type { Document } from 'flexsearch';
import { Enter, MagnifyingGlass } from 'radix-icons-svelte';
import {
enrichResult,
initializeSearch,
@ -54,8 +53,8 @@
<svelte:window on:keydown={handleKeydown} />
<button on:click={() => (open = !open)} class="mr-2 block md:hidden">
<MagnifyingGlass class="h-7 w-7 text-neutral-200" />
<button on:click={() => (open = !open)} class="mr-2 block aspect-square md:hidden">
<iconify-icon icon="ion:search" class="text-2xl text-neutral-200"></iconify-icon>
</button>
<button
@ -63,7 +62,7 @@
on:click={() => (open = !open)}
>
<div class="inline-flex items-center space-x-2">
<MagnifyingGlass class="h-5 w-5 text-neutral-500" />
<iconify-icon icon="ion:search" class="text-xl text-neutral-500"></iconify-icon>
<span class="text-neutral-500">Search...</span>
</div>
<kbd
@ -102,7 +101,7 @@
{/if}
<div class="absolute right-2 top-1/2 hidden -translate-y-1/2 group-aria-selected:block">
<Enter class="h-5 w-5 text-neutral-400" />
<iconify-icon icon="mi:enter" class="text-xl text-neutral-400"></iconify-icon>
</div>
</Command.Item>
{/each}

View file

@ -1,16 +1,4 @@
<script lang="ts">
import {
Code,
CodesandboxLogo,
Cube,
Dashboard,
Download,
Face,
FontFamily,
Slash,
File,
FontStyle
} from 'radix-icons-svelte';
import SidebarLink from './SidebarLink.svelte';
export { className as class };
@ -23,66 +11,90 @@
<!-- Introduction -->
<SidebarLink href="/introduction">
<Dashboard class="h-5 w-5" />
<iconify-icon icon="radix-icons:dashboard" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Introduction</span>
</SidebarLink>
<!-- Examples -->
<SidebarLink href="/examples">
<CodesandboxLogo class="h-5 w-5" />
<iconify-icon icon="ph:codesandbox-logo" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Examples</span>
</SidebarLink>
<!-- Getting Started -->
<SidebarLink href="/getting-started">
<Download class="h-5 w-5" />
<iconify-icon icon="ic:round-download" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Getting Started</span>
</SidebarLink>
<!-- Editing Styles -->
<SidebarLink href="/editing-styles">
<FontStyle class="h-5 w-5" />
<iconify-icon icon="lucide:palette" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Editing Styles</span>
</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>
<!-- Math -->
<SidebarLink href="/plugins/math">
<FontFamily class="h-5 w-5" />
<iconify-icon icon="tabler:math" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Math</span>
</SidebarLink>
<!-- Code -->
<SidebarLink href="/plugins/code">
<Code class="h-5 w-5" />
<iconify-icon icon="fluent:code-16-filled" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Code</span>
</SidebarLink>
<!-- Emoji -->
<SidebarLink href="/plugins/emoji">
<Face class="h-5 w-5" />
<iconify-icon icon="mingcute:emoji-line" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Emoji</span>
</SidebarLink>
<!-- Slash -->
<SidebarLink href="/plugins/slash">
<Slash class="h-5 w-5" />
<iconify-icon icon="tabler:slash" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Slash</span>
</SidebarLink>
<!-- TikZ -->
<SidebarLink href="/plugins/tikz">
<Cube class="h-5 w-5" />
<iconify-icon icon="mdi:draw-pen" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">TikZ</span>
</SidebarLink>
<!-- Attachment -->
<SidebarLink href="/plugins/attachment">
<File class="h-5 w-5" />
<iconify-icon icon="tdesign:attach" class="text-xl"></iconify-icon>
<span class="text-[0.95rem]">Attachment</span>
</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>
<!-- Utilities -->

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,6 +34,7 @@
.carta-font-code {
font-family: 'Fira Code', monospace;
caret-color: white;
font-size: 1.1rem;
}
.carta-toolbar {
@ -64,9 +65,34 @@
.carta-toolbar-right {
justify-content: flex-start;
}
}
[class*='shj-lang-'] {
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;
}
}
}
@ -75,3 +101,8 @@
padding: 1rem;
}
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}

View file

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

View file

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

View file

@ -40,3 +40,52 @@ Svelte action that allows you to bind a specific element to the caret position.
<!-- ... -->
</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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,70 @@
---
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
<script>
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
import { attachment } from '@cartamd/plugin-attachment';
const carta = new Carta({
@ -49,7 +49,7 @@ import '@cartamd/plugin-attachment/default.css';
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
```
</Code>

View file

@ -7,8 +7,7 @@ title: Code
import Code from '$lib/components/code/Code.svelte';
</script>
This plugin adds support for code blocks **syntax highlighting**.
This is done using [Speed-highlight JS](https://github.com/speed-highlight/core), which supports dynamic imports. This way, languages definitions are only imported at the moment of need.
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
## Installation
@ -36,34 +35,34 @@ import '@cartamd/plugin-code/default.css';
### 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.
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.
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.
<Code>
```ts
code({
customHighlight: {
highlighter: (code, lang) => myCustomHighlighter(code, lang),
langPrefix: 'my-highlighter-'
}
const carta = new Carta({
// ...
extensions: [
code({
theme: 'ayu-light'
})
]
});
```
</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
<Code>
```svelte
<script>
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
import { code } from '@cartamd/plugin-code';
const carta = new Carta({
@ -71,46 +70,11 @@ code({
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
```
</Code>
## Options
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;
};
}
```
The options you can pass to `code()` extend the ones provided by [Shiki](https://shiki.matsu.io/guide/transformers).

View file

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

View file

@ -5,7 +5,7 @@ title: Math
<script>
import Code from '$lib/components/code/Code.svelte';
import { CartaViewer, Carta } from 'carta-md';
import { Markdown, Carta } from 'carta-md';
import { math } from '@cartamd/plugin-math';
import 'katex/dist/katex.css';
@ -78,7 +78,7 @@ or by using a content delivery network:
```svelte
<script>
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
import { math } from '@cartamd/plugin-math';
const carta = new Carta({
@ -86,7 +86,7 @@ or by using a content delivery network:
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
```
</Code>
@ -103,7 +103,7 @@ Pythagorean theorem: $a^2+b^2=c^2$
</Code>
<CartaViewer {carta} value={inline} />
<Markdown {carta} value={inline} />
<br>
@ -119,7 +119,7 @@ $$
</Code>
<CartaViewer {carta} value={block} />
<Markdown {carta} value={block} />
## Options
@ -131,7 +131,6 @@ interface MathExtensionOptions {
* Options for inline katex, eg: $a^2+b^2=c^2$
*/
inline?: {
katexOptions?: KatexOptions;
/**
* @default control+m
*/
@ -144,23 +143,18 @@ interface MathExtensionOptions {
* $$
*/
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
*/
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
<script>
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
import { slash } from '@cartamd/plugin-slash';
const carta = new Carta({
@ -47,7 +47,7 @@ import '@cartamd/plugin-slash/default.css';
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
```
</Code>

View file

@ -20,7 +20,7 @@ npm i @cartamd/plugin-tikz
## Important Notes
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">`.
## Setup
@ -29,7 +29,7 @@ npm i @cartamd/plugin-tikz
```svelte
<script>
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
import { tikz } from '@cartamd/plugin-tikz';
import '@cartamd/plugin-tikz/fonts.css';
@ -38,7 +38,7 @@ npm i @cartamd/plugin-tikz
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
```
</Code>

View file

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

View file

@ -4,8 +4,8 @@
import Navbar from '$lib/components/navbar/Navbar.svelte';
import Sidebar from '$lib/components/sidebar/Sidebar.svelte';
import Footer from '$lib/components/footer/Footer.svelte';
import '../app.postcss';
import { base } from '$app/paths';
import '../app.postcss';
</script>
<Navbar />
@ -24,6 +24,6 @@
<slot />
<Footer />
</main>
<HeaderTracker class="sticky top-24 hidden w-[20rem] xl:block" />
<HeaderTracker class="sticky top-24 hidden w-[15rem] flex-shrink-0 xl:block" />
</div>
</div>

View file

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

View file

@ -2,10 +2,10 @@
import type { PageData } from './$types';
import { onMount, type SvelteComponent } from 'svelte';
import { page } from '$app/stores';
import { base } from '$app/paths';
import '$lib/styles/markdown.scss';
import '$lib/styles/coldark.scss';
import { base } from '$app/paths';
export let data: PageData;

View file

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

View file

@ -53,13 +53,5 @@
"@semantic-release/npm",
"@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,31 +1,25 @@
<div align="right">
<a href="https://www.npmjs.com/package/carta-md">
<img src="https://img.shields.io/npm/v/carta-md?color=0384fc&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
</a>
<a href="https://bundlephobia.com/package/carta-md">
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=0384fc&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
</a>
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/carta-md?color=0384fc&labelColor=171d27&logo=git&logoColor=white" alt="license">
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&labelColor=171d27&logo=git&logoColor=white" alt="license">
</a>
<a href="http://beartocode.github.io/carta/">
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=0384fc&logoColor=ffffff&labelColor=171d27" alt="docs">
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs">
</a>
</div>
<div align="center">
<a href="https://beartocode.github.io/carta/">
<img alt="banner" src="https://i.postimg.cc/1XPm8FSD/Frame-8.png">
</a>
</div>
[![Carta.png](https://i.postimg.cc/nV6DMXKM/Carta.png)](https://beartocode.github.io/carta/)
<br>
<div align="center"><strong>Carta</strong></div>
<div align="center">Swiftly edit and render Markdown, with no overhead.</div>
<h1 align="center"><strong>Carta</strong></h1>
<div align="center">Modern, lightweight, powerful Markdown Editor.</div>
<br />
<div align="center">
<a href="https://beartocode.github.io/carta/">Documentation</a>
<a href="https://beartocode.github.io/carta/">📚 Documentation</a>
<span> · </span>
<a href="https://github.com/BearToCode/carta">GitHub</a>
</div>
@ -34,38 +28,81 @@
# Introduction
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer, based on [Marked](https://github.com/markedjs/marked). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
> **NOTE**:
> Carta has recently been updated to `v4`, which features numerous major changes.
>
> Follow the [Migration Guide](http://beartocode.github.io/carta/migration) to update your project.
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer. It is powered by [unified](https://github.com/unifiedjs/unified), [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
Differently from most editors, Carta does not include a code editor, but it is _just_ a textarea with syntax highlighting, shortcuts and more.
## Features
- Keyboard **shortcuts** (extensible);
- Toolbar (extensible);
- Markdown syntax highlighting;
- Scroll sync;
- **SSR** compatible;
- **Katex** support (plugin);
- **Slash** commands (plugin);
- **Emojis**, with included search (plugin);
- **Tikz** support(plugin);
- **Attachment** support(plugin);
- Code blocks **syntax highlighting** (plugin).
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
- 🛠️ Toolbar (extensible);
- ⌨️ Keyboard **shortcuts** (extensible);
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark;
- 🔀 Scroll sync;
- ✅ Accessibility friendly;
- 🖥️ **SSR** compatible;
- ⚗️ **KaTeX** support (plugin);
- 🔨 **Slash** commands (plugin);
- 😄 **Emojis**, with included search (plugin);
- ✏️ **TikZ** support (plugin);
- 📂 **Attachment** support (plugin);
- ⚓ **Anchor** links in headings (plugin);
- 🌈 Code blocks **syntax highlighting** (plugin).
## Packages
| Package | Status | Docs |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [carta-md](https://www.npmjs.com/package/carta-md) | ![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
> **Warning**
> **WARNING**
> 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).
> Checkout the documentation for an example.
## Installation
Core package:
```
npm i carta-md
```
Plugins:
```
npm i @cartamd/plugin-name
```
## Basic configuration
```svelte
<script lang="ts">
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
// Component default theme
import 'carta-md/default.css';
// Markdown input theme (Speed Highlight)
import 'carta-md/light.css';
const carta = new Carta({
// Remember to use a sanitizer to prevent XSS attacks
@ -73,13 +110,14 @@ Differently from most editors, Carta includes neither ProseMirror nor CodeMirror
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
<style>
/* Or in global stylesheet */
/* Set your custom monospace font */
:global(.carta-font-code) {
font-family: '...', monospace;
font-size: 1.1rem;
}
</style>
```
@ -99,7 +137,33 @@ For the full documentation, examples, guides and more checkout the [website](htt
- [Slash](https://beartocode.github.io/carta/plugins/slash)
- [TikZ](https://beartocode.github.io/carta/plugins/tikz)
- [Attachment](https://beartocode.github.io/carta/plugins/attachment)
- [Anchor](https://beartocode.github.io/carta/plugins/anchor)
- API:
- [Utilities](https://beartocode.github.io/carta/api/utilities)
- [Core](https://beartocode.github.io/carta/api/core)
- [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,10 +14,7 @@
"svelte": "./dist/index.js",
"import": "./dist/index.js"
},
"./default.css": "./dist/default.css",
"./default-theme.css": "./dist/default.css",
"./light.css": "./dist/light.css",
"./dark.css": "./dist/dark.css"
"./default.css": "./dist/default.css"
},
"version": "3.0.0",
"scripts": {
@ -27,18 +24,23 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^1.0.0-next.90",
"@sveltejs/kit": "^1.0.0-next.587",
"@sveltejs/package": "^2.0.2",
"svelte-check": "^3.2.0",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/package": "^2.3.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"svelte-check": "^3.6.7",
"tslib": "^2.4.1",
"typescript": "^5.1.6",
"typescript-plugin-css-modules": "^5.0.1"
"vite": "^5.1.6"
},
"type": "module",
"dependencies": {
"@speed-highlight/core": "1.2.2",
"marked": "^9.1.5"
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"shiki": "^1.4.0",
"unified": "^11.0.4"
},
"peerDependencies": {
"svelte": "^3.54.0 || ^4.0.0"

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Carta } from './';
import { loadNestedLanguages, type Carta } from '.';
export let carta: Carta;
export let value: string;
@ -12,7 +12,12 @@
let rendered = carta.renderSSR(value);
onMount(async () => {
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);
mounted = true;

View file

@ -1,12 +1,12 @@
<script lang="ts">
import type { Carta } from './internal/carta';
import { onMount } from 'svelte';
import CartaRenderer from './internal/components/CartaRenderer.svelte';
import MarkdownInput from './internal/components/MarkdownInput.svelte';
import Renderer from './internal/components/Renderer.svelte';
import Input from './internal/components/Input.svelte';
import { debounce } from './internal/utils';
import type { TextAreaProps } from './internal/textarea-props';
import { DefaultCartaLabels, type CartaLabels } from './internal/labels';
import { handleArrowKeysNavigation } from './internal/accessibility';
import { defaultLabels, type Labels } from './internal/labels';
import Toolbar from './internal/components/Toolbar.svelte';
export let carta: Carta;
export let theme = 'default';
@ -17,10 +17,10 @@
export let placeholder = '';
export let textarea: TextAreaProps = {};
let userLabels: Partial<CartaLabels> = {};
let userLabels: Partial<Labels> = {};
export { userLabels as labels };
const labels: CartaLabels = {
...DefaultCartaLabels,
const labels: Labels = {
...defaultLabels,
...userLabels
};
@ -28,13 +28,11 @@
let selectedTab: 'write' | 'preview' = 'write';
let windowMode: 'tabs' | 'split';
let mounted = false;
let hideIcons = false;
let resizeInput: () => void;
onMount(() => (mounted = true));
$: {
windowMode = mode === 'auto' ? (width > 768 ? 'split' : 'tabs') : mode;
hideIcons = width < 576;
}
$: {
@ -100,55 +98,13 @@
<div bind:this={editorElem} bind:clientWidth={width} class="carta-editor carta-theme__{theme}">
{#if !disableToolbar}
<div class="carta-toolbar" role="toolbar">
<div class="carta-toolbar-left">
{#if windowMode == 'tabs'}
<button
type="button"
tabindex={0}
class={selectedTab === 'write' ? 'carta-active' : ''}
on:click={() => (selectedTab = 'write')}
on:keydown={handleArrowKeysNavigation}
>
{labels.writeTab}
</button>
<button
type="button"
tabindex={-1}
class={selectedTab === 'preview' ? 'carta-active' : ''}
on:click={() => (selectedTab = 'preview')}
on:keydown={handleArrowKeysNavigation}
>
{labels.previewTab}
</button>
{/if}
</div>
<div class="carta-toolbar-right">
{#if !hideIcons}
{#each carta.icons as icon, index}
<button
class="carta-icon"
tabindex={index == 0 ? 0 : -1}
aria-label={icon.label}
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}
</div>
</div>
<Toolbar {carta} {labels} mode={windowMode} bind:tab={selectedTab} />
{/if}
<div class="carta-wrapper">
<div class="carta-container mode-{windowMode}">
{#if windowMode == 'split' || selectedTab == 'write'}
<MarkdownInput
<Input
{carta}
{placeholder}
{handleScroll}
@ -165,10 +121,10 @@
<svelte:component this={component} {carta} {...props} />
{/each}
{/if}
</MarkdownInput>
</Input>
{/if}
{#if windowMode == 'split' || selectedTab == 'preview'}
<CartaRenderer {carta} {handleScroll} bind:value bind:elem={rendererElem}>
<Renderer {carta} {handleScroll} bind:value bind:elem={rendererElem}>
<!-- Renderer extensions components -->
{#if mounted}
{#each carta.components.filter(({ parent }) => [parent]
@ -177,7 +133,7 @@
<svelte:component this={component} {carta} {...props} />
{/each}
{/if}
</CartaRenderer>
</Renderer>
{/if}
</div>
</div>
@ -200,17 +156,11 @@
flex-direction: column;
}
.carta-toolbar {
height: 2rem;
display: flex;
flex-shrink: 0;
}
:global(.mode-split > *) {
:global(.carta-container.mode-split > *) {
width: 50%;
}
:global(.mode-tabs > *) {
:global(.carta-container.mode-tabs > *) {
width: 100%;
}
@ -218,27 +168,4 @@
display: flex;
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>

View file

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

View file

@ -3,6 +3,15 @@
--selection-color: #b5f0ff3d;
--focus-outline: #76bbf3;
--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 {
@ -29,10 +38,14 @@
/* Text settings */
.carta-theme__default .carta-input {
caret-color: #4d4d4c;
caret-color: var(--caret-color);
font-size: 0.95rem;
}
.carta-theme__default .carta-input ::placeholder {
color: var(--text-color);
}
/* Splitter */
.carta-theme__default .mode-split.carta-container::after {
content: '';
@ -62,6 +75,10 @@
align-items: flex-end;
}
.carta-theme__default button {
color: var(--text-color);
}
/* Markdown input and renderer */
.carta-theme__default .carta-input,
.carta-theme__default .carta-renderer {
@ -70,12 +87,22 @@
}
/* Icons */
.carta-theme__default .carta-icon {
.carta-theme__default .carta-icon,
.carta-theme__default .carta-icon-full {
border: 0;
background: transparent;
}
.carta-theme__default .carta-icon:hover {
.carta-theme__default .carta-icon-full {
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);
}
@ -83,6 +110,21 @@
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 */
.carta-theme__default .carta-toolbar-left button {
background: none;

View file

@ -1,7 +1,7 @@
export { default as CartaEditor } from '$lib/CartaEditor.svelte';
export { default as CartaViewer } from '$lib/CartaViewer.svelte';
export type { CartaInput, TextSelection } from '$lib/internal/input';
export type { CartaIcon } from '$lib/internal/icons';
export { default as MarkdownEditor } from '$lib/MarkdownEditor.svelte';
export { default as Markdown } from '$lib/Markdown.svelte';
export type { InputEnhancer, TextSelection } from '$lib/internal/input';
export type { Icon } from '$lib/internal/icons';
export type { KeyboardShortcut } from '$lib/internal/shortcuts';
export type { Prefix } from '$lib/internal/prefixes';
export * from '$lib/internal/carta';
@ -9,4 +9,7 @@ export * from '$lib/internal/highlight';
export * from '$lib/internal/textarea-props';
export * from '$lib/internal/labels';
export * from './default.css?inline';
export * from './light.css?inline';
// Legacy
export { default as CartaEditor } from '$lib/MarkdownEditor.svelte';
export { default as CartaViewer } from '$lib/Markdown.svelte';

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -2,6 +2,8 @@
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 = '';
@ -11,7 +13,7 @@
export let props: TextAreaProps = {};
let textarea: HTMLTextAreaElement;
let highlighElem: HTMLPreElement;
let highlighElem: HTMLDivElement;
let highlighted = value;
let mounted = false;
@ -36,10 +38,44 @@
});
};
const highlight = async (text: string) => (highlighted = (await carta.highlight(text)) as string);
$: highlight(value).then(resize);
const highlight = async (text: string) => {
const highlighter = await carta.highlighter();
let html: string;
onMount(() => (mounted = true));
if (isSingleTheme(highlighter.theme)) {
// Single theme
html = highlighter.codeToHtml(text, {
lang: highlighter.lang,
theme: highlighter.theme
});
} else {
// Dual theme
html = highlighter.codeToHtml(text, {
lang: highlighter.lang,
themes: highlighter.theme
});
}
if (carta.sanitizer) {
highlighted = carta.sanitizer(html);
} else {
highlighted = html;
}
};
const highlightNestedLanguages = debounce(async (text: string) => {
const highlighter = await carta.highlighter();
const { updated } = await loadNestedLanguages(highlighter, text);
if (updated) highlight(text);
}, 300);
$: highlight(value).then(resize);
$: highlightNestedLanguages(value);
onMount(() => {
mounted = true;
requestAnimationFrame(resize);
});
onMount(setInput);
</script>
@ -56,11 +92,14 @@
bind:this={elem}
>
<div class="carta-input-wrapper">
<pre
class="shj-lang-md carta-font-code"
bind:this={highlighElem}
<div
class="carta-highlight carta-font-code"
tabindex="-1"
aria-hidden="true"><!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}</pre>
aria-hidden="true"
bind:this={highlighElem}
>
<!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}
</div>
<textarea
name="md"
@ -110,12 +149,11 @@
color: transparent;
background: transparent;
font-size: inherit;
outline: none;
tab-size: 4;
}
pre {
.carta-highlight {
position: absolute;
left: 0;
right: 0;
@ -133,6 +171,21 @@
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;

View file

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

View file

@ -0,0 +1,197 @@
<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

@ -0,0 +1,6 @@
<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>

After

Width:  |  Height:  |  Size: 249 B

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import type { ComponentType } from 'svelte';
import type { CartaInput } from './input';
import type { InputEnhancer } from './input';
import HeadingIcon from './components/icons/HeadingIcon.svelte';
import ItalicIcon from './components/icons/ItalicIcon.svelte';
import BoldIcon from './components/icons/BoldIcon.svelte';
@ -14,16 +14,16 @@ import StrikethroughIcon from './components/icons/StrikethroughIcon.svelte';
/**
* Editor toolbar icon information.
*/
export interface CartaIcon {
export interface Icon {
/**
* The icon's unique identifier.
*/
id: string;
/**
* Callback function to execute when the icon is clicked.
* @param input CartaInput instance
* @param input InputEnhancer instance
*/
action: (input: CartaInput) => void;
action: (input: InputEnhancer) => void;
/**
* The icon's component.
*/
@ -100,6 +100,6 @@ export const defaultIcons = [
component: ListTaskIcon,
label: 'Task list'
}
] as const satisfies readonly CartaIcon[];
] as const satisfies readonly Icon[];
export type DefaultIconId = (typeof defaultIcons)[number]['id'];
export type DefaultIconId = (typeof defaultIcons)[number]['id'] | 'menu';

View file

@ -1,7 +1,7 @@
import type { CartaListener } from './carta';
import type { Listener } from './carta';
import type { Prefix } from './prefixes';
import type { KeyboardShortcut } from './shortcuts';
import { CartaHistory, type CartaHistoryOptions } from './history';
import { TextAreaHistory as TextAreaHistory, type TextAreaHistoryOptions } from './history';
import { areEqualSets } from './utils';
/**
@ -21,17 +21,17 @@ export interface InputSettings {
readonly shortcuts: KeyboardShortcut[];
readonly prefixes: Prefix[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly listeners: CartaListener<any>[];
readonly historyOpts?: Partial<CartaHistoryOptions>;
readonly listeners: Listener<any>[];
readonly historyOpts?: Partial<TextAreaHistoryOptions>;
}
export class CartaInput {
export class InputEnhancer {
private pressedKeys: Set<string>;
private escapePressed = false;
// Used to detect keys that actually changed the textarea value
private onKeyDownValue: string | undefined;
public history: CartaHistory;
public history: TextAreaHistory;
public readonly events = new EventTarget();
constructor(
@ -54,7 +54,7 @@ export class CartaInput {
textarea.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.history = new CartaHistory(settings.historyOpts);
this.history = new TextAreaHistory(settings.historyOpts);
// Save initial value
this.history.saveState(this.textarea.value, this.textarea.selectionStart);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,12 @@
// 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.
* @param cb Callback function.

View file

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

View file

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

View file

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

View file

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

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

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

View file

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

11
packages/plugin-anchor/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,36 @@
# 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

@ -0,0 +1,69 @@
{
"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"
]
}

12
packages/plugin-anchor/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
// 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

@ -0,0 +1,12 @@
<!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

@ -0,0 +1,32 @@
.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

@ -0,0 +1,32 @@
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

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

After

Width:  |  Height:  |  Size: 575 B

View file

@ -0,0 +1,94 @@
<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.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,18 @@
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

@ -0,0 +1,16 @@
{
"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

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

View file

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

View file

@ -1,4 +1,4 @@
import type { Carta, CartaExtension, CartaListener } from 'carta-md';
import type { Carta, Plugin, Listener } from 'carta-md';
import { get, writable, type Writable } from 'svelte/store';
import type { SvelteComponent } from 'svelte';
import DropOverlay from './DropOverlay.svelte';
@ -40,7 +40,7 @@ const ImageMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml']
/**
* Carta attachment plugin.
*/
export const attachment = (options: AttachmentExtensionOptions): CartaExtension => {
export const attachment = (options: AttachmentExtensionOptions): Plugin => {
let carta: Carta | undefined;
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
@ -102,15 +102,31 @@ export const attachment = (options: AttachmentExtensionOptions): CartaExtension
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 {
onLoad: ({ carta: c }) => {
carta = c;
},
listeners: [
['drop', handleDrop, false] satisfies CartaListener<'drop'>,
['dragenter', () => draggingOverTextArea.set(true)] satisfies CartaListener<'dragenter'>,
['dragleave', () => draggingOverTextArea.set(false)] satisfies CartaListener<'dragleave'>,
['dragover', (e) => e.preventDefault()] satisfies CartaListener<'dragover'>
['drop', handleDrop, false] satisfies Listener<'drop'>,
['dragenter', () => draggingOverTextArea.set(true)] satisfies Listener<'dragenter'>,
['dragleave', () => draggingOverTextArea.set(false)] satisfies Listener<'dragleave'>,
['dragover', (e) => e.preventDefault()] satisfies Listener<'dragover'>,
['paste', handlePaste, false] satisfies Listener<'paste'>
],
components: [
{

View file

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

View file

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

View file

@ -1,13 +1,11 @@
# Carta Code Plugin
This plugin adds support for code blocks **syntax highlighting**. Install it using:
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
```
npm i @cartamd/plugin-code
```
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
### Styles
@ -20,28 +18,31 @@ import '@cartamd/plugin-code/default.css';
### 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.
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.
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options. Remember to also have it loaded into the highlighter, by specifying it in `shikiOptions`.
```ts
code({
customHighlight: {
highlighter: (code, lang) => myCustomHighlighter(code, lang),
langPrefix: 'my-highlighter-'
const carta = new Carta({
// ...
extensions: [
code({
theme: 'ayu-light'
})
],
shikiOptions: {
themes: ['ayu-light']
}
});
```
### Using a custom highlighter
It is no longer possible to specify a custom highlighter in this plugin. However, there are many different [Remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) that provide syntax highlighting.
### Extension
```svelte
<script lang="ts">
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
import { code } from '@cartamd/plugin-code';
const carta = new Carta({
@ -49,7 +50,7 @@ code({
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
```
## Documentation

View file

@ -16,21 +16,22 @@
"build": "tsc && tscp"
},
"devDependencies": {
"@shikijs/rehype": "^1.4.0",
"@types/node": "^18.16.3",
"carta-md": "workspace:*",
"typescript": "^5.0.4",
"typescript-cp": "^0.1.8",
"marked": "^9.1.5"
"typescript-cp": "^0.1.8"
},
"peerDependencies": {
"carta-md": "^3.0.0"
"carta-md": "^4.0.0"
},
"files": [
"dist"
],
"version": "3.0.0",
"version": "4.0.0",
"dependencies": {
"marked-highlight": "^2.0.6"
"@shikijs/rehype": "^1.4.0",
"unified": "^11.0.4"
},
"keywords": [
"carta",

View file

@ -1,70 +1,52 @@
import type { CartaExtension, HighlightFunctions } from 'carta-md';
import { markedHighlight } from 'marked-highlight';
import type { DualTheme, Theme, Plugin } from 'carta-md';
import type { RehypeShikiOptions } from '@shikijs/rehype';
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
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;
export type CodeExtensionOptions = Omit<RehypeShikiOptions, 'theme' | 'themes'> & {
theme?: Theme | DualTheme;
};
/**
* Options for custom syntax highlighting.
// FIXME: find a better solution then copy-pasting these functions in next version.
// However, when importing from carta-md, this causes a MODULE_NOT_FOUND error
// for some reason.
/**
* Checks if a theme is a dual theme.
* @param theme The theme to check.
* @returns Whether the theme is a dual theme.
*/
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.
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.
*/
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;
};
}
let shj: HighlightFunctions;
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme);
/**
* Carta code highlighting plugin. Themes available on [GitHub](https://github.com/speed-highlight/core/tree/main/dist/themes).
*/
export const code = (options?: CodeExtensionOptions): CartaExtension => {
export const code = (options?: CodeExtensionOptions): Plugin => {
return {
onLoad: ({ highlight }) => (shj = highlight),
markedExtensions: [
markedHighlight({
langPrefix: options?.customHighlight?.langPrefix ?? 'shj-lang-',
async: true,
async highlight(code, lang) {
if (options?.customHighlight) {
return await options.customHighlight.highlighter(code, lang);
transformers: [
{
execution: 'async',
type: 'rehype',
async transform({ processor, carta }) {
let theme = options?.theme;
const highlighter = await carta.highlighter();
if (!theme) {
theme = highlighter.theme; // Use the theme specified in the highlighter
}
const { highlight, highlightAutodetect } = shj;
lang ||= options?.defaultLanguage ?? '';
let highlighted: string | null = null;
if (lang) highlighted = await highlight(code, lang, !(options?.lineNumbering ?? false));
if (highlighted) return highlighted;
if (options?.autoDetect ?? true)
return await highlightAutodetect(code, !(options?.lineNumbering ?? false));
return (await highlight(code, 'plain', !(options?.lineNumbering ?? false))) as string;
if (isSingleTheme(theme)) {
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, theme });
} else {
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, themes: theme });
}
}
}
})
]
};
};

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "@cartamd/plugin-emoji",
"version": "3.0.0",
"version": "4.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@ -33,26 +33,27 @@
],
"dependencies": {
"bezier-easing": "^2.1.0",
"node-emoji": "^1.11.0"
"node-emoji": "^1.11.0",
"remark-gemoji": "^8.0.0"
},
"peerDependencies": {
"carta-md": "^3.1.0",
"marked": "^9.1.5",
"carta-md": "^4.0.0",
"svelte": "^3.54.0 || ^4.0.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"@sveltejs/package": "^2.0.0",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/package": "^2.3.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/node-emoji": "^1.8.2",
"carta-md": "workspace:*",
"marked": "^9.1.5",
"publint": "^0.1.9",
"svelte": "^3.54.0 || ^4.0.0",
"svelte-check": "^3.0.1",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.3.9",
"marked": "^9.1.5"
"vite": "^5.1.6"
},
"svelte": "./dist/index.js",
"keywords": [

View file

@ -1,7 +1,6 @@
import type { CartaExtension, CartaExtensionComponent } from 'carta-md';
import type { TokenizerAndRendererExtension } from 'marked';
import type { Plugin, ExtensionComponent, GrammarRule, HighlightingRule } from 'carta-md';
import remarkGemoji from 'remark-gemoji';
import { fade, scale, type TransitionConfig } from 'svelte/transition';
import nodeEmoji from 'node-emoji';
import Emoji from './Emoji.svelte';
import BezierEasing from 'bezier-easing';
export * from './default.css?inline';
@ -25,7 +24,7 @@ interface ComponentProps {
/**
* Carta emoji plugin. Adds support to render emojis as well as an emojis snippet.
*/
export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
export const emoji = (options?: EmojiExtensionOptions): Plugin => {
const inTransition =
options?.inTransition ??
((node: Element) =>
@ -40,7 +39,7 @@ export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
duration: 100
}));
const emojiComponent: CartaExtensionComponent<ComponentProps> = {
const emojiComponent: ExtensionComponent<ComponentProps> = {
component: Emoji,
parent: 'input',
props: {
@ -49,39 +48,42 @@ export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
}
};
const grammar = {
name: 'emoji',
type: 'inline',
definition: {
match: ':[a-zA-Z_]+:',
name: 'markup.emoji.markdown'
}
} satisfies GrammarRule;
const highlighting = {
light: {
scope: 'markup.emoji',
settings: {
foreground: '#3bf'
}
},
dark: {
scope: 'markup.emoji',
settings: {
foreground: '#4dacfa'
}
}
} satisfies HighlightingRule;
return {
markedExtensions: [
transformers: [
{
extensions: [emojiTokenizerAndRenderer()]
execution: 'sync',
type: 'remark',
transform({ processor }) {
processor.use(remarkGemoji);
}
}
],
components: [emojiComponent],
highlightRules: [
{
type: 'oper',
match: /:[a-z0-9_]+:/g
}
]
grammarRules: [grammar],
highlightingRules: [highlighting]
};
};
function emojiTokenizerAndRenderer(): TokenizerAndRendererExtension {
return {
name: 'emoji',
level: 'inline',
start: (src) => src.indexOf(':'),
tokenizer: (src) => {
const match = src.match(/^:.*?:/)?.at(0);
if (!match) return undefined;
const emoji = nodeEmoji.find(match)?.emoji;
if (emoji) {
return {
type: 'emoji',
raw: match,
emoji
};
}
},
renderer: (token) => token.emoji
};
}

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