diff --git a/docs/src/lib/components/header-tracker/HeaderTracker.svelte b/docs/src/lib/components/header-tracker/HeaderTracker.svelte index f5a6b1d..d584c97 100644 --- a/docs/src/lib/components/header-tracker/HeaderTracker.svelte +++ b/docs/src/lib/components/header-tracker/HeaderTracker.svelte @@ -1,5 +1,7 @@ - + { + throttledHighlightHeader(); + debouncedHighlightHeader(); // So it is called at the end of the scroll event + }} +/>
{#each headers as header, i} {@const margin = Number(header.tagName.split('')[1]) - 1} - {@const nextHeader = headers[i + 1]} - {#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href} - {#key scrollY} + {#key selectedHeaderIndex} + {#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href} {header.innerText} - {/key} - {/if} + {/if} + {/key} {/each}
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 + + + + Using Components + +

Plugins

diff --git a/docs/src/lib/styles/markdown.scss b/docs/src/lib/styles/markdown.scss index 2e3c7cd..61eb539 100644 --- a/docs/src/lib/styles/markdown.scss +++ b/docs/src/lib/styles/markdown.scss @@ -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; } } diff --git a/docs/src/lib/utils.ts b/docs/src/lib/utils.ts index eba19d8..d2b288e 100644 --- a/docs/src/lib/utils.ts +++ b/docs/src/lib/utils.ts @@ -54,3 +54,41 @@ export const flyAndScale = ( easing: cubicOut }; }; + +export const throttle = ( + 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(cb: (...args: T) => unknown, wait = 1000) { + let timeout: NodeJS.Timeout; + return (...args: T) => { + clearTimeout(timeout); + timeout = setTimeout(() => cb(...args), wait); + }; +} diff --git a/docs/src/pages/editing-styles.svelte.md b/docs/src/pages/editing-styles.svelte.md index e54df5a..81736ed 100644 --- a/docs/src/pages/editing-styles.svelte.md +++ b/docs/src/pages/editing-styles.svelte.md @@ -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 `` 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: + + + +```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/introduction.svelte.md b/docs/src/pages/introduction.svelte.md index 6c706f8..12e0af9 100644 --- a/docs/src/pages/introduction.svelte.md +++ b/docs/src/pages/introduction.svelte.md @@ -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. diff --git a/docs/src/pages/migration.svelte.md b/docs/src/pages/migration.svelte.md index ab94937..72d25dc 100644 --- a/docs/src/pages/migration.svelte.md +++ b/docs/src/pages/migration.svelte.md @@ -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; 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/docs/src/routes/+layout.svelte b/docs/src/routes/+layout.svelte index 9dddf88..723f83f 100644 --- a/docs/src/routes/+layout.svelte +++ b/docs/src/routes/+layout.svelte @@ -24,6 +24,6 @@