Compare commits
14 commits
@cartamd/p
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
2177545454 | ||
|
a303834127 | ||
|
3fd18a6796 | ||
|
486a885704 | ||
|
8e5803a1c4 | ||
|
0ff9e5b124 | ||
|
eb114a7311 | ||
|
34d4f400e6 | ||
|
cc79b25288 | ||
|
a44b8e971d | ||
|
bafc86d8d3 | ||
|
a086ece088 | ||
|
dddaad1f1b | ||
|
7261d295e2 |
12 changed files with 314 additions and 54 deletions
|
@ -45,6 +45,12 @@
|
|||
<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 -->
|
||||
|
|
|
@ -46,8 +46,18 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,11 +43,39 @@ While the core styles are embedded in the Svelte components, the others can be s
|
|||
|
||||
### Using multiple themes
|
||||
|
||||
By using the `theme` property in the editor you can differentiate the themes of multiple editors.
|
||||
By using the `theme` property in `<MarkdownEditor>` you can differentiate the themes of multiple editors.
|
||||
|
||||
## Changing Markdown color theme
|
||||
## Dark mode
|
||||
|
||||
Carta uses [Shiki](https://shiki.matsu.io/) for syntax highlighting. Two default themes are included in the core package, which are as a [dual theme](https://shiki.matsu.io/guide/dual-themes) used for light and 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:
|
||||
|
||||
|
@ -62,7 +90,7 @@ const carta = new Carta({
|
|||
|
||||
</Code>
|
||||
|
||||
If you use a [custom theme](https://shiki.matsu.io/guide/load-theme)(or also a custom language), you need to specify it, so that it gets loaded into the highlighter:
|
||||
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>
|
||||
|
||||
|
|
|
@ -24,8 +24,8 @@ 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-'] {
|
||||
/* 👈 To be removed! */
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
@ -38,7 +38,7 @@ Many exports have been renamed to make them less verbose:
|
|||
- `CartaRenderer` -> `Markdown` (old one still supported);
|
||||
- `CartaEvent` -> `Event`;
|
||||
- `CartaEventType` -> `EventType`;
|
||||
- `CartaExtension` -> `Extension`;
|
||||
- `CartaExtension` -> `Plugin`;
|
||||
- `CartaExtensionComponent` -> `ExtensionComponent`;
|
||||
- `CartaOptions` -> `Options`;
|
||||
- `CartaHistory` -> `TextAreaHistory`;
|
||||
|
|
217
docs/src/pages/using-components.svelte.md
Normal file
217
docs/src/pages/using-components.svelte.md
Normal file
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
title: Using Svelte Components
|
||||
section: Overview
|
||||
---
|
||||
|
||||
<script>
|
||||
import Code from '$lib/components/code/Code.svelte';
|
||||
</script>
|
||||
|
||||
Svelte components can be embedded into the rendered HTML to make certain elements interactive. However, they require a bit more work, as Remark is configured to only render static HTML. To get around this, the idea is to do the following:
|
||||
|
||||
1. Create a Unified plugin to isolate the targeted element;
|
||||
2. Replace all the elements with the component, after every render.
|
||||
|
||||
## Example
|
||||
|
||||
Let's say we want to replace all hashtags, such as `#something`, with a custom component. Here is as example of how that could be achieved.
|
||||
|
||||
### Parsing the hashtags
|
||||
|
||||
First things first: we need to tell the parser that we want to parse hashtags as custom elements. To do this, it's useful to first install the following packages:
|
||||
|
||||
<Code>
|
||||
|
||||
```shell
|
||||
npm i unist-util-visit
|
||||
# Types
|
||||
npm i -D unified hast
|
||||
```
|
||||
|
||||
</Code>
|
||||
|
||||
Let's create a Unified plugin. The basic structure of a plugin is the following:
|
||||
|
||||
<Code>
|
||||
|
||||
```ts
|
||||
import type { Plugin as UnifiedPlugin } from 'unified'
|
||||
import { SKIP, visit } from 'unist-util-visit'
|
||||
|
||||
const unifiedPlugin: UnifiedPlugin<[], hast.Root> = () => {
|
||||
return function (tree) {
|
||||
// Visit every node in the syntax tree
|
||||
visit(tree, (node, index, parent) => {
|
||||
// Do something with the node
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Code>
|
||||
|
||||
We now want to parse text nodes, so that words such as `#pizza` and `#123` are separated from the rest. This is a possible implementation:
|
||||
|
||||
<Code>
|
||||
|
||||
```ts
|
||||
const unifiedPlugin: UnifiedPlugin<[], hast.Root> = () => {
|
||||
return function (tree) {
|
||||
visit(tree, (node, index, parent) => {
|
||||
// Skip code blocks and their children
|
||||
if (node.type === 'element' && node.tagName === 'pre') return [SKIP];
|
||||
// Skip non-text nodes
|
||||
if (node.type !== 'text') return;
|
||||
const text = node as hast.Text;
|
||||
|
||||
// Parse the text node and replace hashtags with spans
|
||||
const regex = /#(\w+)/g;
|
||||
const children: (hast.Element | hast.Text)[] = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = regex.exec(text.value))) {
|
||||
const before = text.value.slice(lastIndex, match.index);
|
||||
if (before) {
|
||||
children.push({ type: 'text', value: before });
|
||||
}
|
||||
children.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: { type: 'hashtag', value: match[1] },
|
||||
children: [{ type: 'text', value: match[0] }]
|
||||
});
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
if (lastIndex < text.value.length) {
|
||||
children.push({ type: 'text', value: text.value.slice(lastIndex) });
|
||||
}
|
||||
|
||||
// Replace the text node with all the children
|
||||
parent!.children.splice(index!, 1, ...children);
|
||||
|
||||
// Skip the children
|
||||
return [SKIP, index! + children.length];
|
||||
});
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
</Code>
|
||||
|
||||
If you want a more in-depth guide on writing Unified plugins, you can check out the official [documentation](https://unifiedjs.com/learn/guide/create-a-plugin/).
|
||||
|
||||
Notice that hashtags are now replaced with the following:
|
||||
|
||||
```html
|
||||
<span type="hashtag" value="pizza"> #pizza </span>
|
||||
```
|
||||
|
||||
### Configuring the transformer
|
||||
|
||||
Unified plugins need to be wrapped inside a `UnifiedTransformer` type, to be able to be used in Carta.
|
||||
|
||||
<Code>
|
||||
|
||||
```ts
|
||||
import type { UnifiedTransformer } from 'carta-md';
|
||||
|
||||
const hashtagTransformer: UnifiedTransformer<'sync'> = {
|
||||
execution: 'sync', // Sync, since the plugin is synchronous
|
||||
type: 'rehype', // Rehype, since it operates on HTML
|
||||
transform({ processor }) {
|
||||
processor.use(unifiedPlugin);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
</Code>
|
||||
|
||||
### Mounting the components
|
||||
|
||||
We now want to replace the generated hashtag placeholders with the following element:
|
||||
|
||||
<Code>
|
||||
|
||||
```svelte
|
||||
<!-- Hashtag.svelte -->
|
||||
<script>
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
console.log('Hashtag clicked!');
|
||||
}}
|
||||
>
|
||||
#{value}
|
||||
</button>
|
||||
```
|
||||
|
||||
</Code>
|
||||
|
||||
To do that, we create a listener that:
|
||||
|
||||
1. Finds all the previous placeholders;
|
||||
2. Mounts the component next to them;
|
||||
3. Removes the placeholders.
|
||||
|
||||
<Code>
|
||||
|
||||
```ts
|
||||
import type { Listener } from 'carta-md';
|
||||
import Hashtag from './Hashtag.svelte';
|
||||
|
||||
const convertHashtags: Listener<'carta-render'> = [
|
||||
'carta-render',
|
||||
function onRender({ detail: { carta } }) {
|
||||
const rendererContainer = carta.renderer?.container;
|
||||
if (!rendererContainer) return;
|
||||
|
||||
// Find all hashtag spans and replace them with Svelte components
|
||||
const hashtagSpans = rendererContainer.querySelectorAll('span[type="hashtag"]');
|
||||
for (const span of hashtagSpans) {
|
||||
const hashtag = span.getAttribute('value') ?? '';
|
||||
|
||||
new Hashtag({
|
||||
target: span.parentElement!,
|
||||
anchor: span,
|
||||
props: { value: hashtag }
|
||||
});
|
||||
|
||||
span.remove();
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
</Code>
|
||||
|
||||
### Using the plugin
|
||||
|
||||
Let's now create a Plugin with the transformer and the listener:
|
||||
|
||||
<Code>
|
||||
|
||||
```ts
|
||||
import type { Plugin } from 'carta-md';
|
||||
|
||||
export const hashtag = (): Plugin => ({
|
||||
transformers: [hashtagTransformer],
|
||||
listeners: [convertHashtags]
|
||||
});
|
||||
```
|
||||
|
||||
</Code>
|
||||
|
||||
We can now use the plugin with the following:
|
||||
|
||||
```ts
|
||||
import { Carta } from 'carta-md';
|
||||
|
||||
const carta = new Carta({
|
||||
// ...
|
||||
extensions: [hashtag()]
|
||||
});
|
||||
```
|
||||
|
||||
You can find the example source code [here](https://github.com/BearToCode/svelte-in-carta-example).
|
|
@ -39,7 +39,7 @@
|
|||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"shiki": "^1.3.0",
|
||||
"shiki": "^1.4.0",
|
||||
"unified": "^11.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
@ -63,18 +63,14 @@
|
|||
}
|
||||
};
|
||||
|
||||
const normalize = (text: string) => {
|
||||
return text.replaceAll('\r\n', '\n');
|
||||
};
|
||||
|
||||
const highlightNestedLanguages = debounce(async (text: string) => {
|
||||
const highlighter = await carta.highlighter();
|
||||
const { updated } = await loadNestedLanguages(highlighter, text);
|
||||
if (updated) highlight(text);
|
||||
}, 300);
|
||||
|
||||
$: highlight(normalize(value)).then(resize);
|
||||
$: highlightNestedLanguages(normalize(value));
|
||||
$: highlight(value).then(resize);
|
||||
$: highlightNestedLanguages(value);
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
|
|
|
@ -249,6 +249,8 @@ const findNestedLanguages = (text: string) => {
|
|||
* @returns Whether the highlighter was updated with new languages.
|
||||
*/
|
||||
export const loadNestedLanguages = async (highlighter: Highlighter, text: string) => {
|
||||
text = text.replaceAll('\r\n', '\n'); // Normalize line endings
|
||||
|
||||
const languages = findNestedLanguages(text);
|
||||
const loadedLanguages = highlighter.getLoadedLanguages();
|
||||
let updated = false;
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
.carta-renderer h1,
|
||||
.carta-renderer h2,
|
||||
.carta-renderer h3,
|
||||
.carta-renderer h4,
|
||||
.carta-renderer h5,
|
||||
.carta-renderer h6 {
|
||||
.carta-viewer h1,
|
||||
.carta-viewer h2,
|
||||
.carta-viewer h3,
|
||||
.carta-viewer h4,
|
||||
.carta-viewer h5,
|
||||
.carta-viewer h6 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carta-renderer h1 .icon.icon-link,
|
||||
.carta-renderer h2 .icon.icon-link,
|
||||
.carta-renderer h3 .icon.icon-link,
|
||||
.carta-renderer h4 .icon.icon-link,
|
||||
.carta-renderer h5 .icon.icon-link,
|
||||
.carta-renderer h6 .icon.icon-link {
|
||||
.carta-viewer h1 .icon.icon-link,
|
||||
.carta-viewer h2 .icon.icon-link,
|
||||
.carta-viewer h3 .icon.icon-link,
|
||||
.carta-viewer h4 .icon.icon-link,
|
||||
.carta-viewer h5 .icon.icon-link,
|
||||
.carta-viewer h6 .icon.icon-link {
|
||||
opacity: 0;
|
||||
content: url('./link.svg');
|
||||
position: absolute;
|
||||
|
@ -22,11 +22,11 @@
|
|||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.carta-renderer h1:hover .icon-link,
|
||||
.carta-renderer h2:hover .icon-link,
|
||||
.carta-renderer h3:hover .icon-link,
|
||||
.carta-renderer h4:hover .icon-link,
|
||||
.carta-renderer h5:hover .icon-link,
|
||||
.carta-renderer h6:hover .icon-link {
|
||||
.carta-viewer h1:hover .icon-link,
|
||||
.carta-viewer h2:hover .icon-link,
|
||||
.carta-viewer h3:hover .icon-link,
|
||||
.carta-viewer h4:hover .icon-link,
|
||||
.carta-viewer h5:hover .icon-link,
|
||||
.carta-viewer h6:hover .icon-link {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
# Carta Code Plugin
|
||||
|
||||
This plugin adds support for code blocks **syntax highlighting**. Install it using:
|
||||
This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki).
|
||||
|
||||
```
|
||||
npm i @cartamd/plugin-code
|
||||
```
|
||||
|
||||
This is done using [Speed-highlight JS](https://github.com/speed-highlight/core), which supports dynamic imports. This way, languages definitions are only imported at the moment of need.
|
||||
|
||||
## Setup
|
||||
|
||||
### Styles
|
||||
|
@ -20,7 +18,7 @@ import '@cartamd/plugin-code/default.css';
|
|||
|
||||
### Using the default highlighter
|
||||
|
||||
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options.
|
||||
Carta comes with a default highlighter that matches the one used to highlight markdown in the editor and is used by default (Shiki). If you want to use a theme different from the one used to highlight Markdown, you can specify it in the options. Remember to also have it loaded into the highlighter, by specifying it in `shikiOptions`.
|
||||
|
||||
```ts
|
||||
const carta = new Carta({
|
||||
|
@ -29,7 +27,10 @@ const carta = new Carta({
|
|||
code({
|
||||
theme: 'ayu-light'
|
||||
})
|
||||
]
|
||||
],
|
||||
shikiOptions: {
|
||||
themes: ['ayu-light']
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"build": "tsc && tscp"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shikijs/rehype": "^1.3.0",
|
||||
"@shikijs/rehype": "^1.4.0",
|
||||
"@types/node": "^18.16.3",
|
||||
"carta-md": "workspace:*",
|
||||
"typescript": "^5.0.4",
|
||||
|
@ -30,7 +30,7 @@
|
|||
],
|
||||
"version": "4.0.0",
|
||||
"dependencies": {
|
||||
"@shikijs/rehype": "^1.3.0",
|
||||
"@shikijs/rehype": "^1.4.0",
|
||||
"unified": "^11.0.4"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
|
@ -172,8 +172,8 @@ importers:
|
|||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
shiki:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
svelte:
|
||||
specifier: ^3.54.0 || ^4.0.0
|
||||
version: 4.2.2
|
||||
|
@ -297,8 +297,8 @@ importers:
|
|||
packages/plugin-code:
|
||||
dependencies:
|
||||
'@shikijs/rehype':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
unified:
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
|
@ -1422,25 +1422,25 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@shikijs/core@1.3.0:
|
||||
resolution: {integrity: sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==}
|
||||
/@shikijs/core@1.4.0:
|
||||
resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==}
|
||||
dev: false
|
||||
|
||||
/@shikijs/rehype@1.3.0:
|
||||
resolution: {integrity: sha512-CknEidx0ZTg3TeYAPU4ah8cr31a16neBbMyQ5kwAVdkloCe65uhQp+C/FEFs8NRir4eU5XCDA/+w2v5wnN6zgQ==}
|
||||
/@shikijs/rehype@1.4.0:
|
||||
resolution: {integrity: sha512-Ba6QHYx+EIEvmqyNy/B49KAz3rXsTfAqYRY3KTZjPWonytokGOiJ1q/FV9l13D/ad6Qv+eWKhkAz6ITxx6ziFA==}
|
||||
dependencies:
|
||||
'@shikijs/transformers': 1.3.0
|
||||
'@shikijs/transformers': 1.4.0
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-string: 3.0.0
|
||||
shiki: 1.3.0
|
||||
shiki: 1.4.0
|
||||
unified: 11.0.4
|
||||
unist-util-visit: 5.0.0
|
||||
dev: false
|
||||
|
||||
/@shikijs/transformers@1.3.0:
|
||||
resolution: {integrity: sha512-3mlpg2I9CjhjE96dEWQOGeCWoPcyTov3s4aAsHmgvnTHa8MBknEnCQy8/xivJPSpD+olqOqIEoHnLfbNJK29AA==}
|
||||
/@shikijs/transformers@1.4.0:
|
||||
resolution: {integrity: sha512-kzvlWmWYYSeaLKRce/kgmFFORUtBtFahfXRKndor0b60ocYiXufBQM6d6w1PlMuUkdk55aor9xLvy9wy7hTEJg==}
|
||||
dependencies:
|
||||
shiki: 1.3.0
|
||||
shiki: 1.4.0
|
||||
dev: false
|
||||
|
||||
/@sveltejs/adapter-auto@3.1.1(@sveltejs/kit@2.5.4):
|
||||
|
@ -6435,10 +6435,10 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/shiki@1.3.0:
|
||||
resolution: {integrity: sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==}
|
||||
/shiki@1.4.0:
|
||||
resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==}
|
||||
dependencies:
|
||||
'@shikijs/core': 1.3.0
|
||||
'@shikijs/core': 1.4.0
|
||||
dev: false
|
||||
|
||||
/signal-exit@3.0.7:
|
||||
|
|
Loading…
Add table
Reference in a new issue