diff --git a/docs/src/lib/components/sidebar/Sidebar.svelte b/docs/src/lib/components/sidebar/Sidebar.svelte index cb82c46..b02930c 100644 --- a/docs/src/lib/components/sidebar/Sidebar.svelte +++ b/docs/src/lib/components/sidebar/Sidebar.svelte @@ -45,12 +45,6 @@ Community Plugins - - - - Using Components - -

Plugins

diff --git a/docs/src/lib/styles/markdown.scss b/docs/src/lib/styles/markdown.scss index 61eb539..cef9e64 100644 --- a/docs/src/lib/styles/markdown.scss +++ b/docs/src/lib/styles/markdown.scss @@ -46,18 +46,8 @@ @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; } } diff --git a/docs/src/pages/editing-styles.svelte.md b/docs/src/pages/editing-styles.svelte.md index 81736ed..e54df5a 100644 --- a/docs/src/pages/editing-styles.svelte.md +++ b/docs/src/pages/editing-styles.svelte.md @@ -43,39 +43,11 @@ While the core styles are embedded in the Svelte components, the others can be s ### Using multiple themes -By using the `theme` property in `` you can differentiate the themes of multiple editors. +By using the `theme` property in the editor you can differentiate the themes of multiple editors. -## Dark mode +## Changing Markdown color theme -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. +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. You can change theme in the options: @@ -90,7 +62,7 @@ const carta = new Carta({ -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: +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: diff --git a/docs/src/pages/migration.svelte.md b/docs/src/pages/migration.svelte.md index 72d25dc..1a2383c 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` -> `Plugin`; +- `CartaExtension` -> `Extension`; - `CartaExtensionComponent` -> `ExtensionComponent`; - `CartaOptions` -> `Options`; - `CartaHistory` -> `TextAreaHistory`; diff --git a/docs/src/pages/using-components.svelte.md b/docs/src/pages/using-components.svelte.md deleted file mode 100644 index f301c51..0000000 --- a/docs/src/pages/using-components.svelte.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -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 bc074ef..e1eeb05 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.4.0", + "shiki": "^1.3.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 3874591..c3fe8b6 100644 --- a/packages/carta-md/src/lib/internal/components/Input.svelte +++ b/packages/carta-md/src/lib/internal/components/Input.svelte @@ -63,14 +63,18 @@ } }; + 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(value).then(resize); - $: highlightNestedLanguages(value); + $: highlight(normalize(value)).then(resize); + $: highlightNestedLanguages(normalize(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 16cfddf..ed2df86 100644 --- a/packages/carta-md/src/lib/internal/highlight.ts +++ b/packages/carta-md/src/lib/internal/highlight.ts @@ -249,8 +249,6 @@ 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 1d18753..afbed18 100644 --- a/packages/plugin-anchor/src/lib/default.css +++ b/packages/plugin-anchor/src/lib/default.css @@ -1,18 +1,18 @@ -.carta-viewer h1, -.carta-viewer h2, -.carta-viewer h3, -.carta-viewer h4, -.carta-viewer h5, -.carta-viewer h6 { +.carta-renderer h1, +.carta-renderer h2, +.carta-renderer h3, +.carta-renderer h4, +.carta-renderer h5, +.carta-renderer h6 { position: relative; } -.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 { +.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 { opacity: 0; content: url('./link.svg'); position: absolute; @@ -22,11 +22,11 @@ transform: translateY(-50%); } -.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 { +.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 { opacity: 1; } diff --git a/packages/plugin-code/README.md b/packages/plugin-code/README.md index cbb5c68..7eab23b 100644 --- a/packages/plugin-code/README.md +++ b/packages/plugin-code/README.md @@ -1,11 +1,13 @@ # Carta Code Plugin -This plugin adds support for code blocks **syntax highlighting**. It uses the same highlighter from the core package(Shiki). +This plugin adds support for code blocks **syntax highlighting**. Install it using: ``` 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 @@ -18,7 +20,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. Remember to also have it loaded into the highlighter, by specifying it in `shikiOptions`. +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. ```ts const carta = new Carta({ @@ -27,10 +29,7 @@ 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 0da3535..e57937b 100644 --- a/packages/plugin-code/package.json +++ b/packages/plugin-code/package.json @@ -16,7 +16,7 @@ "build": "tsc && tscp" }, "devDependencies": { - "@shikijs/rehype": "^1.4.0", + "@shikijs/rehype": "^1.3.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.4.0", + "@shikijs/rehype": "^1.3.0", "unified": "^11.0.4" }, "keywords": [ diff --git a/packages/plugin-tikz/src/index.ts b/packages/plugin-tikz/src/index.ts index 8ef05de..f46161b 100644 --- a/packages/plugin-tikz/src/index.ts +++ b/packages/plugin-tikz/src/index.ts @@ -86,20 +86,15 @@ const tikzTransformer: UnifiedPlugin< hast.Root > = ({ carta, options }) => { return async function (tree) { - visit(tree, (pre, index, parent) => { + visit(tree, (node, index, parent) => { if (typeof document === 'undefined') { // Cannot run outside the browser return; } - 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 (node.type !== 'element') return; + const element = node as hast.Element; 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 @@ -134,7 +129,6 @@ 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 1968455..f2eb6c4 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.4.0 - version: 1.4.0 + specifier: ^1.3.0 + version: 1.3.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.4.0 - version: 1.4.0 + specifier: ^1.3.0 + version: 1.3.0 unified: specifier: ^11.0.4 version: 11.0.4 @@ -1422,25 +1422,25 @@ packages: - supports-color dev: true - /@shikijs/core@1.4.0: - resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==} + /@shikijs/core@1.3.0: + resolution: {integrity: sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==} dev: false - /@shikijs/rehype@1.4.0: - resolution: {integrity: sha512-Ba6QHYx+EIEvmqyNy/B49KAz3rXsTfAqYRY3KTZjPWonytokGOiJ1q/FV9l13D/ad6Qv+eWKhkAz6ITxx6ziFA==} + /@shikijs/rehype@1.3.0: + resolution: {integrity: sha512-CknEidx0ZTg3TeYAPU4ah8cr31a16neBbMyQ5kwAVdkloCe65uhQp+C/FEFs8NRir4eU5XCDA/+w2v5wnN6zgQ==} dependencies: - '@shikijs/transformers': 1.4.0 + '@shikijs/transformers': 1.3.0 '@types/hast': 3.0.4 hast-util-to-string: 3.0.0 - shiki: 1.4.0 + shiki: 1.3.0 unified: 11.0.4 unist-util-visit: 5.0.0 dev: false - /@shikijs/transformers@1.4.0: - resolution: {integrity: sha512-kzvlWmWYYSeaLKRce/kgmFFORUtBtFahfXRKndor0b60ocYiXufBQM6d6w1PlMuUkdk55aor9xLvy9wy7hTEJg==} + /@shikijs/transformers@1.3.0: + resolution: {integrity: sha512-3mlpg2I9CjhjE96dEWQOGeCWoPcyTov3s4aAsHmgvnTHa8MBknEnCQy8/xivJPSpD+olqOqIEoHnLfbNJK29AA==} dependencies: - shiki: 1.4.0 + shiki: 1.3.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.4.0: - resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==} + /shiki@1.3.0: + resolution: {integrity: sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==} dependencies: - '@shikijs/core': 1.4.0 + '@shikijs/core': 1.3.0 dev: false /signal-exit@3.0.7: