Compare commits
	
		
			28 commits
		
	
	
		
	
	| 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 | ||
|  | 036af273f9 | 
					 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 }); | ||||
| 		retrieveHeaders(); | ||||
| 	}); | ||||
| 
 | ||||
| 	onDestroy(() => { | ||||
| 		observer?.disconnect(); | ||||
| 	onNavigate(() => { | ||||
| 		setTimeout(() => { | ||||
| 			retrieveHeaders(); | ||||
| 			highlightHeader(); | ||||
| 		}, 300); | ||||
| 	}); | ||||
| 	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]} | ||||
| 		{#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href} | ||||
| 			{#key scrollY} | ||||
| 		{#key selectedHeaderIndex} | ||||
| 			{#if header.children[0] instanceof HTMLAnchorElement && header.children[0].href} | ||||
| 				<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} | ||||
| 			{/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
		Add a link
		
	
		Reference in a new issue