Compare commits

...

125 commits

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

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

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

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

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

fix #42
2024-03-19 15:16:54 +01:00
BearToCode
f0aaaed536 ci: remove signatures audit step
The step is currently failing due to vite 5.1.6 invalid signatures
2024-03-19 14:50:47 +01:00
BearToCode
3e8e7417d4 build: update dev dependencies 2024-03-18 19:41:22 +01:00
BearToCode
a36b57f0c4 docs: fix labels in wrong section 2024-03-06 19:00:51 +01:00
BearToCode
46a58adbec docs: update sanitizer example 2024-03-05 14:28:53 +01:00
Davide
b0deb50a25
docs: add missing disableIcons option 2024-02-11 14:15:00 +01:00
BearToCode
db3770cece docs: fix links style 2024-01-23 21:25:07 +01:00
BearToCode
00d67f3cbf docs: fix styles and links for community plugins 2024-01-23 21:16:16 +01:00
BearToCode
0af2e23f25 docs: fix margin styles in community plugins 2024-01-22 18:51:15 +01:00
BearToCode
893742d90e docs: add community plugins in README and docs 2024-01-22 18:45:57 +01:00
Davide
d33285251a
feat(cartaeditor): add tooltip on quick toolbar icon buttons (#34) 2024-01-19 09:34:24 +01:00
maisonsmd
4693658089 feat(cartaeditor): add tooltip on quick toolbar icon buttons
add tooltip on quick toolbar icon buttons
2024-01-18 10:39:51 +07:00
Davide
d3f3b4cc09
Add heading anchor link plugin (#31) 2024-01-16 19:27:27 +01:00
BearToCode
4843c27c67 chore: add plugin-achor to list of packages 2024-01-16 19:24:48 +01:00
maisonsmd
8f98e2ad36 docs(add docs for anchor plugin): add docs for anchor plugin
Add docs for anchor plugin
2024-01-16 23:22:26 +07:00
maisonsmd
2884ceeff1 feat(add new plugin: anchor): add new plugin: anchor
Add ability to render anchor links after headings
2024-01-16 23:17:32 +07:00
BearToCode
5ddddbdc2e fix(plugin-math): automatically enable displayMode for blocks 2024-01-06 12:31:12 +01:00
BearToCode
5b0ce03e6d ci: update pnpm version in workflows 2023-12-19 21:51:56 +01:00
BearToCode
2a7c77e757 ci: update pnpm version in workflows 2023-12-19 21:49:34 +01:00
BearToCode
6fa30f2d3e fix(tikzjax): prevent multiple scripts from being loaded 2023-12-19 21:46:43 +01:00
Davide
b7c8704de3
Improve editor accessibility (#28) 2023-12-17 12:18:26 +01:00
BearToCode
4e097b81ab feat(accessibility): add label to attachment icon 2023-12-17 12:16:02 +01:00
BearToCode
502e564f77 docs: mention accessibility in the docs 2023-12-17 12:06:41 +01:00
BearToCode
dc270ffce9 fix(accessibility): better tab support, allow editor unfocus by using tab 2023-12-17 12:01:57 +01:00
BearToCode
68350779f6 feat(accessibility): arrow keys navigation, add aria-roles
fix #22
2023-12-17 12:01:46 +01:00
BearToCode
56e1572a75 chore: add aria labels to icons 2023-12-17 12:01:15 +01:00
Davide
1b75fe794e
feat: add custom labels support (#23)
* feat: add custom labels
* build: pin prettier to v3.1.0
2023-12-16 23:23:38 +01:00
Davide
14be70bb74
feat: add textarea property to allow additional textarea properties (#27) 2023-12-09 11:20:25 +01:00
BearToCode
661cd137d9 feat: add textarea property to allow additional textarea properties
fix #26
2023-12-09 11:15:46 +01:00
Davide
4075b8a76f
fix: prevent form submission by the "write" and "preview" tab buttons (#25) 2023-12-08 18:41:13 +01:00
Christopher Carson
f7f49a513a added type="button" to the tab buttons 2023-12-08 10:35:51 -06:00
Davide
c9002efa7e
docs: use custom warning box 2023-12-02 21:44:59 +01:00
BearToCode
7bdd027b0a docs: update old links in readme 2023-11-29 22:30:45 +01:00
BearToCode
0bef4b8d37 docs: add links to images 2023-11-19 23:55:56 +01:00
BearToCode
345c987273 docs: fix href 2023-11-19 23:54:39 +01:00
BearToCode
32a9862c4c docs: change content order 2023-11-19 20:13:11 +01:00
BearToCode
d2e82deadd docs: fix wrong search urls 2023-11-19 20:09:40 +01:00
BearToCode
fd189175cd docs: fix wrong search urls 2023-11-19 20:06:05 +01:00
BearToCode
e3200b93a3 docs: fix wrong urls 2023-11-19 20:01:57 +01:00
BearToCode
3420f1893f ci: fix wrong gh-pages dir 2023-11-19 19:56:06 +01:00
Davide
c5f5a8ed1a
docs: add documentation site (#19) 2023-11-19 19:51:27 +01:00
BearToCode
1600e4ec23 docs: add docs columns to packages 2023-11-19 19:49:34 +01:00
BearToCode
1d78d5b7b7 docs: use same color for all shields 2023-11-19 19:44:57 +01:00
BearToCode
18ca2902ab docs: fix broken align 2023-11-19 19:43:25 +01:00
BearToCode
71d41f6ee7 docs: fix broken shield 2023-11-19 19:42:35 +01:00
BearToCode
e510e913df docs: update all READMEs 2023-11-19 19:37:48 +01:00
BearToCode
515c98fe0e docs: update deps 2023-11-19 15:49:20 +01:00
BearToCode
a56b05b36a docs: replace demo with docs 2023-11-19 15:44:30 +01:00
BearToCode
9ce2539e3d docs: add search 2023-11-19 15:04:06 +01:00
BearToCode
6d80e55cfb Merge branch 'docs' of https://github.com/BearToCode/carta-md into docs 2023-11-19 12:04:28 +01:00
BearToCode
06cd281170 docs: responsive design 2023-11-19 12:04:12 +01:00
BearToCode
39df745e6e docs: add remaining pages 2023-11-19 12:04:12 +01:00
BearToCode
26fe1f6cba docs: add math stack exchange example 2023-11-19 12:04:12 +01:00
BearToCode
4ae846ed08 docs: getting started page 2023-11-19 12:04:12 +01:00
BearToCode
a0325eb73d style: format files using new prettier version 2023-11-19 12:04:11 +01:00
BearToCode
0501342b4c docs: add introduction and examples 2023-11-19 12:04:11 +01:00
BearToCode
08d52652d1 style: update prettier 2023-11-19 12:04:11 +01:00
BearToCode
389f09739c docs: responsive design 2023-11-19 00:08:40 +01:00
BearToCode
7d7f69ee0a docs: add remaining pages 2023-11-18 19:59:33 +01:00
BearToCode
471d5d5eae docs: add math stack exchange example 2023-11-16 19:31:54 +01:00
BearToCode
c063d314d6 fix(core): correct carta viewer class 2023-11-16 18:59:02 +01:00
BearToCode
eba352fe15 docs: getting started page 2023-11-16 18:17:07 +01:00
BearToCode
75a535eed9 style: format files using new prettier version 2023-11-16 08:18:52 +01:00
BearToCode
521fadaa35 docs: add introduction and examples 2023-11-15 22:32:07 +01:00
BearToCode
ef1a15f4fa style: update prettier 2023-11-15 21:44:08 +01:00
BearToCode
be8a860bcb fix(core): teleported elements theme class 2023-11-15 21:43:39 +01:00
BearToCode
b187a6a904 fix(core): keep history state when reloading input 2023-11-15 21:26:52 +01:00
BearToCode
a61e04987c refactor(core): use local highlighting rules 2023-11-15 20:43:21 +01:00
BearToCode
4cdd7ed551 fix: custom markdown not correctly loaded 2023-11-15 19:17:38 +01:00
BearToCode
1c099e8f3c refactor: remove unused vars 2023-11-14 22:28:16 +01:00
BearToCode
cf623603b6 fix(core): lost of elements reference on input change 2023-11-14 22:27:32 +01:00
BearToCode
b070f5dbe2 fix(core): allow any html element as portal for bindToCaret 2023-11-14 21:15:22 +01:00
BearToCode
e52e12f7f1 fix(core): correct row height calculation 2023-11-14 20:51:22 +01:00
BearToCode
e14f9fc666 fix(core): wrong caret elements positioning 2023-11-14 19:13:25 +01:00
BearToCode
e5e4ad4f06 feat: better caret-bounded components positioning 2023-11-14 18:44:18 +01:00
BearToCode
20bc2819c9 fix: remove typescript project references 2023-11-14 18:37:19 +01:00
BearToCode
1db7fc42b7 fix: tsconfig TS6310 may not disable emit 2023-11-14 18:31:24 +01:00
BearToCode
0d7442aff6 feat(core): add bindToCaret action 2023-11-14 15:05:54 +01:00
BearToCode
25144519d7 fix(plugin-attachment): fix prop mismatch for custom loading overlay 2023-11-14 00:14:04 +01:00
188 changed files with 11468 additions and 3602 deletions

View file

@ -1 +1,2 @@
dist
.svelte-kit

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

23
.vscode/settings.json vendored
View file

@ -1,7 +1,26 @@
{
"cSpell.words": ["Carta", "cartamd", "Katex", "tikzjax", "tikz"],
"cSpell.words": [
"Carta",
"cartamd",
"coldark",
"dompurify",
"flexsearch",
"Gemoji",
"gruvbox",
"iconify",
"Katex",
"mdsvex",
"oldschool",
"rehype",
"shiki",
"shikijs",
"tikz",
"tikzjax",
"typeof"
],
"typescript.tsdk": "node_modules\\typescript\\lib",
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
}
},
"css.customData": [".vscode/tailwind.json"]
}

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

@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that youd like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}

196
README.md
View file

@ -1,42 +1,88 @@
<div align="right">
<a href="https://www.npmjs.com/package/carta-md">
<img src="https://img.shields.io/npm/v/carta-md?color=ff7cc6&labelColor=171d27&logo=npm&logoColor=white" alt="npm">
</a>
<a href="https://bundlephobia.com/package/carta-md">
<img src="https://img.shields.io/bundlephobia/min/carta-md?color=4dacfa&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle">
</a>
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE">
<img src="https://img.shields.io/npm/l/carta-md?color=71d58a&labelColor=171d27&logo=git&logoColor=white" alt="license">
</a>
<a href="http://beartocode.github.io/carta/">
<img src="https://img.shields.io/readthedocs/carta?logo=svelte&color=b581fd&logoColor=ffffff&labelColor=171d27" alt="docs">
</a>
</div>
[![Carta.png](https://i.postimg.cc/nV6DMXKM/Carta.png)](https://beartocode.github.io/carta/)
<h1 align="center"><strong>Carta</strong></h1>
<div align="center">Modern, lightweight, powerful Markdown Editor.</div>
<br />
<div align="center">
<img src="https://see.fontimg.com/api/renderfont4/lemD/eyJyIjoiZnMiLCJoIjoxMjMsInciOjEyNTAsImZzIjo5OCwiZmdjIjoiIzQ2RUJFNyIsImJnYyI6IiNGRkZGRkYiLCJ0IjoxfQ/Q2FydGE/bukhari-script.png" width="240" alt="logo">
<a href="https://beartocode.github.io/carta/">📚 Documentation</a>
<span> · </span>
<a href="https://github.com/BearToCode/carta">GitHub</a>
</div>
<br>
<div align="center">
<a href="https://www.npmjs.com/package/carta-md"><img src="https://img.shields.io/npm/v/carta-md?color=16b57c&labelColor=171d27&logo=npm&logoColor=white" alt="npm"></a>
<a href="https://bundlephobia.com/package/carta-md"><img src="https://img.shields.io/bundlephobia/min/carta-md?color=16b57c&labelColor=171d27&logo=javascript&logoColor=white" alt="bundle"></a>
<a href="https://github.com/BearToCode/carta/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/carta-md?color=16b57c&labelColor=171d27&logo=git&logoColor=white" alt="license"></a>
<a href="http://beartocode.github.io/carta/"><img src="https://img.shields.io/badge/available-red?label=demo&color=16b57c&labelColor=171d27&logo=svelte&logoColor=white" alt="demo"></a>
</div>
# Introduction
<br>
> [!NOTE]
> Carta has recently been updated to `v4`, which features numerous major changes.
>
> Follow the [Migration Guide](http://beartocode.github.io/carta/migration) to update your project.
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer, based on [Marked](https://github.com/markedjs/marked). Check out the [demo](http://beartocode.github.io/carta/) to see it in action.
Differently from most editors, Carta includes neither ProseMirror nor CodeMirror, allowing for an extremely small bundle size and fast loading time.
Carta is a **lightweight**, **fast** and **extensible** Svelte Markdown editor and viewer. It is powered by [unified](https://github.com/unifiedjs/unified), [remark](https://github.com/remarkjs/remark) and [rehype](https://github.com/rehypejs/rehype). Check out the [examples](http://beartocode.github.io/carta/examples) to see it in action.
Differently from most editors, Carta does not include a code editor, but it is _just_ a textarea with syntax highlighting, shortcuts and more.
## Features
- Keyboard **shortcuts** (extensible);
- Toolbar (extensible);
- Markdown syntax highlighting;
- Scroll sync;
- **SSR** compatible;
- **Katex** support (plugin);
- **Slash** commands (plugin);
- **Emojis**, with included search (plugin);
- **Tikz** support(plugin);
- **Attachment** support(plugin);
- Code blocks **syntax highlighting** (plugin).
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
- 🛠️ Toolbar (extensible);
- ⌨️ Keyboard **shortcuts** (extensible);
- 📦 Supports **[150+ plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark;
- 🔀 Scroll sync;
- ✅ Accessibility friendly;
- 🖥️ **SSR** compatible;
- ⚗️ **KaTeX** support (plugin);
- 🔨 **Slash** commands (plugin);
- 😄 **Emojis**, with included search (plugin);
- ✏️ **TikZ** support (plugin);
- 📂 **Attachment** support (plugin);
- ⚓ **Anchor** links in headings (plugin);
- 🌈 Code blocks **syntax highlighting** (plugin).
## Getting started
## Packages
> **Warning**
| Package | Status | Docs |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [carta-md](https://www.npmjs.com/package/carta-md) | ![carta-md](https://img.shields.io/npm/v/carta-md) | [/](https://beartocode.github.io/carta/introduction) |
| [plugin-math](https://www.npmjs.com/package/@cartamd/plugin-math) | ![plugin-math](https://img.shields.io/npm/v/@cartamd/plugin-math) | [/plugins/math](https://beartocode.github.io/carta/plugins/math) |
| [plugin-code](https://www.npmjs.com/package/@cartamd/plugin-code) | ![plugin-code](https://img.shields.io/npm/v/@cartamd/plugin-code) | [/plugins/code](https://beartocode.github.io/carta/plugins/code) |
| [plugin-emoji](https://www.npmjs.com/package/@cartamd/plugin-emoji) | ![plugin-emoji](https://img.shields.io/npm/v/@cartamd/plugin-emoji) | [/plugins/emoji](https://beartocode.github.io/carta/plugins/emoji) |
| [plugin-slash](https://www.npmjs.com/package/@cartamd/plugin-slash) | ![plugin-slash](https://img.shields.io/npm/v/@cartamd/plugin-slash) | [/plugins/slash](https://beartocode.github.io/carta/plugins/slash) |
| [plugin-tikz](https://www.npmjs.com/package/@cartamd/plugin-tikz) | ![plugin-tikz](https://img.shields.io/npm/v/@cartamd/plugin-tikz) | [/plugins/tikz](https://beartocode.github.io/carta/plugins/tikz) |
| [plugin-attachment](https://www.npmjs.com/package/@cartamd/plugin-attachment) | ![plugin-attachment](https://img.shields.io/npm/v/@cartamd/plugin-attachment) | [/plugins/attachment](https://beartocode.github.io/carta/plugins/attachment) |
| [plugin-anchor](https://www.npmjs.com/package/@cartamd/plugin-anchor) | ![plugin-anchor](https://img.shields.io/npm/v/@cartamd/plugin-anchor) | [/plugins/anchor](https://beartocode.github.io/carta/plugins/anchor) |
## Community plugins
| Plugin | Description |
| ----------------------------------------------------------------------------- | ---------------------------------- |
| [carta-plugin-video](https://github.com/maisonsmd/carta-plugin-video) | Render online videos |
| [carta-plugin-imsize](https://github.com/maisonsmd/carta-plugin-imsize) | Render images in specific sizes |
| [carta-plugin-subscript](https://github.com/maisonsmd/carta-plugin-subscript) | Render subscripts and superscripts |
| [carta-plugin-ins-del](https://github.com/maisonsmd/carta-plugin-ins-del) | `<ins>` and `<del>` tags support |
# Getting started
> [!WARNING]
> Sanitization is not dealt with by Carta. You need to provide a `sanitizer` in the options.
> Common sanitizers are [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) (suggested) and [sanitize-html](https://www.npmjs.com/package/sanitize-html).
> Checkout the documentation for an example.
### Installation
## Installation
Core package:
@ -50,15 +96,13 @@ Plugins:
npm i @cartamd/plugin-name
```
### Basic configuration
## Basic configuration
```svelte
<script lang="ts">
import { Carta, CartaEditor } from 'carta-md';
import { Carta, MarkdownEditor } from 'carta-md';
// Component default theme
import 'carta-md/default.css';
// Markdown input theme (Speed Highlight)
import 'carta-md/light.css';
const carta = new Carta({
// Remember to use a sanitizer to prevent XSS attacks
@ -66,85 +110,40 @@ npm i @cartamd/plugin-name
});
</script>
<CartaEditor {carta} />
<MarkdownEditor {carta} />
<style>
/* Or in global stylesheet */
/* Set your custom monospace font */
:global(.carta-font-code) {
font-family: '...', monospace;
font-size: 1.1rem;
}
</style>
```
Editor component exported properties:
# Documentation
| Name | Type | Description |
| ---------------- | ----------------------------- | ----------------------------------------- |
| `carta` | `Carta` | Carta Editor |
| `theme` | `string` | For custom css themes, see below for more |
| `value` | `string` | Markdown input |
| `placeholder` | `string` | Placeholder text for textarea |
| `mode` | `'tabs' \| 'split' \| 'auto'` | Tabs settings |
| `disableToolbar` | `boolean` | Option to disable the toolbar |
For the full documentation, examples, guides and more checkout the [website](https://beartocode.github.io/carta/).
### Plugins
- [Introduction](https://beartocode.github.io/carta/introduction)
- [Examples](https://beartocode.github.io/carta/examples)
- [Getting Started](https://beartocode.github.io/carta/getting-started)
- [Editing Styles](https://beartocode.github.io/carta/editing-styles)
- Plugins:
- [Math](https://beartocode.github.io/carta/plugins/math)
- [Code](https://beartocode.github.io/carta/plugins/code)
- [Emoji](https://beartocode.github.io/carta/plugins/emoji)
- [Slash](https://beartocode.github.io/carta/plugins/slash)
- [TikZ](https://beartocode.github.io/carta/plugins/tikz)
- [Attachment](https://beartocode.github.io/carta/plugins/attachment)
- [Anchor](https://beartocode.github.io/carta/plugins/anchor)
- API:
- [Utilities](https://beartocode.github.io/carta/api/utilities)
- [Core](https://beartocode.github.io/carta/api/core)
- [Extension](https://beartocode.github.io/carta/api/extension)
Each plugin's _readme_ includes a guide on its use.
| Name | Description |
| ----------------------------------------------------------------------------------------------- | --------------------------------------- |
| [plugin-math](https://github.com/BearToCode/carta/tree/master/packages/plugin-math) | Katex support |
| [plugin-slash](https://github.com/BearToCode/carta/tree/master/packages/plugin-slash) | Slash commands support |
| [plugin-emoji](https://github.com/BearToCode/carta/tree/master/packages/plugin-emoji) | Emojis support, including inline search |
| [plugin-code](https://github.com/BearToCode/carta/tree/master/packages/plugin-code) | Code blocks syntax highlighting |
| [plugin-tikz](https://github.com/BearToCode/carta/tree/master/packages/plugin-tikz) | TikZ support using TikZJax |
| [plugin-attachment](https://github.com/BearToCode/carta/tree/master/packages/plugin-attachment) | Attachments support |
## Themes customization
By using the `theme` property in `CartaEditor` and `CartaPreview` you can change their classes to `carta-editor__{theme}` and `carta-viewer__{theme}`.
Check out the [default theme](https://github.com/BearToCode/carta/blob/master/packages/carta-md/src/lib/default.css) to customize it.
If you are using a plugin, look at its _readme_ for its customization.
Markdown highlighting is done using **Speed Highlight JS**, [here](https://github.com/speed-highlight/core/tree/main/src/themes) you can find more themes.
You can find complete Markdown stylesheet online. For example [github-markdown-css](https://github.com/sindresorhus/github-markdown-css)(used in the demo), or [tailwind-typography](https://tailwindcss.com/docs/typography-plugin).
## Extensibility
### Options
Carta options:
| Name | Type | Description |
| ------------------ | ------------------------------ | ----------------------------------------------- |
| `extensions` | `CartaExtension[]` | Editor/viewer extensions |
| `rendererDebounce` | `number` | Renderer debouncing timeout, in ms (def. 300ms) |
| `disableShortcuts` | `DefaultShortcutId[] \| true` | Remove default shortcuts by ids |
| `disableIcons` | `DefaultIconId[] \| true` | Remove default icons by ids |
| `disablePrefixes` | `DefaultPrefixId[] \| true` | Remove default prefixes by ids |
| `historyOptions` | `Partial<CartaHistoryOptions>` | History (Undo/Redo) options |
| `sanitizer` | `(html: string) => string` | HTML sanitizer |
You can easily extend Carta by creating custom plugins. Here are all the `CartaExtension` properties:
| Name | Type | Description |
| ------------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `markedExtensions` | `marked.MarkedExtension[]` | Marked extensions, more on that [here](https://marked.js.org/using_advanced) |
| `shortcuts` | `KeyboardShortcut[]` | Additional keyboard shortcuts |
| `icons` | `CartaIcon[]` | Additional icons |
| `prefixes` | `Prefix[]` | Additional prefixes |
| `listeners` | `CartaListener[]` | Textarea event listeners |
| `components` | `CartaExtensionComponents` | Additional components, that will be put after the editor. All components are given a `carta: Carta`. prop The editor has a `relative` position, so you can position elements absolutely |
| `highlightRules` | `HighlightRule[]` | Custom markdown highlight rules. See [Speed-Highlight Wiki](https://github.com/speed-highlight/core/wiki/Create-or-suggest-new-languages). |
| `onLoad` | `(data: { carta:Carta, ... }) => void` | Use this callback to execute code when one Carta instance loads the extension. |
If you created a plugin and want to share it, you can open an _issue_ and we will consider sponsoring it on this guide.
## Contributions
# Contributing & Development
Every contribution is well accepted. If you have a feature request you can open a new issue.
@ -159,3 +158,12 @@ npm run commit
# or, if you have commitizen installed globally
git cz
```
### Running docs
If you want to preview the docs:
```
cd docs
npm run dev
```

View file

@ -1,2 +0,0 @@
engine-strict=true
resolution-mode=highest

View file

@ -1,144 +0,0 @@
<script lang="ts">
import { Carta, CartaEditor } from 'carta-md';
import { math } from '@cartamd/plugin-math';
import { slash } from '@cartamd/plugin-slash';
import { emoji } from '@cartamd/plugin-emoji';
import { code } from '@cartamd/plugin-code';
import { tikz } from '@cartamd/plugin-tikz';
import { placeholderText } from './placeholder';
import 'carta-md/default.css';
import 'carta-md/light.css';
import 'katex/dist/katex.css';
import '@cartamd/plugin-code/default.css';
import '@cartamd/plugin-slash/default.css';
import '@cartamd/plugin-emoji/default.css';
import '@cartamd/plugin-tikz/fonts.css';
const carta = new Carta({
extensions: [
tikz({
debug: true
}),
slash(),
emoji(),
code(),
math()
]
});
let syncScroll = true;
let mode: 'tabs' | 'split' | 'auto' = 'auto';
</script>
<svelte:head>
<!-- Custom fonts -->
<!-- Fira font -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<!-- Inter -->
<link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
</svelte:head>
<main>
<div class="options">
<fieldset>
<legend>Scroll</legend>
<input bind:checked={syncScroll} type="checkbox" id="scroll" name="scroll" />
<label for="scroll">Sync scroll</label>
</fieldset>
<fieldset>
<legend>Editor mode</legend>
<div>
<input type="radio" id="auto" name="drone" value="auto" bind:group={mode} />
<label for="auto">Auto</label>
</div>
<div>
<input type="radio" id="tabs" name="drone" value="tabs" bind:group={mode} />
<label for="tabs">Tabs</label>
</div>
<div>
<input type="radio" id="split" name="drone" value="split" bind:group={mode} />
<label for="split">Split</label>
</div>
</fieldset>
</div>
<CartaEditor value={placeholderText} scroll={syncScroll ? 'sync' : 'async'} {carta} {mode} />
</main>
<style>
:global(body) {
margin: 0;
font-family: 'Inter var', sans-serif;
min-height: 100vh;
}
:global(.carta-font-code, code) {
font-family: 'Fira Code', monospace;
font-variant-ligatures: normal;
}
:global(input, textarea, button) {
font-family: inherit;
}
main {
max-width: 1536px;
margin: 0 auto 0 auto;
padding: 2rem 0 2rem 0;
}
fieldset {
display: flex;
}
.options {
margin-bottom: 1.5rem;
display: flex;
}
/* Responsive main */
@media screen and (max-width: 640px) {
main {
width: 95%;
}
}
@media screen and (min-width: 640px) and (max-width: 767px) {
main {
width: 640px;
}
}
@media screen and (min-width: 767px) and (max-width: 1023px) {
main {
width: 768px;
}
}
@media screen and (min-width: 1023px) and (max-width: 1279px) {
main {
width: 1024px;
}
}
@media screen and (min-width: 1279px) and (max-width: 1535px) {
main {
width: 1280px;
}
}
</style>

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,122 +0,0 @@
export const placeholderText = `# Carta Demo
Far out in the uncharted backwaters of the unfashionable end of the western spiral arm of the **Galaxy** lies a small unregarded yellow sun.
Orbiting this at a distance of roughly _ninety-two million miles_ is an utterly insignificant little blue green planet :earth_africa: whose ape-descended life forms are so amazingly primitive that they still think digital watches are a pretty neat idea.
\`\`\`tikz
\\usepackage{pgfplots}
\\pgfplotsset{compat=1.16}
\\pgfplotsset{width=7cm,compat=1.8}
\\begin{document}
\\begin{tikzpicture}[thick,scale=1.2, every node/.style={scale=1.2}]
\\begin{axis}
\\addplot3[
surf,
colormap/viridis,
samples=20,
domain=0:2*pi,
y domain=0:2*pi,
z buffer=sort
]
( {(2+cos(deg(x)))*cos(deg(y+pi/2))},
{(2+cos(deg(x)))*sin(deg(y+pi/2))},
{sin(deg(x))}
);
\\end{axis}
\\end{tikzpicture}
\\end{document}
\`\`\`
# Basic Markdown
You can have different types of text: **bold**, _italic_, \`code\` and ~~strikethrough~~. This is a [link](http://beartocode.github.io/carta/) to the page you are currently viewing.
Here is a quote:
> Time is an illusion. Lunchtime doubly so.
And then some lists:
- This is a bulleted list
- Which does not have an order
- but only some dots
1. And then that's one with numbers
2. That just keep going
3. and going
- [ ] And finally this is a task list
- [ ] Where you can keep track of to-dos
- [x] by putting an _x_ inside the brackets
You can also create tables, like so:
| Item | Price | Origin |
| ------- | ----- | ------ |
| :apple: | 2.1 | Italy |
| :banana:| 42 | Brazil |
| :lemon: | 18 | Spain |
# Official Plugins
## \`plugin-code\`
This plugin adds support for **syntax highlighting** on your code blocks:
\`\`\`rs
fn visit_mars() {
let spaceship = get_spaceship();
spaceship.liftoff();
spaceship.head_to(Planet::Mars);
thread::sleep(Time::Month(6));
spaceship.land();
}
\`\`\`
## \`plugin-math\`
With this plugin you can write beautiful Katex expressions, both _inline_ $\\underline{v}=A-\\lambda I_d$, and as _block_ equations:
$$
{\\displaystyle {\\boldsymbol {\\sigma }}=\\zeta (\\nabla \\cdot \\mathbf {u} )\\mathbf {I} +\\mu \\left[\\nabla \\mathbf {u} +(\\nabla \\mathbf {u} )^{\\mathrm {T} }-{\\tfrac {2}{3}}(\\nabla \\cdot \\mathbf {u} )\\mathbf {I} \\right]}
$$
## \`plugin-emoji\`
Adds support for **emojis**, as well as an inline emoji search, that appears after typing a colon. :smile_cat:
Try typing an icon to see the inline search. :alien:
## \`plugin-slash\`
Use a **slash** / to use commands while writing markdown. You can add your custom commands too. Also includes a inline command search.
## \`plugin-tikz\`
\`\`\`tikz
\\usepackage{pgfplots}
\\pgfplotsset{compat=1.16}
\\pgfplotsset{width=7cm}
\\begin{document}
\\begin{tikzpicture}[thick,scale=1.2, every node/.style={scale=1.2}]
\\begin{axis}[domain=-1:1,y domain=-1:1]
\\addplot3[
surf,
colormap/viridis,
samples = 18
]
{x*y*exp(x+2*y-9*x^2-9*y^2)};
\\end{axis}
\\end{tikzpicture}
\\end{document}
\`\`\`
Adds support for **TikZ**, thanks to [TikZJax](https://tikzjax.com/).
`;

View file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
.DS_Store
node_modules
/build
/dist
/.svelte-kit
/package
.env

13
docs/components.json Normal file
View file

@ -0,0 +1,13 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.postcss",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
}
}

16
docs/mdsvex.config.js Normal file
View file

@ -0,0 +1,16 @@
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
const config = defineConfig({
extensions: ['.svelte.md', '.md', '.svx'],
smartypants: {
dashes: 'oldschool'
},
remarkPlugins: [],
rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]]
});
export default config;

View file

@ -1,5 +1,5 @@
{
"name": "demo",
"name": "docs",
"version": "0.0.1",
"private": true,
"license": "MIT",
@ -15,24 +15,40 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "1.0.0-next.50",
"@sveltejs/kit": "^1.5.0",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/adapter-static": "3.0.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/package": "^2.3.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/flexsearch": "^0.7.6",
"@types/katex": "^0.16.0",
"svelte": "^3.54.0 || ^4.0.0",
"svelte-check": "^3.0.1",
"autoprefixer": "^10.4.16",
"mdsvex": "^0.11.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"sass": "^1.69.5",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tailwindcss": "^3.3.5",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.3.9"
"vite": "^5.1.6"
},
"type": "module",
"dependencies": {
"@cartamd/plugin-attachment": "workspace:^",
"@cartamd/plugin-code": "workspace:^",
"@cartamd/plugin-emoji": "workspace:^",
"@cartamd/plugin-math": "workspace:^",
"@cartamd/plugin-slash": "workspace:^",
"@cartamd/plugin-tikz": "workspace:^",
"bits-ui": "^0.9.1",
"carta-md": "workspace:^",
"katex": "^0.16.7"
"clsx": "^2.0.0",
"cmdk-sv": "^0.0.6",
"flexsearch": "0.7.21",
"iconify-icon": "^2.0.0",
"katex": "^0.16.10",
"tailwind-merge": "^2.0.0"
}
}

8
docs/postcss.config.cjs Normal file
View file

@ -0,0 +1,8 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const config = {
plugins: [tailwindcss(), autoprefixer]
};
module.exports = config;

25
docs/src/app.html Normal file
View file

@ -0,0 +1,25 @@
<!doctype html>
<html class="dark" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Inter -->
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
rel="stylesheet"
/>
<!-- Fira Code -->
<link
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

84
docs/src/app.postcss Normal file
View file

@ -0,0 +1,84 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
font-family: 'Inter', sans-serif;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
nav div[data-portal] {
display: none;
}

View file

@ -0,0 +1,20 @@
<script lang="ts">
let elem: HTMLElement;
</script>
<div bind:this={elem} class="relative">
<slot />
<!-- Copy button -->
<button
title="Copy"
on:click={() => {
navigator.clipboard.writeText(elem.innerText);
}}
class="
absolute right-4 top-[min(50%_,_32px)] aspect-square -translate-y-1/2 transform
rounded hover:bg-neutral-800 hover:text-neutral-300 active:text-sky-300
"
>
<iconify-icon icon="octicon:copy-16" class="p-2 text-lg"></iconify-icon>
</button>
</div>

View file

@ -0,0 +1,9 @@
<div class="mt-12 w-full border-t border-neutral-800 text-end">
<span class="mt-6 block text-sm italic text-neutral-400"
>Handmade by <a
target="_blank"
class="hover:text-sky-300 hover:underline"
href="https://github.com/BearToCode">Davide</a
></span
>
</div>

View file

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

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { base } from '$app/paths';
export let href = '';
</script>
<a
{...$$restProps}
href={href.startsWith('/') ? `${base}${href}` : href}
on:click
on:focusin
on:focusout
on:mouseenter
on:mouseleave
>
<slot />
</a>

View file

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

View file

@ -0,0 +1,34 @@
<script lang="ts">
import Sidebar from '../sidebar/Sidebar.svelte';
import { page } from '$app/stores';
export { className as class };
let className = '';
let enabled = false;
$: {
$page.url;
enabled = false;
}
</script>
<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">
<iconify-icon icon="ci:hamburger-lg" class="text-3xl"></iconify-icon>
</button>
</div>
{#if enabled}
<div class="fixed bottom-0 left-0 right-0 top-0 z-10 bg-neutral-950 bg-opacity-50"></div>
<div class="fixed bottom-0 left-0 top-0 z-10 rounded-r-xl bg-neutral-900">
<Sidebar class="w-[20rem] px-4 py-4" />
<button
on:click={() => (enabled = false)}
class="absolute right-4 top-4 text-neutral-500 hover:text-neutral-200"
>
<iconify-icon icon="charm:cross" class="text-2xl"></iconify-icon>
</button>
</div>
{/if}

View file

@ -0,0 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte';
export { className as class };
let className = '';
let loading = true;
let stars: number;
onMount(async () => {
const res = await fetch('https://api.github.com/repos/BearToCode/carta');
const json = await res.json();
stars = json.stargazers_count;
loading = false;
});
</script>
<a
href="https://github.com/BearToCode/carta"
class="flex h-12 items-center space-x-2 p-2 {className}"
>
<iconify-icon icon="mdi:github" class="text-2xl"></iconify-icon>
<div class="hidden h-min flex-col justify-center space-y-1 md:flex">
<p class="text-[0.9rem] font-semibold leading-3">BearToCode/carta</p>
{#if loading}
<div class="pulse my-1.5 h-3 w-[80px] rounded-full bg-neutral-800" />
{:else}
<div class="inline-flex items-center space-x-1">
<iconify-icon icon="ic:round-star" class="h-3 w-3"></iconify-icon>
<span class="mt-1 text-[0.8rem] leading-3">{stars}</span>
</div>
{/if}
</div>
</a>

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { base } from '$app/paths';
import Link from '../link/Link.svelte';
import GitHub from './GitHub.svelte';
import Search from './Search.svelte';
</script>
<nav
class="fixed left-0 right-0 top-0 z-10 bg-neutral-900 bg-opacity-50 backdrop-blur-2xl backdrop-filter"
>
<div class="container mx-auto flex items-center justify-between px-4 py-1 sm:px-6">
<Link href="/">
<img src="{base}/logo.png" class="h-8" alt="carta logo" />
</Link>
<div class="flex-grow" />
<Search class="mx-auto" />
<div class="hidden flex-grow md:block" />
<GitHub class="ml-auto" />
</div>
<div
class="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-neutral-900 via-neutral-700 to-neutral-900"
/>
</nav>

View file

@ -0,0 +1,111 @@
<script lang="ts">
import * as Command from '$lib/components/ui/command';
import type { Document } from 'flexsearch';
import {
enrichResult,
initializeSearch,
type EnrichedSearchResult,
type SearchResult
} from '$lib/search';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
export { className as class };
let open = false;
let value = '';
let className = '';
let index: Document<SearchResult, true>;
let results: EnrichedSearchResult[] = [];
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
open = !open;
}
}
function search(query: string) {
if (!index || !query) {
results = [];
return;
}
const pages = new Map<string, SearchResult>();
const searchResult = index
.search(query, 5, { enrich: true })
.map((res) => res.result)
.flat();
for (const res of searchResult) {
pages.set(res.doc.path, enrichResult(res.doc, value));
}
results = Array.from(pages.values()).slice(0, 5);
}
onMount(async () => {
index = await initializeSearch();
});
$: search(value);
</script>
<svelte:window on:keydown={handleKeydown} />
<button on:click={() => (open = !open)} class="mr-2 block aspect-square md:hidden">
<iconify-icon icon="ion:search" class="text-2xl text-neutral-200"></iconify-icon>
</button>
<button
class="hidden w-[360px] items-center justify-between rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1.5 text-sm md:flex {className}"
on:click={() => (open = !open)}
>
<div class="inline-flex items-center space-x-2">
<iconify-icon icon="ion:search" class="text-xl text-neutral-500"></iconify-icon>
<span class="text-neutral-500">Search...</span>
</div>
<kbd
class="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100"
>
<span class="text-xs"></span>K
</kbd>
</button>
<Command.Dialog shouldFilter={false} bind:open>
<Command.Input bind:value placeholder="Search anything..." />
<Command.List>
{#if value}
<Command.Empty>No results found.</Command.Empty>
<Command.Group>
{#each results as result}
<Command.Item
onSelect={() => {
if (result.match?.heading) goto(`${base}/${result.path}#${result.match.heading.id}`);
else goto(`${base}/${result.path}`);
open = false;
}}
class="group"
value={result.title}
>
<h2 class="text-base font-medium">
{result.title}
{#if result.match?.heading}
<span class="text-neutral-400"> - {result.match.heading.text}</span>
{/if}
</h2>
{#if result.match}
<p class="line-clamp-1 pr-8 text-sm text-neutral-400">{result.match.text}</p>
{/if}
<div class="absolute right-2 top-1/2 hidden -translate-y-1/2 group-aria-selected:block">
<iconify-icon icon="mi:enter" class="text-xl text-neutral-400"></iconify-icon>
</div>
</Command.Item>
{/each}
</Command.Group>
{/if}
</Command.List>
</Command.Dialog>

View file

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

View file

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

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div class={cn('p-6 pt-0', className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type $$Props = HTMLAttributes<HTMLParagraphElement>;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<p class={cn('text-muted-foreground text-base', className)} {...$$restProps}>
<slot />
</p>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div class={cn('flex items-center p-6 pt-0', className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div class={cn('flex flex-col space-y-1.5 p-6', className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import type { HeadingLevel } from '.';
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;
};
let className: $$Props['class'] = undefined;
export let tag: $$Props['tag'] = 'h3';
export { className as class };
</script>
<svelte:element
this={tag}
class={cn('font-semibold leading-none tracking-tight', className)}
{...$$restProps}
>
<slot />
</svelte:element>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import Link from '$lib/components/link/Link.svelte';
import { cn } from '$lib/utils';
let className = '';
export { className as class };
export let href: string;
</script>
<Link
{href}
class={cn(
'bg-card text-card-foreground block rounded-xl border bg-opacity-30 shadow hover:border-sky-300',
className
)}
{...$$restProps}
on:click
on:focusin
on:focusout
on:mouseenter
on:mouseleave
>
<slot />
</Link>

View file

@ -0,0 +1,24 @@
import Root from './card.svelte';
import Content from './card-content.svelte';
import Description from './card-description.svelte';
import Footer from './card-footer.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';
export {
Root,
Content,
Description,
Footer,
Header,
Title,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle
};
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';

View file

@ -0,0 +1,23 @@
<script lang="ts">
import Command from './command.svelte';
import * as Dialog from '$lib/components/ui/dialog';
import type { Dialog as DialogPrimitive } from 'bits-ui';
import type { Command as CommandPrimitive } from 'cmdk-sv';
type $$Props = DialogPrimitive.Props & CommandPrimitive.CommandProps;
export let open: $$Props['open'] = false;
export let value: $$Props['value'] = undefined;
</script>
<Dialog.Root bind:open {...$$restProps}>
<Dialog.Content class="overflow-hidden p-0">
<Command
class="[&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[data-cmdk-input]]:h-12 [&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:h-5 [&_[data-cmdk-item]_svg]:w-5"
{...$$restProps}
bind:value
>
<slot />
</Command>
</Dialog.Content>
</Dialog.Root>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils';
// type $$Props = CommandPrimitive.EmptyProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Empty class={cn('py-6 text-center text-sm', className)} {...$$restProps}>
<slot />
</CommandPrimitive.Empty>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils';
// type $$Props = CommandPrimitive.GroupProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Group
class={cn(
'text-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-medium',
className
)}
{...$$restProps}
>
<slot />
</CommandPrimitive.Group>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils';
// type $$Props = CommandPrimitive.InputProps;
let className: string | undefined | null = undefined;
export let value = '';
export { className as class };
</script>
<div class="flex items-center border-b px-3" data-cmdk-input-wrapper="">
<iconify-icon icon="ion:search" class="mr-2 shrink-0 text-xl opacity-50"></iconify-icon>
<CommandPrimitive.Input
bind:value
class={cn(
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...$$restProps}
/>
</div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils';
// type $$Props = CommandPrimitive.ItemProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Item
class={cn(
'aria-selected:bg-accent aria-selected:text-accent-foreground relative cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...$$restProps}
>
<slot />
</CommandPrimitive.Item>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils';
// type $$Props = CommandPrimitive.ListProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.List
class={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...$$restProps}
>
<slot />
</CommandPrimitive.List>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils';
// type $$Props = CommandPrimitive.SeparatorProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Separator class={cn('bg-border -mx-1 h-px', className)} {...$$restProps} />

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { cn } from '$lib/utils';
// type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<span
class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
{...$$restProps}
>
<slot />
</span>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv';
import { cn } from '$lib/utils';
type $$Props = CommandPrimitive.CommandProps;
export let value: $$Props['value'] = undefined;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Root
class={cn(
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
className
)}
bind:value
{...$$restProps}
>
<slot />
</CommandPrimitive.Root>

View file

@ -0,0 +1,37 @@
import { Command as CommandPrimitive } from 'cmdk-sv';
import Root from './command.svelte';
import Dialog from './command-dialog.svelte';
import Empty from './command-empty.svelte';
import Group from './command-group.svelte';
import Item from './command-item.svelte';
import Input from './command-input.svelte';
import List from './command-list.svelte';
import Separator from './command-separator.svelte';
import Shortcut from './command-shortcut.svelte';
const Loading = CommandPrimitive.Loading;
export {
Root,
Dialog,
Empty,
Group,
Item,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading
};

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import * as Dialog from '.';
import { cn, flyAndScale } from '$lib/utils';
type $$Props = DialogPrimitive.ContentProps;
let className: $$Props['class'] = undefined;
export let transition: $$Props['transition'] = flyAndScale;
export let transitionConfig: $$Props['transitionConfig'] = {
duration: 200
};
export { className as class };
</script>
<Dialog.Portal>
<Dialog.Overlay />
<DialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
'bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full',
className
)}
{...$$restProps}
>
<slot />
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-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"
>
<iconify-icon icon="basil:cross-solid" class="text-2xl"></iconify-icon>
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
type $$Props = DialogPrimitive.DescriptionProps;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<DialogPrimitive.Description
class={cn('text-muted-foreground text-sm', className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Description>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...$$restProps}
>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { cn } from '$lib/utils';
import type { HTMLAttributes } from 'svelte/elements';
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<div class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { fade } from 'svelte/transition';
type $$Props = DialogPrimitive.OverlayProps;
let className: $$Props['class'] = undefined;
export let transition: $$Props['transition'] = fade;
export let transitionConfig: $$Props['transitionConfig'] = {
duration: 150
};
export { className as class };
</script>
<DialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn('bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ', className)}
{...$$restProps}
/>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
// type $$Props = DialogPrimitive.PortalProps;
</script>
<DialogPrimitive.Portal {...$$restProps}>
<slot />
</DialogPrimitive.Portal>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
type $$Props = DialogPrimitive.TitleProps;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<DialogPrimitive.Title
class={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...$$restProps}
>
<slot />
</DialogPrimitive.Title>

View file

@ -0,0 +1,34 @@
import { Dialog as DialogPrimitive } from 'bits-ui';
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
import Title from './dialog-title.svelte';
import Portal from './dialog-portal.svelte';
import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte';
import Content from './dialog-content.svelte';
import Description from './dialog-description.svelte';
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription
};

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { Carta, MarkdownEditor } from 'carta-md';
import { emoji } from '@cartamd/plugin-emoji';
import { code } from '@cartamd/plugin-code';
import PlusCircled from './assets/PlusIcon.svelte';
import '$lib/styles/discord.scss';
const carta = new Carta({
sanitizer: false,
disableIcons: true,
extensions: [
emoji(),
code(),
{
components: [
{
component: PlusCircled,
parent: 'input',
props: {}
}
]
}
]
});
</script>
<MarkdownEditor placeholder="Send a message to @someone" mode="tabs" theme="discord" {carta} />

View file

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

View file

@ -0,0 +1,39 @@
<script lang="ts">
import { Carta, MarkdownEditor, Markdown } from 'carta-md';
import placeholder from './math-stack-exchange-placeholder.tex?raw';
import { math } from '@cartamd/plugin-math';
import { tikz } from '@cartamd/plugin-tikz';
import '$lib/styles/math-stack-exchange.scss';
import 'katex/dist/katex.min.css';
const carta = new Carta({
sanitizer: false,
extensions: [
math(),
tikz({
postProcessing: (html) => {
// Simple dark mode support
return html
.replaceAll('#000000', '~~~')
.replaceAll('#000', '~~~')
.replaceAll('black', '~~~')
.replaceAll('#ffffff', '#000')
.replaceAll('#fff', '#000')
.replaceAll('white', '#000')
.replaceAll('~~~', '#fff');
}
})
]
});
export let value = placeholder;
</script>
<div class="math-stack-exchange-container">
<MarkdownEditor bind:value mode="tabs" theme="math-stack-exchange" {carta} />
{#key value}
<Markdown theme="math-stack-exchange" {value} {carta} />
{/key}
</div>

View file

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

View file

@ -0,0 +1,23 @@
> Here is a formula:
$$
\dfrac{\partial}{\partial t}(
\dfrac{\partial \mathcal{L}}{\partial \dot{q}_k}
) - \dfrac{\partial \mathcal{L}}{\partial q_k} = 0
$$
> And here is a circuit:
```tikz
\usepackage{circuitikz}
\begin{document}
\begin{circuitikz} \draw
(0,0) to[battery] (0,4)
to[ammeter] (4,4) -- (4,0)
to[lamp] (0,0)
;
\end{circuitikz}
\end{document}
```

View file

@ -0,0 +1,96 @@
import flexsearch from 'flexsearch';
import type { SvelteComponent } from 'svelte';
export interface SearchResult {
path: string;
content: string;
html: string;
title: string;
}
export type EnrichedSearchResult = SearchResult & {
match?: {
heading?: {
text: string;
id: string;
};
text: string;
};
};
export async function initializeSearch() {
const indexedPages = new flexsearch.Document<SearchResult, true>({
tokenize: 'full',
cache: true,
context: true,
document: {
id: 'path',
index: ['title', 'content'],
store: true
}
});
const pages = import.meta.glob('../../pages/**/*.svelte.md');
await Promise.all(
Object.keys(pages).map(async (page) => {
const module = (await pages[page]()) as {
default: typeof SvelteComponent;
metadata: Record<string, unknown>;
};
const elem = document.createElement('div');
new module.default({ target: elem });
const path = page.replace('../../pages/', '').replace('.svelte.md', '');
const title = module.metadata.title as string;
const html = elem.innerHTML;
const content = extractText(module.default);
indexedPages.add({
path,
html,
title,
content
});
})
);
return indexedPages;
}
function extractText(component: typeof SvelteComponent) {
const parentElem = document.createElement('div');
new component({ target: parentElem });
const text = parentElem.textContent ?? '';
// Remove extra spaces
return text.replace(/\s+/g, ' ').trim();
}
export function enrichResult(result: SearchResult, query: string): EnrichedSearchResult {
let heading: HTMLHeadingElement | null = null;
const parentElem = document.createElement('div');
parentElem.innerHTML = result.html;
for (const node of parentElem.childNodes) {
const content = node.textContent?.replaceAll('\n', '').replace(/\s+/g, ' ').trim() ?? '';
if (['h1', 'h2', 'h3'].includes(node.nodeName.toLowerCase())) {
heading = node as HTMLHeadingElement;
}
if (content?.toLowerCase().includes(query.toLowerCase())) {
return {
...result,
match: {
heading: heading
? {
text: heading.textContent ?? '',
id: heading.id
}
: undefined,
text: content
}
};
}
}
return result;
}

View file

@ -0,0 +1,317 @@
/**
* Coldark Theme for Prism.js
* Theme variation: Dark
* Tested with HTML, CSS, JS, JSON, PHP, YAML, Bash script
* @author Armand Philippot <contact@armandphilippot.com>
* @homepage https://github.com/ArmandPhilippot/coldark-prism
* @license MIT
*/
code[class*='language-'],
pre[class*='language-'] {
color: #e3eaf2;
background: none;
font-family: 'Fira Code', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
background: #3c526d;
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
background: #3c526d;
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
@apply rounded-md border border-neutral-800 bg-neutral-950 bg-opacity-50;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em 0.3em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #8da1b9;
}
.token.punctuation {
color: #e3eaf2;
}
.token.delimiter.important,
.token.selector .parent,
.token.tag,
.token.tag .token.punctuation {
color: #66cccc;
}
.token.attr-name,
.token.boolean,
.token.boolean.important,
.token.number,
.token.constant,
.token.selector .token.attribute {
color: #e6d37a;
}
.token.class-name,
.token.key,
.token.parameter,
.token.property,
.token.property-access,
.token.variable {
color: #6cb8e6;
}
.token.attr-value,
.token.inserted,
.token.color,
.token.selector .token.value,
.token.string,
.token.string .token.url-link {
color: #91d076;
}
.token.builtin,
.token.keyword-array,
.token.package,
.token.regex {
color: #f4adf4;
}
.token.function,
.token.selector .token.class,
.token.selector .token.id {
color: #c699e3;
}
.token.atrule .token.rule,
.token.combinator,
.token.keyword,
.token.operator,
.token.pseudo-class,
.token.pseudo-element,
.token.selector,
.token.unit {
color: #e9ae7e;
}
.token.deleted,
.token.important {
color: #cd6660;
}
.token.keyword-this,
.token.this {
color: #6cb8e6;
}
.token.important,
.token.keyword-this,
.token.this,
.token.bold {
font-weight: bold;
}
.token.delimiter.important {
font-weight: inherit;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.language-markdown .token.title,
.language-markdown .token.title .token.punctuation {
color: #6cb8e6;
font-weight: bold;
}
.language-markdown .token.blockquote.punctuation {
color: #f4adf4;
}
.language-markdown .token.code {
color: #66cccc;
}
.language-markdown .token.hr.punctuation {
color: #6cb8e6;
}
.language-markdown .token.url .token.content {
color: #91d076;
}
.language-markdown .token.url-link {
color: #e6d37a;
}
.language-markdown .token.list.punctuation {
color: #f4adf4;
}
.language-markdown .token.table-header {
color: #e3eaf2;
}
.language-json .token.operator {
color: #e3eaf2;
}
.language-scss .token.variable {
color: #66cccc;
}
/* overrides color-values for the Show Invisibles plugin
* https://prismjs.com/plugins/show-invisibles/
*/
.token.token.tab:not(:empty):before,
.token.token.cr:before,
.token.token.lf:before,
.token.token.space:before {
color: #8da1b9;
}
/* overrides color-values for the Toolbar plugin
* https://prismjs.com/plugins/toolbar/
*/
div.code-toolbar > .toolbar.toolbar > .toolbar-item > a,
div.code-toolbar > .toolbar.toolbar > .toolbar-item > button {
color: #111b27;
background: #6cb8e6;
}
div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover,
div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus,
div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover,
div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus {
color: #111b27;
background: #6cb8e6da;
text-decoration: none;
}
div.code-toolbar > .toolbar.toolbar > .toolbar-item > span,
div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover,
div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus {
color: #111b27;
background: #8da1b9;
}
/* overrides color-values for the Line Highlight plugin
* http://prismjs.com/plugins/line-highlight/
*/
.line-highlight.line-highlight {
background: #3c526d5f;
background: linear-gradient(to right, #3c526d5f 70%, #3c526d55);
}
.line-highlight.line-highlight:before,
.line-highlight.line-highlight[data-end]:after {
background-color: #8da1b9;
color: #111b27;
box-shadow: 0 1px #3c526d;
}
pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before {
background-color: #8da1b918;
}
/* overrides color-values for the Line Numbers plugin
* http://prismjs.com/plugins/line-numbers/
*/
.line-numbers.line-numbers .line-numbers-rows {
border-right: 1px solid #0b121b;
background: #0b121b7a;
}
.line-numbers .line-numbers-rows > span:before {
color: #8da1b9da;
}
/* overrides color-values for the Match Braces plugin
* https://prismjs.com/plugins/match-braces/
*/
.rainbow-braces .token.token.punctuation.brace-level-1,
.rainbow-braces .token.token.punctuation.brace-level-5,
.rainbow-braces .token.token.punctuation.brace-level-9 {
color: #e6d37a;
}
.rainbow-braces .token.token.punctuation.brace-level-2,
.rainbow-braces .token.token.punctuation.brace-level-6,
.rainbow-braces .token.token.punctuation.brace-level-10 {
color: #f4adf4;
}
.rainbow-braces .token.token.punctuation.brace-level-3,
.rainbow-braces .token.token.punctuation.brace-level-7,
.rainbow-braces .token.token.punctuation.brace-level-11 {
color: #6cb8e6;
}
.rainbow-braces .token.token.punctuation.brace-level-4,
.rainbow-braces .token.token.punctuation.brace-level-8,
.rainbow-braces .token.token.punctuation.brace-level-12 {
color: #c699e3;
}
/* overrides color-values for the Diff Highlight plugin
* https://prismjs.com/plugins/diff-highlight/
*/
pre.diff-highlight > code .token.token.deleted:not(.prefix),
pre > code.diff-highlight .token.token.deleted:not(.prefix) {
background-color: #cd66601f;
}
pre.diff-highlight > code .token.token.inserted:not(.prefix),
pre > code.diff-highlight .token.token.inserted:not(.prefix) {
background-color: #91d0761f;
}
/* overrides color-values for the Command Line plugin
* https://prismjs.com/plugins/command-line/
*/
.command-line .command-line-prompt {
border-right: 1px solid #0b121b;
}
.command-line .command-line-prompt > span:before {
color: #8da1b9da;
}

View file

@ -0,0 +1,83 @@
.carta-theme__discord {
// Core styles
$background: #2f3136;
$background-light: #161b22;
$background-contrast: #46484b;
&.carta-editor {
background-color: $background;
border-radius: 0.5rem;
::placeholder {
opacity: 0.5;
}
.carta-wrapper {
padding: 1rem;
padding-left: 56px;
flex-grow: 1;
overflow-y: auto;
.carta-container {
min-height: 32px;
max-height: 400px;
}
}
.carta-font-code {
font-family: 'Fira Code', monospace;
caret-color: white;
font-size: 1.1rem;
}
.carta-toolbar {
display: none;
}
.discord-plus-icon {
position: absolute;
top: 15px;
left: -24px;
width: 1.75rem;
height: 1.75rem;
transform: translateX(-50%) translateY(-50%);
}
}
// Plugin emoji
&.carta-emoji {
width: 18rem;
max-height: 14rem;
overflow-y: scroll;
border-radius: 4px;
font-family: inherit;
background-color: $background;
padding: 6px;
scroll-padding: 6px;
gap: 6px;
}
&.carta-emoji button {
background: $background-light;
aspect-ratio: 1;
border-radius: 4px;
border: 0;
padding: 0;
cursor: pointer;
font-size: 1.2rem;
line-height: 100%;
}
&.carta-emoji button:hover,
&.carta-emoji button.carta-active {
background: $background-contrast;
}
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}

View file

@ -0,0 +1,203 @@
.carta-theme__github {
// Core styles
$background: #0d1117;
$background-light: #161b22;
$border: #2b3138;
$accent: #1f6feb;
&.carta-editor {
background-color: $background;
border: 1px solid $border;
border-radius: 0.5rem;
&:focus-within {
outline: 2px solid $accent;
}
.carta-wrapper {
padding: 1rem;
flex-grow: 1;
overflow-y: auto;
.carta-container {
min-height: 120px;
max-height: 160px;
}
}
.carta-font-code {
font-family: 'Fira Code', monospace;
caret-color: white;
font-size: 1.1rem;
}
.carta-toolbar {
height: 2.5rem;
background-color: $background-light;
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
.carta-icon {
width: 2rem;
height: 2rem;
&:hover {
color: white;
background-color: $border;
}
}
}
.carta-toolbar-left button,
.carta-toolbar-right,
.carta-filler {
border-bottom: 1px solid $border;
}
.carta-toolbar-left {
& > *:first-child {
border-top-left-radius: 0.5rem;
}
& > * {
padding-left: 1rem;
padding-right: 1rem;
font-size: 0.95rem;
}
button {
height: 100%;
}
.carta-active {
background-color: $background;
color: white;
border-right: 1px solid $border;
border-bottom: 1px solid $background;
&:not(:first-child) {
border-left: 1px solid $border;
}
}
}
.carta-toolbar-right {
padding-right: 12px;
}
.carta-icons-menu {
padding: 8px;
border: 1px solid $border;
border-radius: 6px;
min-width: 180px;
background: $background;
.carta-icon-full {
padding-left: 6px;
padding-right: 6px;
margin-top: 2px;
&:first-child {
margin-top: 0;
}
&:hover {
color: white;
background-color: $border;
}
span {
margin-left: 6px;
color: white;
font-size: 0.85rem;
}
}
}
}
// Plugin emoji
&.carta-emoji {
width: 18rem;
max-height: 14rem;
overflow-y: scroll;
border-radius: 4px;
font-family: inherit;
background-color: $background;
padding: 6px;
scroll-padding: 6px;
gap: 6px;
}
&.carta-emoji button {
background: $background-light;
aspect-ratio: 1;
border-radius: 4px;
border: 0;
padding: 0;
cursor: pointer;
font-size: 1.2rem;
line-height: 100%;
}
&.carta-emoji button:hover,
&.carta-emoji button.carta-active {
background: $border;
}
// Plugin slash
&.carta-slash {
width: 18rem;
max-height: 14rem;
overflow-y: scroll;
border-radius: 4px;
font-family: inherit;
background-color: $background;
padding: 6px;
scroll-padding: 6px;
}
&.carta-slash span {
width: fit-content;
}
&.carta-slash button {
background: none;
width: 100%;
padding: 10px;
border: 0;
border-radius: 4px;
}
&.carta-slash .carta-slash-group {
padding: 0 4px 0 4px;
margin-bottom: 4px;
font-size: 0.8rem;
}
&.carta-slash button.carta-active,
&.carta-slash button:hover {
background: $background-light;
cursor: pointer;
}
&.carta-slash .carta-snippet-title {
font-size: 0.85rem;
font-weight: 600;
}
&.carta-slash .carta-snippet-description {
font-size: 0.8rem;
text-overflow: ellipsis;
}
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}

View file

@ -0,0 +1,63 @@
.markdown {
& {
@apply text-[1.05rem] leading-7 text-neutral-400;
}
h1 {
@apply mb-4 mt-8 text-4xl font-bold text-white first:mt-0;
}
h2 {
@apply mb-4 mt-8 text-3xl font-semibold text-white first:mt-0;
}
h3 {
@apply mb-3 mt-7 text-2xl font-medium text-white first:mt-0;
}
h4 {
@apply mb-3 mt-3 text-lg font-medium text-white first:mt-0;
}
h1,
h2,
h3,
h4 {
@apply scroll-mt-16;
}
blockquote {
@apply mb-3 text-lg italic text-neutral-300;
}
li {
@apply ml-4 list-disc;
}
strong {
@apply text-neutral-300;
}
a:not(h1 > a, h2 > a, h3 > a, h4 > a, .bg-card) {
@apply text-sky-300 underline;
}
code {
@apply rounded bg-neutral-800 px-1 text-neutral-50;
}
p,
h1,
h2,
h3 {
code {
@apply px-1;
}
}
.carta-editor code {
font-family: 'Fira Code', monospace;
background: transparent;
padding: 0;
}
}

View file

@ -0,0 +1,108 @@
.math-stack-exchange-container {
background-color: #252526;
border-radius: 0.25rem;
}
.carta-theme__math-stack-exchange {
// Core styles
$background: #252526;
$background-light: #333333;
$border: #333333;
$accent: #2e8acb;
&.carta-editor {
background-color: $background;
border: 1px solid $border;
border-radius: 0.25rem;
.carta-wrapper {
padding: 1rem;
flex-grow: 1;
overflow-y: auto;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
&:focus-within {
outline: 3px solid rgba($accent, 0.5);
}
.carta-container {
height: 200px;
}
}
.carta-font-code {
font-family: 'Fira Code', monospace;
caret-color: white;
font-size: 1.1rem;
}
.carta-toolbar {
height: 2.5rem;
background-color: $background-light;
border-bottom: 1px solid $border;
padding-right: 12px;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
.carta-icon {
width: 2rem;
height: 2rem;
&:hover {
color: white;
background-color: $border;
}
}
}
.carta-toolbar-left {
display: none;
}
.carta-toolbar-right {
justify-content: flex-start;
}
}
.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;
}
}
}
&.carta-viewer {
min-height: 60px;
padding: 1rem;
}
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
}

94
docs/src/lib/utils.ts Normal file
View file

@ -0,0 +1,94 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, '');
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};
export const throttle = <R, A extends unknown[]>(
fn: (...args: A) => R,
delay: number
): [(...args: A) => R | undefined, () => void] => {
let wait = false;
let timeout: undefined | number;
let cancelled = false;
return [
(...args: A) => {
if (cancelled) return undefined;
if (wait) return undefined;
const val = fn(...args);
wait = true;
timeout = window.setTimeout(() => {
wait = false;
}, delay);
return val;
},
() => {
cancelled = true;
clearTimeout(timeout);
}
];
};
export function debounce<T extends unknown[]>(cb: (...args: T) => unknown, wait = 1000) {
let timeout: NodeJS.Timeout;
return (...args: T) => {
clearTimeout(timeout);
timeout = setTimeout(() => cb(...args), wait);
};
}

View file

@ -0,0 +1,173 @@
---
section: API
title: Core
---
<script>
import { base } from '$app/paths';
</script>
# `Carta` options
List of options that can be used when creating `Carta`:
```ts
new Carta({
/* ... */
});
```
### `gfmOptions`
Type: `GfmOptions`
GitHub Flavored Markdown options.
### `extensions`
Type: `Extension[]`
List of extensions(plugins) to use.
### `rendererDebounce`
Type: `number`
Rendering debouncing timeout, in milliseconds.
Defaults to 300ms.
### `disableShortcuts`
Type: `DefaultShortcutId[] | true`
Remove default shortcuts by id. You can use `true` to disable all of them.
### `disableIcons`
Type: `DefaultIconId[] | true`
Remove default icons by id. You can use `true` to disable all of them.
### `disablePrefixes`
Type: `DefaultPrefixId[] | true`
Remove default prefixes by id. You can use `true` to disable all of them.
### `historyOptions`
History management options.
#### `historyOptions.minInterval`
Type: `number`
Minimum interval between save states in milliseconds.
Defaults to 300ms.
#### `historyOptions.maxSize`
Type: `number`
Maximum history size in bytes.
Defaults to 1MB.
### `sanitizer`
Type: `(html: string) => void`
HTML sanitizer. See [here]({base}/getting-started#sanitization) for more details.
### `shikiOptions`
Type: `ShikiOptions`
Highlighter(Shiki) options.
### `theme`
Type: `Theme | DualTheme`
Shiki theme to use to highlight Markdown.
# `MarkdownEditor` options
List of options that can be used in the `<MarkdownEditor>` component.
### `carta`
Type: `Carta`
Carta manager to use for this editor.
### `theme`
Type: `string`
The theme of this editor. The editor and related elements will have the `carta-theme__<theme>` as a class.
### `value`
Type: `string`
Current Markdown input value.
### `mode`
Type: `'tabs' | 'split' | 'auto'`
Editor windows mode. With `auto` it will split when the window size is greater than 768px.
### `scroll`
Type: `'sync' | 'async'`
Scroll synchronization.
### `disableToolbar`
Type: `boolean`
Option to disable the toolbar.
### `placeholder`
Type: `string`
Set the textarea placeholder.
### `textarea`
Type: `TextAreaProps` (extends `Record<string, unknown>`)
Additional properties that will be used in the textarea used under the hood in the editor.
`class`, `placeholder` and `value` are not allowed. Use the corresponding editor properties
instead.
### `labels`
Type: `Partial<Labels>`
Can be used to provide custom text for labels in the editor.
# `Markdown` options
List of options that can be used in the `<Markdown>` component.
### `carta`
Type: `Carta`
Carta manager to use for this editor.
### `theme`
Type: `string`
The theme of this editor. The viewer and related elements will have the `carta-theme__<theme>` as a class.
### `value`
Type: `string`
Current Markdown input value.

View file

@ -0,0 +1,263 @@
---
section: API
title: Extension
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
# `Plugin` properties
You can easily extend Carta by creating custom plugins.
<Code>
```ts
const ext: Plugin = {
// ...
};
const carta = new Carta({
extensions: [ext]
});
```
</Code>
Here are all the `Plugin` properties:
### `transformers`
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`
Type: `KeyboardShortcut[]`
Additional keyboards shortcut. For example:
<Code>
```ts
const shortcut: KeyboardShortcut = {
id: 'bold',
combination: new Set(['control', 'b']),
action: (input) => input.toggleSelectionSurrounding('**')
};
```
</Code>
#### `KeyboardShortcut.id`
Type: `string`
Id of the shortcut.
#### `KeyboardShortcut.combination`
Type: `Set<string>`
Set of keys, corresponding to the `e.key` of `KeyboardEvent`s, but lowercase.
#### `KeyboardShortcut.action`
Type: `(input: InputEnhancer) => void`
Shortcut callback.
#### `KeyboardShortcut.preventSave`
Prevent saving the current state in history.
### `icons`
Type: `Icon[]`
Additional toolbar icons. For example:
<Code>
```ts
const icon: Icon = {
id: 'heading',
action: (input) => input.toggleLinePrefix('###'),
component: HeadingIcon
};
```
</Code>
#### `Icon.id`
Type: `string`
Id of the icon.
#### `Icon.action`
Type: `(input: InputEnhancer) => void`
Click callback.
#### `Icon.component`
Type: `ComponentType` (SvelteComponent)
The Icon as a Svelte component.
### `prefixes`
Type: `Prefix[]`
Text prefixes, default ones include the `- ` for bulleted lists, `1. ` for numbered lists, `- [ ]` for task lists.
<Code>
```ts
const prefix: Prefix = {
id: 'bulletedList',
match: (line) => {
const prefix = line.slice(0, 2);
if (prefix === '- ') return prefix;
},
maker: () => '- '
};
```
</Code>
#### `Prefix.id`
Type: `string`
Id of the prefix.
#### `Prefix.match`
Type: `(line: string) => string | undefined`
Function that returns the prefix, if it is present.
#### `Prefix.maker`
Type: `(previousMatch: string, previousLine: string) => string`
Function that returns the prefix for the new line.
Example:
<Code>
```ts
const prefix: Prefix = {
id: 'numberedList',
match: (line) => line.match(/^\d+\./)?.at(0),
maker: (prev) => `${Number(prev.slice(0, -1)) + 1}. `
};
```
</Code>
### `listeners`
Type: `Listener[]`
Textarea event listeners. Has an additional `carta-render` and `carta-render-ssr` events keys.
<Code>
```ts
const click: Listener = ['click', () => console.log('I was clicked!')];
const render: Listener = [
'carta-render',
(e) => {
const carta = e.detail.carta;
// ...
},
{
once: true
}
];
```
</Code>
### `components`
Type: `ExtensionComponent[]`
Additional components to be added to the editor or viewer.
#### `ExtensionComponent<T>.component`
Type: `typeof SvelteComponentTyped<T & { carta: Carta }>`
Svelte components that exports `carta: Carta` and all the other properties specified as the generic parameter and in `props`.
#### `ExtensionComponent<T>.props`
Type: `T`
Properties that will be handed to the component.
#### `ExtensionComponent<T>.parent`
Type: `MaybeArray<'editor' | 'input' | 'renderer' | 'preview'>`
Where the element will be placed.
### `grammarRules`
Type: `GrammarRule[]`
Custom Markdown TextMate grammar rules for Shiki. They will be injected into the language.
### `highlightRules`
Type: `HighlightingRule[]`
Custom highlighting rules for ShiKi. They will be injected into the selected theme.
### `onLoad`
Type: `(data: { carta: Carta; highlight: HighlightFunctions }) => void`
Use this callback to execute code when one Carta instance loads the extension.

View file

@ -0,0 +1,91 @@
---
section: API
title: Utilities
---
## `Carta.render`
Allows you to render Markdown asynchronously.
```ts
const carta = new Carta({
/* ... */
});
const markdown = '# Some Markdown';
const html = await carta.render(markdown);
```
## `Carta.renderSSR`
Allows you to render Markdown synchronously, suitable for Server Side Rendering. Note that particular extensions that add content asynchronously will not work in this configuration.
```ts
const carta = new Carta({
/* ... */
});
const markdown = '# Some Markdown';
const html = carta.renderSSR(markdown);
```
## `Carta.bindToCaret`
Svelte action that allows you to bind a specific element to the caret position. Used, for example, in `plugin-emoji` and `plugin-slash`.
```svelte
<script>
export let carta;
</script>
<div use:carta.bindToCaret>
<!-- ... -->
</div>
```
## `Carta.highlighter`
Get the Shiki highlighter.
```ts
const highlighter = await carta.highlighter();
const userTheme = carta.theme;
```
Here are some other highlight related utilities:
### `isBundleLanguage`
Checks if a language is a bundled language.
```ts
export const isBundleLanguage = (lang: string): lang is BundledLanguage;
```
### `isBundleTheme`
Checks if a theme is a bundled theme.
```ts
export const isBundleTheme = (theme: string): theme is BundledTheme;
```
### `isDualTheme`
Checks if a theme is a dual theme.
```ts
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme;
```
### `isSingleTheme`
```ts
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme;
```
### `isThemeRegistration`
Checks if a theme is a theme registration.
```ts
export const isThemeRegistration = (theme: Theme): theme is ThemeRegistration;
```

View file

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

View file

@ -0,0 +1,111 @@
---
title: Editing Styles
section: Overview
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
## Customizing editor styles
While the core styles are embedded in the Svelte components, the others can be set in a custom stylesheet. Here is what the final rendered HTML looks like.
<Code>
```html
<div class="carta-editor carta-theme__<theme>">
<div class="carta-toolbar">
<div class="carta-toolbar-left">
<!-- ... -->
</div>
<div class="carta-toolbar-right">
<button class="carta-icon"><!-- ... --></button>
<!-- Other icons -->
</div>
</div>
<div class="carta-wrapper">
<div class="carta-container mode-<split|tabs>">
<div class="carta-input-wrapper">
<pre class="carta-font-code"><!-- ... --></pre>
<textarea class="carta-font-code" id="md" />
</div>
<div class="carta-renderer">
<!-- Rendered Markdown -->
</div>
</div>
</div>
</div>
```
</Code>
### Using multiple themes
By using the `theme` property in `<MarkdownEditor>` you can differentiate the themes of multiple editors.
## Dark mode
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 is converted into standard HTML, so you can edit the final styles by using standard CSS rules. If you do not wish to create one from the ground up, you can use some already complete stylesheets, like [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) or [Tailwind Typography](https://tailwindcss.com/docs/typography-plugin).

View file

@ -0,0 +1,33 @@
---
title: Examples
section: Overview
---
<script>
import GitHubExample from '$lib/examples/GitHubExample.svelte';
import DiscordExample from '$lib/examples/DiscordExample.svelte';
import MathStackExchangeExample from '$lib/examples/MathStackExchangeExample.svelte';
</script>
Here is a list of examples made using Carta and some of its plugins.
## GitHub
<GitHubExample />
<br>
Plugins used: code, slash, emoji, attachment. View source on [GitHub](https://github.com/BearToCode/carta/blob/master/docs/src/lib/examples/GitHubExample.svelte).
## Discord
<DiscordExample />
<br>
Plugins used: code, emoji. View source on [GitHub](https://github.com/BearToCode/carta/blob/master/docs/src/lib/examples/DiscordExample.svelte).
## Math Stack Exchange
<MathStackExchangeExample />
<br>
Plugins used: math, tikz. View source on [GitHub](https://github.com/BearToCode/carta/blob/master/docs/src/lib/examples/MathStackExchangeExample.svelte).

View file

@ -0,0 +1,108 @@
---
title: Getting Started
section: Overview
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
## Installation
Installing the core package:
<Code>
```
npm i carta-md
```
</Code>
Installing plugins:
<Code>
```
npm i @cartamd/plugin-name
```
</Code>
## Setup
Setup a basic editor:
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import 'carta-md/default.css'; /* Default theme */
const carta = new Carta({
// Remember to use a sanitizer to prevent XSS attacks!
// More on that below
// sanitizer: ...
});
let value = '';
</script>
<MarkdownEditor {carta} bind:value />
<style>
/* Set your custom monospace font */
:global(.carta-font-code) {
font-family: '...', monospace;
font-size: 1.1rem;
}
</style>
```
</Code>
Or, if you just want to render content:
<Code>
```svelte
<script>
import { Carta, Markdown } from 'carta-md';
const carta = new Carta({
/* ... */
});
let value = '...';
</script>
<Markdown {carta} {value} />
```
</Code>
## Sanitization
By default Carta does **not** sanitize user input, which can include malicious code that could lead to [XSS attacks](https://en.wikipedia.org/wiki/Cross-site_scripting). For this reason it is _strongly recommended_ to install a package that handles that for you.
Since Carta operates both on the server and the client, you'd need a sanitizer able to work in both environments, for example [isomorphic-dompurify](https://www.npmjs.com/package/isomorphic-dompurify) or [sanitize-html](https://www.npmjs.com/package/sanitize-html). Here is an example using the former, which requires minimum configuration.
<Code>
```svelte
<script>
// Your other stuff...
import DOMPurify from 'isomorphic-dompurify';
const carta = new Carta({
sanitizer: DOMPurify.sanitize
});
let value = '';
</script>
<MarkdownEditor {carta} bind:value />
```
</Code>

View file

@ -0,0 +1,130 @@
---
title: Carta
section: Overview
---
<script>
import * as Card from "$lib/components/ui/card";
</script>
> Modern, lightweight, powerful Markdown Editor.
Carta is a lightweight, fast and extensible Svelte Markdown editor and viewer, designed for flexibility. It works natively in SvelteKit, and supports Server Side Rendering.
## Features
- 🌈 Markdown syntax highlighting ([Shiki](https://shiki.style/));
- 🛠️ Toolbar (extensible);
- ⌨️ Keyboard **shortcuts** (extensible);
- 📦 Supports **[+150 plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins)** thanks to remark.
- 🔀 Scroll sync;
- ✅ Accessibility friendly;
- 🖥️ **SSR** compatible;
## Official Plugins
Carta comes with a set of official plugins for the most common use cases.
<br />
<div class="w-full grid sm:grid-cols-2 gap-4">
<Card.Root href="/plugins/math">
<Card.Header>
<iconify-icon icon="tabler:math" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Math</Card.Title>
<Card.Description>Support for KaTex expressions.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/plugins/code">
<Card.Header>
<iconify-icon icon="fluent:code-16-filled" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Code</Card.Title>
<Card.Description>Code blocks syntax highlighting.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/plugins/emoji">
<Card.Header>
<iconify-icon icon="mingcute:emoji-line" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Emoji</Card.Title>
<Card.Description>Embed emojis in Markdown.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/plugins/slash">
<Card.Header>
<iconify-icon icon="tabler:slash" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Slash</Card.Title>
<Card.Description>Support for slash commands.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/plugins/tikz">
<Card.Header>
<iconify-icon icon="mdi:draw-pen" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>TikZ</Card.Title>
<Card.Description>Support for TikZ/PgfPlots diagrams.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/plugins/attachment">
<Card.Header>
<iconify-icon icon="tdesign:attach" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Attachment</Card.Title>
<Card.Description>Handle text attachments.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/plugins/anchor">
<Card.Header>
<iconify-icon icon="mingcute:link-fill" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Anchor</Card.Title>
<Card.Description>Add anchor links to headings.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/community-plugins">
<Card.Header>
<iconify-icon icon="ph:stack-fill" class="text-3xl text-sky-300"></iconify-icon>
<Card.Title>Community Plugins</Card.Title>
<Card.Description>Explore plugins from the community.</Card.Description>
</Card.Header>
</Card.Root>
</div>
## Examples
A list of examples inspired by popular platforms.
<br>
<div class="w-full grid sm:grid-cols-2 gap-4">
<Card.Root href="/examples#github">
<Card.Header>
<iconify-icon icon="mdi:github" class="text-3xl text-sky-300" ></iconify-icon>
<Card.Title>GitHub</Card.Title>
<Card.Description>Inspired by GitHub.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/examples#discord">
<Card.Header>
<iconify-icon icon="ic:baseline-discord" class="text-3xl text-sky-300" ></iconify-icon>
<Card.Title>Discord</Card.Title>
<Card.Description>Inspired by Discord.</Card.Description>
</Card.Header>
</Card.Root>
<Card.Root href="/examples#math-stack-exchange">
<Card.Header>
<iconify-icon icon="fluent:math-formula-16-filled" class="text-3xl text-sky-300" ></iconify-icon>
<Card.Title>Math Stack Exchange</Card.Title>
<Card.Description>Inspired by Math Stack Exchange.</Card.Description>
</Card.Header>
</Card.Root>
</div>

View file

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

View file

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

View file

@ -0,0 +1,91 @@
---
section: Plugins
title: Attachment
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
This plugin adds support for attachments.
## Installation
```
npm i @cartamd/plugin-attachment
```
## Setup
### Styles
Import the default theme, or create you own:
<Code>
```ts
import '@cartamd/plugin-attachment/default.css';
```
</Code>
### Extension
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { attachment } from '@cartamd/plugin-attachment';
const carta = new Carta({
extensions: [
attachment({
upload(file) {
/* ... */
}
})
]
});
</script>
<MarkdownEditor {carta} />
```
</Code>
## Options
Here are the options you can pass to `attachment()`:
```ts
export interface AttachmentExtensionOptions {
/**
* Upload a file to the server. Return the url of the uploaded file.
* If an error occurs, return null. This function does **not** handle errors.
* @param file The file to upload
* @returns The uploaded file url, or null if it failed
*/
upload: (file: File) => Promise<string | null>;
/**
* Supported mime types.
*
* @default ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml'].
*/
supportedMimeTypes?: string[];
/**
* Whether to disable the attach icon.
*
* @default false
*/
disableIcon?: boolean;
/**
* Custom drop overlay component. Use `false` to disable the overlay.
*/
dropOverlay?: false | typeof SvelteComponent;
/**
* Custom loading overlay component. Use `false` to disable the overlay.
*/
loadingOverlay?: false | typeof SvelteComponent<{ uploadingFiles: Writable<File[]> }>;
}
```

View file

@ -0,0 +1,80 @@
---
section: Plugins
title: Code
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
## Installation
<Code>
```
npm i @cartamd/plugin-code
```
</Code>
## Setup
### Styles
Import the default styles:
<Code>
```ts
import '@cartamd/plugin-code/default.css';
```
</Code>
### Using the default highlighter
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options.
<Code>
```ts
const carta = new Carta({
// ...
extensions: [
code({
theme: 'ayu-light'
})
]
});
```
</Code>
### Using a custom highlighter
It is no longer possible to specify a custom highlighter in this plugin. However, there are many different [Remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) that provide syntax highlighting.
### Extension
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { code } from '@cartamd/plugin-code';
const carta = new Carta({
extensions: [code()]
});
</script>
<MarkdownEditor {carta} />
```
</Code>
## Options
The options you can pass to `code()` extend the ones provided by [Shiki](https://shiki.matsu.io/guide/transformers).

View file

@ -0,0 +1,70 @@
---
section: Plugins
title: Emoji
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
This plugin adds support for **Emojis**.
## Installation
<Code>
```
npm i @cartamd/plugin-emoji
```
</Code>
## Setup
### Styles
Import the default theme, or create you own:
<Code>
```ts
import '@cartamd/plugin-emoji/default.css';
```
</Code>
### Extension
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { emoji } from '@cartamd/plugin-emoji';
const carta = new Carta({
extensions: [emoji()]
});
</script>
<MarkdownEditor {carta} />
```
</Code>
## Options
Here are the options you can pass to `emoji()`:
```ts
export interface EmojiExtensionOptions {
/**
* Custom in transition. See https://svelte.dev/docs#run-time-svelte-transition.
*/
inTransition?: (node: Element) => TransitionConfig;
/**
* Custom out transition. See https://svelte.dev/docs#run-time-svelte-transition.
*/
outTransition?: (node: Element) => TransitionConfig;
}
```

View file

@ -0,0 +1,160 @@
---
section: Plugins
title: Math
---
<script>
import Code from '$lib/components/code/Code.svelte';
import { Markdown, Carta } from 'carta-md';
import { math } from '@cartamd/plugin-math';
import 'katex/dist/katex.css';
const carta = new Carta({
extensions: [math()]
})
export let inline = "$a^2+b^2=c^2$";
export let block = `
$$
\\mathcal{L}\\{f\\}(s) = \\int_0^{\\infty} {f(t)e^{-st}dt}
$$
`;
</script>
This plugins adds support for [KaTeX](https://katex.org/) expressions.
## Installation
<Code>
```
npm i @cartamd/plugin-math
```
</Code>
## Setup
### Styles
You need to get access to the katex **stylesheet**,
to do so, you can either install katex using:
<Code>
```
npm i katex
```
</Code>
and then adding this import to your app:
<Code>
```ts
import 'katex/dist/katex.css';
```
</Code>
or by using a content delivery network:
<Code>
```html
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/katex.min.css"
integrity="sha384-3UiQGuEI4TTMaFmGIZumfRPtfKQ3trwQE2JgosJxCnGmQpL/lJdjpcHkaaFwHlcI"
crossorigin="anonymous"
/>
```
</Code>
### Extension
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { math } from '@cartamd/plugin-math';
const carta = new Carta({
extensions: [math()]
});
</script>
<MarkdownEditor {carta} />
```
</Code>
## Usage
Inline:
<Code>
```
Pythagorean theorem: $a^2+b^2=c^2$
```
</Code>
<Markdown {carta} value={inline} />
<br>
Block:
<Code>
```
$$
\\mathcal{L}\\{f\\}(s) = \\int_0^{\\infty} {f(t)e^{-st}dt}
$$
```
</Code>
<Markdown {carta} value={block} />
## Options
Here are the options you can pass to `math()`:
```ts
interface MathExtensionOptions {
/**
* Options for inline katex, eg: $a^2+b^2=c^2$
*/
inline?: {
/**
* @default control+m
*/
shortcut?: Set<string>;
};
/**
* Option for block katex, eg:
* $$
* a^2+b^2=c^2
* $$
*/
block?: {
/**
* @default ctrl+shift+m
*/
shortcut?: Set<string>;
};
/**
* Options for remark-math
*/
remarkMath?: RemarkMathOptions;
/**
* Options for rehype-katex
*/
rehypeKatex?: RehypeKatexOptions;
}
```

View file

@ -0,0 +1,78 @@
---
section: Plugins
title: Slash
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
This plugin adds support for **Slash** commands.
## Installation
<Code>
```
npm i @cartamd/plugin-slash
```
</Code>
## Setup
### Styles
Import the default theme, or create you own:
<Code>
```ts
import '@cartamd/plugin-slash/default.css';
```
</Code>
### Extension
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { slash } from '@cartamd/plugin-slash';
const carta = new Carta({
extensions: [slash()]
});
</script>
<MarkdownEditor {carta} />
```
</Code>
## Options
Here are the options you can pass to `slash()`:
```ts
export interface SlashExtensionOptions {
/**
* List of default snippets to disable.
*/
disableDefaultSnippets?: DefaultSnippetId[] | true;
/**
* Additional snippets.
*/
snippets?: SlashSnippet[];
/**
* Custom in transition. See https://svelte.dev/docs#run-time-svelte-transition.
*/
inTransition?: (node: Element) => TransitionConfig;
/**
* Custom out transition. See https://svelte.dev/docs#run-time-svelte-transition.
*/
outTransition?: (node: Element) => TransitionConfig;
}
```

View file

@ -0,0 +1,71 @@
---
section: Plugins
title: TikZ
---
<script>
import Code from '$lib/components/code/Code.svelte';
</script>
This plugin adds support for **PGF/TikZ** illustrations thanks to [TikZJax](https://tikzjax.com/). It uses the code generated for the [Obsidian-TikZ plugin](https://github.com/artisticat1/obsidian-tikzjax).
<Code>
```
npm i @cartamd/plugin-tikz
```
</Code>
## Important Notes
1. This plugin requires the import of a **heavy** library (~7Mb), which is dynamically imported at runtime;
2. Generated images are **not SSR compatible**, as they are rendered in the browser;
3. You need to update your sanitizer to allow the specific tag: `<div type="text/tikz">`.
## Setup
<Code>
```svelte
<script>
import { Carta, MarkdownEditor } from 'carta-md';
import { tikz } from '@cartamd/plugin-tikz';
import '@cartamd/plugin-tikz/fonts.css';
const carta = new Carta({
extensions: [tikz()]
});
</script>
<MarkdownEditor {carta} />
```
</Code>
## Options
Here are the options you can pass to `tikz()`:
```ts
interface TikzExtensionOptions {
/**
* Enables Tikzjax console output.
*/
debug?: boolean;
/**
* Class for generated svg div container.
*/
class?: string;
/**
* Whether to center the generated expression.
* @default true
*/
center?: boolean;
/**
* Post processing function for html.
* This also runs on stored html.
*/
postProcessing?: (html: string) => string;
}
```

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,23 @@
import type { SvelteComponent } from 'svelte';
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { base } from '$app/paths';
export const load: PageServerLoad = async (event) => {
const pages = import.meta.glob('../../pages/**/*.svelte.md');
const path = event.url.pathname.slice(base.length).slice(1);
const match = pages[`../../pages/${path}.svelte.md`];
if (!match) throw error(404, 'Not found');
const Markdown = (await match()) as {
default: SvelteComponent;
metadata: Record<string, unknown>;
};
const content = Markdown.default.render();
const metadata = Markdown.metadata;
return {
content,
metadata
};
};

View file

@ -0,0 +1,51 @@
<script lang="ts">
import type { PageData } from './$types';
import { onMount, type SvelteComponent } from 'svelte';
import { page } from '$app/stores';
import { base } from '$app/paths';
import '$lib/styles/markdown.scss';
import '$lib/styles/coldark.scss';
export let data: PageData;
let mounted = false;
let clientSideComponent: typeof SvelteComponent | null = null;
async function renderClientSideComponent() {
// Load a reactive version of the page to keep reactivity
const pages = import.meta.glob('../../pages/**/*.svelte.md');
const path = $page.url.pathname.slice(base.length).slice(1);
const match = pages[`../../pages/${path}.svelte.md`];
clientSideComponent = ((await match()) as { default: typeof SvelteComponent }).default;
}
onMount(() => {
mounted = true;
renderClientSideComponent();
});
$: if (mounted) {
$page.url;
clientSideComponent = null;
renderClientSideComponent();
}
</script>
<svelte:head>
<title>{data.metadata['title']} - Carta</title>
</svelte:head>
<h3 class="mb-2 font-semibold text-sky-300">{data.metadata['section']}</h3>
<h1 class="mb-4 text-5xl font-bold text-white">{data.metadata['title']}</h1>
<div class="markdown">
{#if clientSideComponent}
<svelte:component this={clientSideComponent} />
{:else}
{#key $page.url}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html data.content.html}
{/key}
{/if}
</div>

BIN
docs/static/background.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

BIN
docs/static/favicon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

BIN
docs/static/logo-small.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
docs/static/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -1,11 +1,15 @@
import mdsvexConfig from './mdsvex.config.js';
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { mdsvex } from 'mdsvex';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', ...mdsvexConfig.extensions],
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
preprocess: [vitePreprocess(), mdsvex(mdsvexConfig)],
kit: {
adapter: adapter({

59
docs/tailwind.config.js Normal file
View file

@ -0,0 +1,59 @@
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: 'class',
content: ['./src/**/*.{html,js,svelte,ts,md}'],
safelist: ['dark'],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border) / <alpha-value>)',
input: 'hsl(var(--input) / <alpha-value>)',
ring: 'hsl(var(--ring) / <alpha-value>)',
background: 'hsl(var(--background) / <alpha-value>)',
foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: {
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
},
secondary: {
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
},
destructive: {
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
},
muted: {
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
},
accent: {
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
},
popover: {
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
},
card: {
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
}
};
export default config;

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