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

|
||||||
|
|
||||||
|
> Markdown uses email-style
|
||||||
|
> characters for blockquoting.
|
||||||
|
> Multiple paragraphs need to be prepended individually.
|
||||||
|
|
||||||
|
| Item | Price | In stock |
|
||||||
|
| ------------ | -------- | -------- |
|
||||||
|
| Juicy Apples | 1.99 | _7_ |
|
||||||
|
| Bananas | **1.89** | 5234 |
|
BIN
packages/carta-md/static/pic.jpg
Normal file
BIN
packages/carta-md/static/pic.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
|
@ -1,5 +1,5 @@
|
||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
|
|
|
@ -13,12 +13,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"ignoreDeprecations": "5.0",
|
"ignoreDeprecations": "5.0",
|
||||||
"plugins": [],
|
"plugins": []
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"$lib": ["src/lib"],
|
|
||||||
"$lib/*": ["src/lib/*"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["./src"]
|
"include": ["./src"]
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
|
|
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-anchor/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script>
|
<script>
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { anchor } from '@cartamd/plugin-anchor';
|
import { anchor } from '@cartamd/plugin-anchor';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -28,7 +28,7 @@ import '@cartamd/plugin-anchor/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -32,25 +32,26 @@
|
||||||
"!dist/**/*.spec.*"
|
"!dist/**/*.spec.*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"slugify": "^1.6.6"
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
"rehype-slug": "^6.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.1.0",
|
"carta-md": "^4.0.0",
|
||||||
"marked": "^9.1.5",
|
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "^3.1.1",
|
||||||
"@sveltejs/kit": "^1.5.0",
|
"@sveltejs/kit": "^2.5.4",
|
||||||
"@sveltejs/package": "^2.0.0",
|
"@sveltejs/package": "^2.3.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
|
"marked": "^9.1.5",
|
||||||
"publint": "^0.1.9",
|
"publint": "^0.1.9",
|
||||||
"svelte": "^3.54.0 || ^4.0.0",
|
"svelte": "^3.54.0 || ^4.0.0",
|
||||||
"svelte-check": "^3.0.1",
|
"svelte-check": "^3.6.7",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^4.3.9",
|
"vite": "^5.1.6"
|
||||||
"marked": "^9.1.5"
|
|
||||||
},
|
},
|
||||||
"svelte": "./dist/index.js",
|
"svelte": "./dist/index.js",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -1,13 +1,32 @@
|
||||||
.carta-renderer .anchor-link {
|
.carta-viewer h1,
|
||||||
visibility: hidden;
|
.carta-viewer h2,
|
||||||
opacity: 0.6;
|
.carta-viewer h3,
|
||||||
|
.carta-viewer h4,
|
||||||
|
.carta-viewer h5,
|
||||||
|
.carta-viewer h6 {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carta-renderer h1:hover .anchor-link,
|
.carta-viewer h1 .icon.icon-link,
|
||||||
.carta-renderer h2:hover .anchor-link,
|
.carta-viewer h2 .icon.icon-link,
|
||||||
.carta-renderer h3:hover .anchor-link,
|
.carta-viewer h3 .icon.icon-link,
|
||||||
.carta-renderer h4:hover .anchor-link,
|
.carta-viewer h4 .icon.icon-link,
|
||||||
.carta-renderer h5:hover .anchor-link,
|
.carta-viewer h5 .icon.icon-link,
|
||||||
.carta-renderer h6:hover .anchor-link {
|
.carta-viewer h6 .icon.icon-link {
|
||||||
visibility: visible;
|
opacity: 0;
|
||||||
|
content: url('./link.svg');
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
top: 50%;
|
||||||
|
padding-right: 4px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carta-viewer h1:hover .icon-link,
|
||||||
|
.carta-viewer h2:hover .icon-link,
|
||||||
|
.carta-viewer h3:hover .icon-link,
|
||||||
|
.carta-viewer h4:hover .icon-link,
|
||||||
|
.carta-viewer h5:hover .icon-link,
|
||||||
|
.carta-viewer h6:hover .icon-link {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,30 @@
|
||||||
import type { CartaExtension } from 'carta-md';
|
import rehypeSlug, { type Options as SlugOptions } from 'rehype-slug';
|
||||||
import { generateUniqueSlug } from './slug';
|
import rehypeAutolinkHeadings, { type Options as AutolinkOptions } from 'rehype-autolink-headings';
|
||||||
|
import type { Plugin } from 'carta-md';
|
||||||
export * from './default.css?inline';
|
export * from './default.css?inline';
|
||||||
|
|
||||||
export interface AnchorExtensionOptions {
|
export interface AnchorExtensionOptions {
|
||||||
/**
|
/**
|
||||||
* Maximum depth of headers to generate anchors for. Defaults to 6.
|
* rehype-slug options.
|
||||||
*/
|
*/
|
||||||
maxDepth?: number;
|
slug?: SlugOptions;
|
||||||
|
/**
|
||||||
|
* rehype-autolink-headings options.
|
||||||
|
*/
|
||||||
|
autolink?: AutolinkOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta anchor plugin. Adds support to render anchor links in header tags.
|
* Carta anchor plugin. Adds support to render anchor links in header tags.
|
||||||
*/
|
*/
|
||||||
export const anchor = (options?: AnchorExtensionOptions): CartaExtension => {
|
export const anchor = (options?: AnchorExtensionOptions): Plugin => {
|
||||||
let slugs: string[] = [];
|
|
||||||
|
|
||||||
const maxDepth = options?.maxDepth ?? 6;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Reset the slug history after rendering completes, so the links persist after re-rendering
|
transformers: [
|
||||||
listeners: [
|
|
||||||
['carta-render', () => (slugs = [])],
|
|
||||||
['carta-render-ssr', () => (slugs = [])]
|
|
||||||
],
|
|
||||||
markedExtensions: [
|
|
||||||
{
|
{
|
||||||
renderer: {
|
execution: 'sync',
|
||||||
heading(text, level, raw) {
|
type: 'rehype',
|
||||||
if (level > maxDepth) {
|
transform({ processor }) {
|
||||||
return false;
|
processor.use(rehypeSlug, options?.slug).use(rehypeAutolinkHeadings, options?.autolink);
|
||||||
}
|
|
||||||
|
|
||||||
const slug = generateUniqueSlug(raw, slugs);
|
|
||||||
|
|
||||||
return `
|
|
||||||
<h${level}>
|
|
||||||
<span>${text}</span>
|
|
||||||
<a id="${slug}" href="#${slug}" class="anchor-link">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 16 16" width="16" height="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>
|
|
||||||
</a>
|
|
||||||
</h${level}>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
3
packages/plugin-anchor/src/lib/link.svg
Normal file
3
packages/plugin-anchor/src/lib/link.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" fill-rule="evenodd" d="M9.929 3.132a2.078 2.078 0 1 1 2.94 2.94l-.65.648a.75.75 0 0 0 1.061 1.06l.649-.648a3.579 3.579 0 0 0-5.06-5.06L6.218 4.72a3.58 3.58 0 0 0 0 5.06a.75.75 0 0 0 1.061-1.06a2.08 2.08 0 0 1 0-2.94zm-.15 3.086a.75.75 0 0 0-1.057 1.064c.816.81.818 2.13.004 2.942l-2.654 2.647a2.08 2.08 0 0 1-2.94-2.944l.647-.647a.75.75 0 0 0-1.06-1.06l-.648.647a3.58 3.58 0 0 0 5.06 5.066l2.654-2.647a3.575 3.575 0 0 0-.007-5.068Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 575 B |
|
@ -1,23 +0,0 @@
|
||||||
import slugify from 'slugify';
|
|
||||||
|
|
||||||
function generateSlug(raw: string) {
|
|
||||||
const base = slugify(raw, {
|
|
||||||
lower: true,
|
|
||||||
remove: /[^a-zA-Z0-9_\- ]/g
|
|
||||||
});
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateUniqueSlug(raw: string, slugs: string[]) {
|
|
||||||
const base = generateSlug(raw);
|
|
||||||
let slug = base;
|
|
||||||
|
|
||||||
let i = 1;
|
|
||||||
// Add unique suffix to slug if it already exists
|
|
||||||
while (slugs.includes(slug)) {
|
|
||||||
slug = `${base}-${i}`;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
slugs.push(slug);
|
|
||||||
return slug;
|
|
||||||
}
|
|
|
@ -1,13 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { anchor } from '$lib';
|
import { anchor } from '$lib';
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
sanitizer: false,
|
||||||
extensions: [
|
extensions: [
|
||||||
anchor({
|
anchor({
|
||||||
maxDepth: 2
|
autolink: {}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -28,7 +29,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<CartaEditor {carta} {value} />
|
<MarkdownEditor {carta} {value} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -38,9 +39,15 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code, code) {
|
:global(.carta-font-code) {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
font-variant-ligatures: normal;
|
font-variant-ligatures: normal;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.carta-renderer) {
|
||||||
|
/* Add some space to show icons */
|
||||||
|
padding-left: 2.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(input, textarea, button) {
|
:global(input, textarea, button) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-attachment/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { attachment } from '@cartamd/plugin-attachment';
|
import { attachment } from '@cartamd/plugin-attachment';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -34,7 +34,7 @@ import '@cartamd/plugin-attachment/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -34,23 +34,24 @@
|
||||||
"!dist/**/*.spec.*"
|
"!dist/**/*.spec.*"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.4.0",
|
"carta-md": "^4.0.0",
|
||||||
"marked": "^9.1.5",
|
"marked": "^9.1.5",
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "^3.1.1",
|
||||||
"@sveltejs/kit": "^1.27.1",
|
"@sveltejs/kit": "^2.5.4",
|
||||||
"@sveltejs/package": "^2.2.2",
|
"@sveltejs/package": "^2.3.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@types/node-emoji": "^1.8.2",
|
"@types/node-emoji": "^1.8.2",
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
"marked": "^9.1.5",
|
"marked": "^9.1.5",
|
||||||
"publint": "^0.1.9",
|
"publint": "^0.1.9",
|
||||||
"svelte": "^4.2.2",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.5.2",
|
"svelte-check": "^3.6.7",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^4.3.9"
|
"vite": "^5.1.6"
|
||||||
},
|
},
|
||||||
"svelte": "./dist/index.js",
|
"svelte": "./dist/index.js",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Carta, CartaExtension, CartaListener } from 'carta-md';
|
import type { Carta, Plugin, Listener } from 'carta-md';
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import type { SvelteComponent } from 'svelte';
|
import type { SvelteComponent } from 'svelte';
|
||||||
import DropOverlay from './DropOverlay.svelte';
|
import DropOverlay from './DropOverlay.svelte';
|
||||||
|
@ -40,7 +40,7 @@ const ImageMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml']
|
||||||
/**
|
/**
|
||||||
* Carta attachment plugin.
|
* Carta attachment plugin.
|
||||||
*/
|
*/
|
||||||
export const attachment = (options: AttachmentExtensionOptions): CartaExtension => {
|
export const attachment = (options: AttachmentExtensionOptions): Plugin => {
|
||||||
let carta: Carta | undefined;
|
let carta: Carta | undefined;
|
||||||
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
|
const allowedMimeTypes = options.supportedMimeTypes || ImageMimeTypes;
|
||||||
|
|
||||||
|
@ -102,15 +102,31 @@ export const attachment = (options: AttachmentExtensionOptions): CartaExtension
|
||||||
for (const file of files) handleFile(file);
|
for (const file of files) handleFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePaste(this: HTMLTextAreaElement, e: ClipboardEvent) {
|
||||||
|
const items = e.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
const itemsArray = Array.from(items);
|
||||||
|
for (const item of itemsArray) {
|
||||||
|
if (item.kind === 'file') {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (!file) continue;
|
||||||
|
e.preventDefault();
|
||||||
|
handleFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onLoad: ({ carta: c }) => {
|
onLoad: ({ carta: c }) => {
|
||||||
carta = c;
|
carta = c;
|
||||||
},
|
},
|
||||||
listeners: [
|
listeners: [
|
||||||
['drop', handleDrop, false] satisfies CartaListener<'drop'>,
|
['drop', handleDrop, false] satisfies Listener<'drop'>,
|
||||||
['dragenter', () => draggingOverTextArea.set(true)] satisfies CartaListener<'dragenter'>,
|
['dragenter', () => draggingOverTextArea.set(true)] satisfies Listener<'dragenter'>,
|
||||||
['dragleave', () => draggingOverTextArea.set(false)] satisfies CartaListener<'dragleave'>,
|
['dragleave', () => draggingOverTextArea.set(false)] satisfies Listener<'dragleave'>,
|
||||||
['dragover', (e) => e.preventDefault()] satisfies CartaListener<'dragover'>
|
['dragover', (e) => e.preventDefault()] satisfies Listener<'dragover'>,
|
||||||
|
['paste', handlePaste, false] satisfies Listener<'paste'>
|
||||||
],
|
],
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { attachment } from '$lib';
|
import { attachment } from '$lib';
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
|
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
# Carta Code Plugin
|
# Carta Code Plugin
|
||||||
|
|
||||||
This plugin adds support for code blocks **syntax highlighting**. Install it using:
|
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
|
||||||
|
|
||||||
```
|
```
|
||||||
npm i @cartamd/plugin-code
|
npm i @cartamd/plugin-code
|
||||||
```
|
```
|
||||||
|
|
||||||
This is done using [Speed-highlight JS](https://github.com/speed-highlight/core), which supports dynamic imports. This way, languages definitions are only imported at the moment of need.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Styles
|
### Styles
|
||||||
|
@ -20,28 +18,31 @@ import '@cartamd/plugin-code/default.css';
|
||||||
|
|
||||||
### Using the default highlighter
|
### Using the default highlighter
|
||||||
|
|
||||||
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default.
|
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options. Remember to also have it loaded into the highlighter, by specifying it in `shikiOptions`.
|
||||||
The theme is the same as the one used in the main carta package (`carta-md/light.css` or `carta-md/dark.css`).
|
|
||||||
[Here](https://github.com/speed-highlight/core/tree/main/src/themes) you can find other themes.
|
|
||||||
|
|
||||||
### Using a custom highlighter
|
|
||||||
|
|
||||||
You can also provide a custom highlighter, that can be either sync or async.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
const carta = new Carta({
|
||||||
|
// ...
|
||||||
|
extensions: [
|
||||||
code({
|
code({
|
||||||
customHighlight: {
|
theme: 'ayu-light'
|
||||||
highlighter: (code, lang) => myCustomHighlighter(code, lang),
|
})
|
||||||
langPrefix: 'my-highlighter-'
|
],
|
||||||
|
shikiOptions: {
|
||||||
|
themes: ['ayu-light']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using a custom highlighter
|
||||||
|
|
||||||
|
It is no longer possible to specify a custom highlighter in this plugin. However, there are many different [Remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) that provide syntax highlighting.
|
||||||
|
|
||||||
### Extension
|
### Extension
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { code } from '@cartamd/plugin-code';
|
import { code } from '@cartamd/plugin-code';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -49,7 +50,7 @@ code({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -16,21 +16,22 @@
|
||||||
"build": "tsc && tscp"
|
"build": "tsc && tscp"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@shikijs/rehype": "^1.4.0",
|
||||||
"@types/node": "^18.16.3",
|
"@types/node": "^18.16.3",
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"typescript-cp": "^0.1.8",
|
"typescript-cp": "^0.1.8"
|
||||||
"marked": "^9.1.5"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.0.0"
|
"carta-md": "^4.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked-highlight": "^2.0.6"
|
"@shikijs/rehype": "^1.4.0",
|
||||||
|
"unified": "^11.0.4"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
|
|
|
@ -1,70 +1,52 @@
|
||||||
import type { CartaExtension, HighlightFunctions } from 'carta-md';
|
import type { DualTheme, Theme, Plugin } from 'carta-md';
|
||||||
import { markedHighlight } from 'marked-highlight';
|
import type { RehypeShikiOptions } from '@shikijs/rehype';
|
||||||
|
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
|
||||||
|
|
||||||
interface CodeExtensionOptions {
|
export type CodeExtensionOptions = Omit<RehypeShikiOptions, 'theme' | 'themes'> & {
|
||||||
/**
|
theme?: Theme | DualTheme;
|
||||||
* Default language when none is provided.
|
|
||||||
*/
|
|
||||||
defaultLanguage?: string;
|
|
||||||
/**
|
|
||||||
* Whether to autodetect a language when none is provided.
|
|
||||||
* Overwritten by `defaultLanguage`.
|
|
||||||
*/
|
|
||||||
autoDetect?: string;
|
|
||||||
/**
|
|
||||||
* Line numbering.
|
|
||||||
* @defaults false.
|
|
||||||
*/
|
|
||||||
lineNumbering?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let shj: HighlightFunctions;
|
// 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.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta code highlighting plugin. Themes available on [GitHub](https://github.com/speed-highlight/core/tree/main/dist/themes).
|
* Carta code highlighting plugin. Themes available on [GitHub](https://github.com/speed-highlight/core/tree/main/dist/themes).
|
||||||
*/
|
*/
|
||||||
export const code = (options?: CodeExtensionOptions): CartaExtension => {
|
export const code = (options?: CodeExtensionOptions): Plugin => {
|
||||||
return {
|
return {
|
||||||
onLoad: ({ highlight }) => (shj = highlight),
|
transformers: [
|
||||||
markedExtensions: [
|
{
|
||||||
markedHighlight({
|
execution: 'async',
|
||||||
langPrefix: options?.customHighlight?.langPrefix ?? 'shj-lang-',
|
type: 'rehype',
|
||||||
async: true,
|
async transform({ processor, carta }) {
|
||||||
async highlight(code, lang) {
|
let theme = options?.theme;
|
||||||
if (options?.customHighlight) {
|
|
||||||
return await options.customHighlight.highlighter(code, lang);
|
const highlighter = await carta.highlighter();
|
||||||
|
if (!theme) {
|
||||||
|
theme = highlighter.theme; // Use the theme specified in the highlighter
|
||||||
}
|
}
|
||||||
|
|
||||||
const { highlight, highlightAutodetect } = shj;
|
if (isSingleTheme(theme)) {
|
||||||
|
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, theme });
|
||||||
lang ||= options?.defaultLanguage ?? '';
|
} else {
|
||||||
let highlighted: string | null = null;
|
processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, themes: theme });
|
||||||
|
}
|
||||||
if (lang) highlighted = await highlight(code, lang, !(options?.lineNumbering ?? false));
|
}
|
||||||
if (highlighted) return highlighted;
|
|
||||||
|
|
||||||
if (options?.autoDetect ?? true)
|
|
||||||
return await highlightAutodetect(code, !(options?.lineNumbering ?? false));
|
|
||||||
|
|
||||||
return (await highlight(code, 'plain', !(options?.lineNumbering ?? false))) as string;
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ import '@cartamd/plugin-emoji/default.css';
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { emoji } from '@cartamd/plugin-emoji';
|
import { emoji } from '@cartamd/plugin-emoji';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -28,7 +28,7 @@ import '@cartamd/plugin-emoji/default.css';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@cartamd/plugin-emoji",
|
"name": "@cartamd/plugin-emoji",
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
@ -33,26 +33,27 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bezier-easing": "^2.1.0",
|
"bezier-easing": "^2.1.0",
|
||||||
"node-emoji": "^1.11.0"
|
"node-emoji": "^1.11.0",
|
||||||
|
"remark-gemoji": "^8.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.1.0",
|
"carta-md": "^4.0.0",
|
||||||
"marked": "^9.1.5",
|
|
||||||
"svelte": "^3.54.0 || ^4.0.0"
|
"svelte": "^3.54.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "^3.1.1",
|
||||||
"@sveltejs/kit": "^1.5.0",
|
"@sveltejs/kit": "^2.5.4",
|
||||||
"@sveltejs/package": "^2.0.0",
|
"@sveltejs/package": "^2.3.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@types/node-emoji": "^1.8.2",
|
"@types/node-emoji": "^1.8.2",
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
|
"marked": "^9.1.5",
|
||||||
"publint": "^0.1.9",
|
"publint": "^0.1.9",
|
||||||
"svelte": "^3.54.0 || ^4.0.0",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.0.1",
|
"svelte-check": "^3.6.7",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^4.3.9",
|
"vite": "^5.1.6"
|
||||||
"marked": "^9.1.5"
|
|
||||||
},
|
},
|
||||||
"svelte": "./dist/index.js",
|
"svelte": "./dist/index.js",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { CartaExtension, CartaExtensionComponent } from 'carta-md';
|
import type { Plugin, ExtensionComponent, GrammarRule, HighlightingRule } from 'carta-md';
|
||||||
import type { TokenizerAndRendererExtension } from 'marked';
|
import remarkGemoji from 'remark-gemoji';
|
||||||
import { fade, scale, type TransitionConfig } from 'svelte/transition';
|
import { fade, scale, type TransitionConfig } from 'svelte/transition';
|
||||||
import nodeEmoji from 'node-emoji';
|
|
||||||
import Emoji from './Emoji.svelte';
|
import Emoji from './Emoji.svelte';
|
||||||
import BezierEasing from 'bezier-easing';
|
import BezierEasing from 'bezier-easing';
|
||||||
export * from './default.css?inline';
|
export * from './default.css?inline';
|
||||||
|
@ -25,7 +24,7 @@ interface ComponentProps {
|
||||||
/**
|
/**
|
||||||
* Carta emoji plugin. Adds support to render emojis as well as an emojis snippet.
|
* Carta emoji plugin. Adds support to render emojis as well as an emojis snippet.
|
||||||
*/
|
*/
|
||||||
export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
|
export const emoji = (options?: EmojiExtensionOptions): Plugin => {
|
||||||
const inTransition =
|
const inTransition =
|
||||||
options?.inTransition ??
|
options?.inTransition ??
|
||||||
((node: Element) =>
|
((node: Element) =>
|
||||||
|
@ -40,7 +39,7 @@ export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
|
||||||
duration: 100
|
duration: 100
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const emojiComponent: CartaExtensionComponent<ComponentProps> = {
|
const emojiComponent: ExtensionComponent<ComponentProps> = {
|
||||||
component: Emoji,
|
component: Emoji,
|
||||||
parent: 'input',
|
parent: 'input',
|
||||||
props: {
|
props: {
|
||||||
|
@ -49,39 +48,42 @@ export const emoji = (options?: EmojiExtensionOptions): CartaExtension => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const grammar = {
|
||||||
|
name: 'emoji',
|
||||||
|
type: 'inline',
|
||||||
|
definition: {
|
||||||
|
match: ':[a-zA-Z_]+:',
|
||||||
|
name: 'markup.emoji.markdown'
|
||||||
|
}
|
||||||
|
} satisfies GrammarRule;
|
||||||
|
|
||||||
|
const highlighting = {
|
||||||
|
light: {
|
||||||
|
scope: 'markup.emoji',
|
||||||
|
settings: {
|
||||||
|
foreground: '#3bf'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
scope: 'markup.emoji',
|
||||||
|
settings: {
|
||||||
|
foreground: '#4dacfa'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies HighlightingRule;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
markedExtensions: [
|
transformers: [
|
||||||
{
|
{
|
||||||
extensions: [emojiTokenizerAndRenderer()]
|
execution: 'sync',
|
||||||
|
type: 'remark',
|
||||||
|
transform({ processor }) {
|
||||||
|
processor.use(remarkGemoji);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
components: [emojiComponent],
|
components: [emojiComponent],
|
||||||
highlightRules: [
|
grammarRules: [grammar],
|
||||||
{
|
highlightingRules: [highlighting]
|
||||||
type: 'oper',
|
|
||||||
match: /:[a-z0-9_]+:/g
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function emojiTokenizerAndRenderer(): TokenizerAndRendererExtension {
|
|
||||||
return {
|
|
||||||
name: 'emoji',
|
|
||||||
level: 'inline',
|
|
||||||
start: (src) => src.indexOf(':'),
|
|
||||||
tokenizer: (src) => {
|
|
||||||
const match = src.match(/^:.*?:/)?.at(0);
|
|
||||||
if (!match) return undefined;
|
|
||||||
const emoji = nodeEmoji.find(match)?.emoji;
|
|
||||||
if (emoji) {
|
|
||||||
return {
|
|
||||||
type: 'emoji',
|
|
||||||
raw: match,
|
|
||||||
emoji
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderer: (token) => token.emoji
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { emoji } from '$lib';
|
import { emoji } from '$lib';
|
||||||
import 'carta-md/default.css';
|
import 'carta-md/default.css';
|
||||||
import '$lib/default.css';
|
import '$lib/default.css';
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -32,9 +32,10 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.carta-font-code, code) {
|
:global(.carta-font-code) {
|
||||||
font-family: 'Fira Code', monospace;
|
font-family: 'Fira Code', monospace;
|
||||||
font-variant-ligatures: normal;
|
font-variant-ligatures: normal;
|
||||||
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(input, textarea, button) {
|
:global(input, textarea, button) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
|
|
|
@ -38,7 +38,7 @@ or by using a content delivery network:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Carta, CartaEditor } from 'carta-md';
|
import { Carta, MarkdownEditor } from 'carta-md';
|
||||||
import { math } from '@cartamd/plugin-math';
|
import { math } from '@cartamd/plugin-math';
|
||||||
|
|
||||||
const carta = new Carta({
|
const carta = new Carta({
|
||||||
|
@ -46,7 +46,7 @@ or by using a content delivery network:
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CartaEditor {carta} />
|
<MarkdownEditor {carta} />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
@ -17,23 +17,20 @@
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/katex": "^0.16.0",
|
|
||||||
"carta-md": "workspace:*",
|
"carta-md": "workspace:*",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4"
|
||||||
"marked": "^9.1.5"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"carta-md": "^3.0.0",
|
"carta-md": "^4.0.0"
|
||||||
"katex": "^0.16.7",
|
|
||||||
"marked": "^9.1.5"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"katex": "^0.16.7"
|
"rehype-katex": "^7.0.0",
|
||||||
|
"remark-math": "^6.0.0"
|
||||||
},
|
},
|
||||||
"version": "3.0.0",
|
"version": "4.0.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"carta",
|
"carta",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import type { Carta, CartaExtension } from 'carta-md';
|
import type { Plugin } from 'carta-md';
|
||||||
import { TokenizerAndRendererExtension } from 'marked';
|
import remarkMath, { type Options as RemarkMathOptions } from 'remark-math';
|
||||||
import katex, { KatexOptions } from 'katex';
|
import rehypeKatex, { type Options as RehypeKatexOptions } from 'rehype-katex';
|
||||||
|
|
||||||
interface MathExtensionOptions {
|
interface MathExtensionOptions {
|
||||||
/**
|
/**
|
||||||
* Options for inline katex, eg: $a^2+b^2=c^2$
|
* Options for inline katex, eg: $a^2+b^2=c^2$
|
||||||
*/
|
*/
|
||||||
inline?: {
|
inline?: {
|
||||||
katexOptions?: KatexOptions;
|
|
||||||
/**
|
/**
|
||||||
* @default control+m
|
* @default control+m
|
||||||
*/
|
*/
|
||||||
|
@ -20,51 +19,45 @@ interface MathExtensionOptions {
|
||||||
* $$
|
* $$
|
||||||
*/
|
*/
|
||||||
block?: {
|
block?: {
|
||||||
/**
|
|
||||||
* Tag the generated katex will be put into. Must have `display: block`.
|
|
||||||
*/
|
|
||||||
tag?: string;
|
|
||||||
/**
|
|
||||||
* Whether to center the generated expression.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
center?: boolean;
|
|
||||||
/**
|
|
||||||
* Class for generated katex.
|
|
||||||
*/
|
|
||||||
class?: string;
|
|
||||||
/**
|
/**
|
||||||
* @default ctrl+shift+m
|
* @default ctrl+shift+m
|
||||||
*/
|
*/
|
||||||
shortcut?: Set<string>;
|
shortcut?: Set<string>;
|
||||||
katexOptions?: KatexOptions;
|
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Options for remark-math
|
||||||
|
*/
|
||||||
|
remarkMath?: RemarkMathOptions;
|
||||||
|
/**
|
||||||
|
* Options for rehype-katex
|
||||||
|
*/
|
||||||
|
rehypeKatex?: RehypeKatexOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeRender(tex: string, options?: KatexOptions | undefined) {
|
|
||||||
try {
|
|
||||||
return katex.renderToString(tex, options);
|
|
||||||
} catch (_) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let carta: Carta;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carta math plugin. Code adapted from [marked-katex-extension](https://github.com/UziTech/marked-katex-extension).
|
* Carta math plugin. Code adapted from [marked-katex-extension](https://github.com/UziTech/marked-katex-extension).
|
||||||
*/
|
*/
|
||||||
export const math = (options?: MathExtensionOptions): CartaExtension => {
|
export const math = (options?: MathExtensionOptions): Plugin => {
|
||||||
return {
|
return {
|
||||||
onLoad: ({ carta: c, highlight: shj }) => {
|
onLoad: async ({ carta }) => {
|
||||||
carta = c;
|
const highlighter = await carta.highlighter();
|
||||||
import('./latex.js')
|
await highlighter.loadLanguage('latex');
|
||||||
.then((module) => shj.loadCustomLanguage('latex', module))
|
carta.input?.update();
|
||||||
.then(() => carta.input?.update());
|
|
||||||
},
|
},
|
||||||
markedExtensions: [
|
transformers: [
|
||||||
{
|
{
|
||||||
extensions: [inlineKatex(options?.inline), blockKatex(options?.block)]
|
execution: 'sync',
|
||||||
|
type: 'remark',
|
||||||
|
transform({ processor }) {
|
||||||
|
processor.use(remarkMath, options?.remarkMath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
execution: 'sync',
|
||||||
|
type: 'rehype',
|
||||||
|
transform({ processor }) {
|
||||||
|
processor.use(rehypeKatex, options?.rehypeKatex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
|
@ -79,64 +72,57 @@ export const math = (options?: MathExtensionOptions): CartaExtension => {
|
||||||
action: (input) => input.toggleSelectionSurrounding(['$$\n', '\n$$'])
|
action: (input) => input.toggleSelectionSurrounding(['$$\n', '\n$$'])
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
highlightRules: [
|
grammarRules: [
|
||||||
{
|
{
|
||||||
match: /\$[{}[\]a-zA-Z0-9.+-_=*/\\ ]+\$/g,
|
name: 'inline_math',
|
||||||
sub: 'latex'
|
type: 'inline',
|
||||||
|
definition: {
|
||||||
|
match: '(\\$+)((?:[^\\$]|(?!(?<!\\$)\\1(?!\\$))\\$)*+)(\\1)',
|
||||||
|
name: 'markup.inline.math.markdown',
|
||||||
|
captures: {
|
||||||
|
'1': { name: 'punctuation.definition.latex.inline' },
|
||||||
|
'2': { name: 'meta.embedded.block.latex', patterns: [{ include: 'text.tex.latex' }] },
|
||||||
|
'3': { name: 'punctuation.definition.latex.inline' }
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /^\$\$+\n([^$]+?)\n\$\$+\n/gm,
|
name: 'block_math',
|
||||||
sub: 'latex'
|
type: 'block',
|
||||||
|
definition: {
|
||||||
|
begin: '(^|\\G)(\\s*)(\\${2,})\\s*(?=([^$]*)?$)',
|
||||||
|
beginCaptures: {
|
||||||
|
'3': { name: 'punctuation.definition.latex.block' }
|
||||||
|
},
|
||||||
|
endCaptures: { '3': { name: 'punctuation.definition.latex.block' } },
|
||||||
|
end: '(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$',
|
||||||
|
name: 'markup.block.math.markdown',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
begin: '(^|\\G)(\\s*)(.*)',
|
||||||
|
contentName: 'meta.embedded.block.latex',
|
||||||
|
patterns: [{ include: 'text.tex.latex' }],
|
||||||
|
while: '(^|\\G)(?!\\s*([$]{2,})\\s*$)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
highlightingRules: [
|
||||||
|
{
|
||||||
|
light: {
|
||||||
|
scope: 'punctuation.definition.latex',
|
||||||
|
settings: {
|
||||||
|
foreground: '#5AF'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
scope: 'punctuation.definition.latex',
|
||||||
|
settings: {
|
||||||
|
foreground: '#4DACFA'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const inlineKatex = (options?: MathExtensionOptions['inline']): TokenizerAndRendererExtension => {
|
|
||||||
return {
|
|
||||||
name: 'inlineKatex',
|
|
||||||
level: 'inline',
|
|
||||||
start: (src) => src.indexOf('$'),
|
|
||||||
tokenizer: (src) => {
|
|
||||||
const match = src.match(/^\$+([^$\n]+?)\$+/);
|
|
||||||
if (match) {
|
|
||||||
return {
|
|
||||||
type: 'inlineKatex',
|
|
||||||
raw: match[0],
|
|
||||||
text: match[1].trim()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderer: (token) => safeRender(token.text, options?.katexOptions)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const blockKatex = (options?: MathExtensionOptions['block']): TokenizerAndRendererExtension => {
|
|
||||||
return {
|
|
||||||
name: 'blockKatex',
|
|
||||||
level: 'block',
|
|
||||||
start: (src) => src.indexOf('\n$$'),
|
|
||||||
tokenizer: (src) => {
|
|
||||||
const match = src.match(/^\$\$+\n([^$]+?)\n\$\$+\n/);
|
|
||||||
if (match) {
|
|
||||||
return {
|
|
||||||
type: 'blockKatex',
|
|
||||||
raw: match[0],
|
|
||||||
text: match[1].trim()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderer: (token) => {
|
|
||||||
const tag = options?.tag ?? 'p';
|
|
||||||
const center = options?.center ?? true;
|
|
||||||
const katexOptions = options?.katexOptions ?? {};
|
|
||||||
if (katexOptions?.displayMode === undefined) katexOptions.displayMode = true;
|
|
||||||
return `
|
|
||||||
<${tag}
|
|
||||||
class="${options?.class ?? ''}"
|
|
||||||
${center ? 'align="center"' : ''}
|
|
||||||
>${safeRender(token.text, katexOptions)}
|
|
||||||
</${tag}>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue