diff --git a/docs/src/lib/components/sidebar/Sidebar.svelte b/docs/src/lib/components/sidebar/Sidebar.svelte
index b02930c..cb82c46 100644
--- a/docs/src/lib/components/sidebar/Sidebar.svelte
+++ b/docs/src/lib/components/sidebar/Sidebar.svelte
@@ -45,6 +45,12 @@
Community Plugins
+
+
+
+```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;
+}
+```
+
+
+
+## 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({
-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:
diff --git a/docs/src/pages/migration.svelte.md b/docs/src/pages/migration.svelte.md
index 1a2383c..72d25dc 100644
--- a/docs/src/pages/migration.svelte.md
+++ b/docs/src/pages/migration.svelte.md
@@ -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`;
diff --git a/docs/src/pages/using-components.svelte.md b/docs/src/pages/using-components.svelte.md
new file mode 100644
index 0000000..f301c51
--- /dev/null
+++ b/docs/src/pages/using-components.svelte.md
@@ -0,0 +1,217 @@
+---
+title: Using Svelte Components
+section: Overview
+---
+
+
+
+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:
+
+
+
+```shell
+npm i unist-util-visit
+# Types
+npm i -D unified hast
+```
+
+
+
+Let's create a Unified plugin. The basic structure of a plugin is the following:
+
+
+
+```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
+ }
+ }
+}
+```
+
+
+
+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:
+
+
+
+```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];
+ });
+ };
+};
+```
+
+
+
+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
+ #pizza
+```
+
+### Configuring the transformer
+
+Unified plugins need to be wrapped inside a `UnifiedTransformer` type, to be able to be used in Carta.
+
+
+
+```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);
+ }
+};
+```
+
+
+
+### Mounting the components
+
+We now want to replace the generated hashtag placeholders with the following element:
+
+
+
+```svelte
+
+
+
+
+```
+
+
+
+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.
+
+
+
+```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();
+ }
+ }
+];
+```
+
+
+
+### Using the plugin
+
+Let's now create a Plugin with the transformer and the listener:
+
+
+
+```ts
+import type { Plugin } from 'carta-md';
+
+export const hashtag = (): Plugin => ({
+ transformers: [hashtagTransformer],
+ listeners: [convertHashtags]
+});
+```
+
+
+
+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).
diff --git a/packages/carta-md/package.json b/packages/carta-md/package.json
index e1eeb05..bc074ef 100644
--- a/packages/carta-md/package.json
+++ b/packages/carta-md/package.json
@@ -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": {
diff --git a/packages/carta-md/src/lib/internal/components/Input.svelte b/packages/carta-md/src/lib/internal/components/Input.svelte
index c3fe8b6..3874591 100644
--- a/packages/carta-md/src/lib/internal/components/Input.svelte
+++ b/packages/carta-md/src/lib/internal/components/Input.svelte
@@ -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;
diff --git a/packages/carta-md/src/lib/internal/highlight.ts b/packages/carta-md/src/lib/internal/highlight.ts
index ed2df86..16cfddf 100644
--- a/packages/carta-md/src/lib/internal/highlight.ts
+++ b/packages/carta-md/src/lib/internal/highlight.ts
@@ -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;
diff --git a/packages/plugin-anchor/src/lib/default.css b/packages/plugin-anchor/src/lib/default.css
index afbed18..1d18753 100644
--- a/packages/plugin-anchor/src/lib/default.css
+++ b/packages/plugin-anchor/src/lib/default.css
@@ -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;
}
diff --git a/packages/plugin-code/README.md b/packages/plugin-code/README.md
index 7eab23b..cbb5c68 100644
--- a/packages/plugin-code/README.md
+++ b/packages/plugin-code/README.md
@@ -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']
+ }
});
```
diff --git a/packages/plugin-code/package.json b/packages/plugin-code/package.json
index e57937b..0da3535 100644
--- a/packages/plugin-code/package.json
+++ b/packages/plugin-code/package.json
@@ -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": [
diff --git a/packages/plugin-tikz/src/index.ts b/packages/plugin-tikz/src/index.ts
index 895eb62..8ef05de 100644
--- a/packages/plugin-tikz/src/index.ts
+++ b/packages/plugin-tikz/src/index.ts
@@ -86,14 +86,18 @@ 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;
@@ -130,6 +134,7 @@ const tikzTransformer: UnifiedPlugin<
}
const hastNode = fromDom(container) as hast.Element;
+
parent?.children.splice(index!, 1, hastNode);
return [SKIP, index!];
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f2eb6c4..1968455 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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: