Compare commits
27 commits
@cartamd/p
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
2177545454 | ||
|
a303834127 | ||
|
3fd18a6796 | ||
|
486a885704 | ||
|
8e5803a1c4 | ||
|
0ff9e5b124 | ||
|
eb114a7311 | ||
|
34d4f400e6 | ||
|
cc79b25288 | ||
|
a44b8e971d | ||
|
bafc86d8d3 | ||
|
a086ece088 | ||
|
dddaad1f1b | ||
|
7261d295e2 | ||
|
482fd4b8af | ||
|
dfc8812123 | ||
|
54358b649b | ||
|
7ad874e6aa | ||
|
545864a202 | ||
|
0cef0b94ed | ||
|
20d58a856b | ||
|
1a61f58e7d | ||
|
7d4034c316 | ||
|
26241b5bcd | ||
|
3785fd01c0 | ||
|
c9be45a1ee | ||
|
c4428b8150 |
22 changed files with 431 additions and 97 deletions
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import { debounce, throttle } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const PADDING = 80;
|
||||
|
||||
|
@ -7,49 +9,58 @@
|
|||
|
||||
let className = '';
|
||||
let headers: HTMLElement[] = [];
|
||||
let scrollY = 0;
|
||||
let selectedHeaderIndex = 0;
|
||||
|
||||
function retrieveHeaders() {
|
||||
const markdownContainer = document.querySelector('.markdown');
|
||||
if (!markdownContainer) return;
|
||||
headers = Array.from(markdownContainer.querySelectorAll('h1, h2, h3')) as HTMLElement[];
|
||||
headers = Array.from(
|
||||
document.querySelectorAll('.markdown > h1, .markdown > h2, .markdown > h3')
|
||||
) as HTMLElement[];
|
||||
}
|
||||
|
||||
function highlightHeader(header: HTMLElement, nextHeader: HTMLElement | null, index: number) {
|
||||
const headerHasReachedTop = header.getBoundingClientRect().top <= PADDING || index == 0;
|
||||
const nextHeaderReachedTop = nextHeader && nextHeader.getBoundingClientRect().top <= PADDING;
|
||||
return !nextHeaderReachedTop && headerHasReachedTop;
|
||||
const highlightHeader = () => {
|
||||
for (let index = headers.length - 1; index >= 0; index--) {
|
||||
const header = headers[index];
|
||||
const rect = header.getBoundingClientRect();
|
||||
if (rect.top < PADDING) {
|
||||
selectedHeaderIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
selectedHeaderIndex = 0;
|
||||
};
|
||||
|
||||
let observer: MutationObserver;
|
||||
const [throttledHighlightHeader] = throttle(highlightHeader, 100);
|
||||
const debouncedHighlightHeader = debounce(highlightHeader, 100);
|
||||
|
||||
onMount(() => {
|
||||
observer = new MutationObserver(retrieveHeaders);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
onNavigate(() => {
|
||||
setTimeout(() => {
|
||||
retrieveHeaders();
|
||||
highlightHeader();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer?.disconnect();
|
||||
});
|
||||
onMount(retrieveHeaders);
|
||||
</script>
|
||||
|
||||
<svelte:window bind:scrollY />
|
||||
<svelte:window
|
||||
on:scroll={() => {
|
||||
throttledHighlightHeader();
|
||||
debouncedHighlightHeader(); // So it is called at the end of the scroll event
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="h-full space-y-3 {className}">
|
||||
{#each headers as header, i}
|
||||
{@const margin = Number(header.tagName.split('')[1]) - 1}
|
||||
{@const nextHeader = headers[i + 1]}
|
||||
{#key selectedHeaderIndex}
|
||||
{#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href}
|
||||
{#key scrollY}
|
||||
<a
|
||||
style="margin-left: {margin * 0.75}rem;"
|
||||
class="block text-sm {highlightHeader(header, nextHeader, i)
|
||||
class="block text-sm {selectedHeaderIndex === i
|
||||
? 'font-medium text-sky-300'
|
||||
: 'text-neutral-400'}"
|
||||
href={header.children[0].href}>{header.innerText}</a
|
||||
>
|
||||
{/key}
|
||||
{/if}
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -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,7 +46,18 @@
|
|||
@apply rounded bg-neutral-800 px-1 text-neutral-50;
|
||||
}
|
||||
|
||||
.carta-renderer code {
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
code {
|
||||
@apply px-1;
|
||||
}
|
||||
}
|
||||
|
||||
.carta-editor code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,3 +54,41 @@ export const flyAndScale = (
|
|||
easing: cubicOut
|
||||
};
|
||||
};
|
||||
|
||||
export const throttle = <R, A extends unknown[]>(
|
||||
fn: (...args: A) => R,
|
||||
delay: number
|
||||
): [(...args: A) => R | undefined, () => void] => {
|
||||
let wait = false;
|
||||
let timeout: undefined | number;
|
||||
let cancelled = false;
|
||||
|
||||
return [
|
||||
(...args: A) => {
|
||||
if (cancelled) return undefined;
|
||||
if (wait) return undefined;
|
||||
|
||||
const val = fn(...args);
|
||||
|
||||
wait = true;
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
wait = false;
|
||||
}, delay);
|
||||
|
||||
return val;
|
||||
},
|
||||
() => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export function debounce<T extends unknown[]>(cb: (...args: T) => unknown, wait = 1000) {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: T) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => cb(...args), wait);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Carta is a lightweight, fast and extensible Svelte Markdown editor and viewer, d
|
|||
|
||||
## Features
|
||||
|
||||
- 🌈 Markdown syntax highlighting ([shiki](https://shiki.style/));
|
||||
- 🌈 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.
|
||||
|
|
|
@ -9,7 +9,7 @@ section: Overview
|
|||
|
||||
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](/plugins/math) and [plugin-anchor](/plugins/anchor).
|
||||
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
|
||||
|
||||
|
@ -21,24 +21,24 @@ Make sure to remove previous themes imports, as Shiki uses JS based ones.
|
|||
import 'carta-md/light.css'; // 👈 To be removed!
|
||||
```
|
||||
|
||||
And also update the default theme. SHJ based selectors should be removed:
|
||||
And also update the default theme. Previous based selectors should be removed:
|
||||
|
||||
```css
|
||||
/* 👇 To be removed! */
|
||||
[class*='shj-lang-'] {
|
||||
/* 👈 To be removed! */
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
## Removed verbose prefixes
|
||||
|
||||
Many exports have been renamed to make them less verbose, here are them:
|
||||
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` -> `Extension`;
|
||||
- `CartaExtension` -> `Plugin`;
|
||||
- `CartaExtensionComponent` -> `ExtensionComponent`;
|
||||
- `CartaOptions` -> `Options`;
|
||||
- `CartaHistory` -> `TextAreaHistory`;
|
||||
|
@ -49,7 +49,7 @@ Many exports have been renamed to make them less verbose, here are them:
|
|||
- `CartaRenderer` -> `Renderer`;
|
||||
- `CartaLabels` -> `Labels`;
|
||||
|
||||
## Minor Changes
|
||||
# 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;
|
||||
|
|
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).
|
|
@ -24,6 +24,6 @@
|
|||
<slot />
|
||||
<Footer />
|
||||
</main>
|
||||
<HeaderTracker class="sticky top-24 hidden w-[30rem] xl:block" />
|
||||
<HeaderTracker class="sticky top-24 hidden w-[15rem] flex-shrink-0 xl:block" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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",
|
||||
|
@ -28,9 +28,9 @@
|
|||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.0",
|
||||
"dependencies": {
|
||||
"@shikijs/rehype": "^1.3.0",
|
||||
"@shikijs/rehype": "^1.4.0",
|
||||
"unified": "^11.0.4"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
@ -1,11 +1,28 @@
|
|||
import { DualTheme, Theme, isSingleTheme, type Plugin } from 'carta-md';
|
||||
import { type RehypeShikiOptions } from '@shikijs/rehype';
|
||||
import type { DualTheme, Theme, Plugin } from 'carta-md';
|
||||
import type { RehypeShikiOptions } from '@shikijs/rehype';
|
||||
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
|
||||
|
||||
export type CodeExtensionOptions = Omit<RehypeShikiOptions, 'theme' | 'themes'> & {
|
||||
theme?: Theme | DualTheme;
|
||||
};
|
||||
|
||||
// FIXME: find a better solution then copy-pasting these functions in next version.
|
||||
// However, when importing from carta-md, this causes a MODULE_NOT_FOUND error
|
||||
// for some reason.
|
||||
/**
|
||||
* Checks if a theme is a dual theme.
|
||||
* @param theme The theme to check.
|
||||
* @returns Whether the theme is a dual theme.
|
||||
*/
|
||||
export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme =>
|
||||
typeof theme == 'object' && 'light' in theme && 'dark' in theme;
|
||||
/**
|
||||
* Checks if a theme is a single theme.
|
||||
* @param theme The theme to check.
|
||||
* @returns Whether the theme is a single theme.
|
||||
*/
|
||||
export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme);
|
||||
|
||||
/**
|
||||
* Carta code highlighting plugin. Themes available on [GitHub](https://github.com/speed-highlight/core/tree/main/dist/themes).
|
||||
*/
|
||||
|
@ -17,6 +34,7 @@ export const code = (options?: CodeExtensionOptions): Plugin => {
|
|||
type: 'rehype',
|
||||
async transform({ processor, carta }) {
|
||||
let theme = options?.theme;
|
||||
|
||||
const highlighter = await carta.highlighter();
|
||||
if (!theme) {
|
||||
theme = highlighter.theme; // Use the theme specified in the highlighter
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@cartamd/plugin-emoji",
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"rehype-katex": "^7.0.0",
|
||||
"remark-math": "^6.0.0"
|
||||
},
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.1",
|
||||
"keywords": [
|
||||
"carta",
|
||||
"markdown",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@cartamd/plugin-slash",
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.0",
|
||||
"keywords": [
|
||||
"carta",
|
||||
"markdown",
|
||||
|
|
|
@ -86,15 +86,20 @@ const tikzTransformer: UnifiedPlugin<
|
|||
hast.Root
|
||||
> = ({ carta, options }) => {
|
||||
return async function (tree) {
|
||||
visit(tree, (node, index, parent) => {
|
||||
visit(tree, (pre, index, parent) => {
|
||||
if (typeof document === 'undefined') {
|
||||
// Cannot run outside the browser
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type !== 'element') return;
|
||||
const element = node as hast.Element;
|
||||
if (pre.type !== 'element') return;
|
||||
const preElement = pre as hast.Element;
|
||||
if (preElement.tagName !== 'pre') return;
|
||||
const element = pre.children.at(0) as hast.Element | undefined;
|
||||
if (!element) return;
|
||||
|
||||
if (element.tagName !== 'code') return;
|
||||
if (!element.properties['className']) return;
|
||||
if (!(element.properties['className'] as string[]).includes('language-tikz')) return;
|
||||
|
||||
// Element is a TikZ code block
|
||||
|
@ -129,6 +134,7 @@ const tikzTransformer: UnifiedPlugin<
|
|||
}
|
||||
|
||||
const hastNode = fromDom(container) as hast.Element;
|
||||
|
||||
parent?.children.splice(index!, 1, hastNode);
|
||||
|
||||
return [SKIP, index!];
|
||||
|
|
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