Skip to content

Commit

Permalink
APP-7173: Add Prism code highlighting action (#605)
Browse files Browse the repository at this point in the history
Co-authored-by: emily hong <[email protected]>
  • Loading branch information
mcous and ehhong authored Dec 16, 2024
1 parent d0a2082 commit 8ec2288
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 105 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@viamrobotics/prime-core",
"version": "0.0.169",
"version": "0.0.170",
"repository": {
"type": "git",
"url": "https://github.com/viamrobotics/prime.git",
Expand Down
10 changes: 0 additions & 10 deletions packages/core/src/lib/__tests__/code-snippet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,6 @@ describe('CodeSnippet', () => {
expect(button).toBeNull();
});

it('Renders with the passed dependencies', () => {
render(CodeSnippet, { ...common, dependencies: ['dep1', 'dep2'] });

const code = screen
.getByRole('figure')
.querySelector('pre > code.language-json');

expect(code).toHaveAttribute('data-dependencies', 'dep1,dep2');
});

it('Renders with a figcaption when the default slot is provided', () => {
render(CaptionedCodeSnippet);

Expand Down
94 changes: 5 additions & 89 deletions packages/core/src/lib/code-snippet.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@ https://prismjs.com
context="module"
lang="ts"
>
import Prism from 'prismjs';
import PrismPackage from 'prismjs/package.json';
import 'prism-themes/themes/prism-vs.min.css';
const PRISM_VERSION = PrismPackage.version;
type CodeSnippetCopyState = 'copy' | 'copied' | 'failed';
const COPY_STATES: Record<
CodeSnippetCopyState,
Expand All @@ -35,11 +29,11 @@ const COPY_STATES: Record<
</script>

<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
import cx from 'classnames';
import { IconButton, type IconName, useTimeout } from '$lib';
import { escape } from 'lodash-es';
import { highlightCode } from '$lib/highlight-code';
/**
* The language to use for syntax highlighting. Must be a language supported by
Expand All @@ -57,30 +51,11 @@ export let code: string;
*/
export let showCopyButton = true;
/**
* Some prism languages have dependencies. For example, C++ requires C. If the
* passed `language` has dependencies, they must be included here to be loaded.
*
* See: https://prismjs.com/plugins/autoloader/
*/
export let dependencies: string[] = [];
/**
* We use the prism autoloader to handle loading in language grammar files. The
* default path for the grammar files is a CDN link so we don't have to include
* grammar files in our bundle. If you prefer to point to your own grammars and
* bypass the CDN, define the path to those files.
*
* See: https://prismjs.com/plugins/autoloader/
*/
export let grammarsPath = `https://cdnjs.cloudflare.com/ajax/libs/prism/${PRISM_VERSION}/components/`;
/** Additional CSS classes to pass to the aside. */
let extraClasses: cx.Argument = '';
export { extraClasses as cx };
const { set: setCopyTimeout } = useTimeout();
let element: HTMLElement | undefined;
$: copyState = 'copy' as CodeSnippetCopyState;
Expand Down Expand Up @@ -110,44 +85,6 @@ const copyToClipboard = async () => {
copyState = 'copy';
}, 2000);
};
const highlight = () => {
if (element) {
/**
* We need to reset the inner HTML of the element to the raw value of
* `code` so it can rescanned for highlighting from the raw.
*/
element.innerHTML = escape(code);
Prism.highlightElement(element);
}
};
$: if (code) {
highlight();
}
onMount(async () => {
try {
/**
* After the HTML is loaded in the DOM, we can use the autoloader to manage
* scanning for languages and including the components properly
*/
await import(
// @ts-expect-error no type declaration for this JS file
'prismjs/plugins/autoloader/prism-autoloader'
);
// Make sure the autoloader knows where to find our languages
(Prism.plugins.autoloader as { languages_path: string }).languages_path =
grammarsPath;
// Do the initial highlighting
highlight();
} catch (error) {
/* eslint-disable-next-line no-console */
console.error('Error initializing prismjs', error);
}
});
</script>

<figure class={cx('flex flex-col gap-2', extraClasses)}>
Expand All @@ -157,11 +94,9 @@ onMount(async () => {

<div class="flex gap-x-4 bg-light p-4">
<!-- The formatting here is intentional to preserve the formatting of `code` -->
<pre class="flex-1 overflow-x-auto language-{language}"><code
bind:this={element}
class="language-{language}"
data-dependencies={dependencies.join(',')}>{code}</code
></pre>
<pre
class="flex-1 overflow-x-auto"
use:highlightCode><code class="language-{language}">{code}</code></pre>

{#if showCopyButton}
<IconButton
Expand All @@ -174,22 +109,3 @@ onMount(async () => {
{/if}
</div>
</figure>

<style>
/* Theme overrides */
figure pre[class*='language-'],
figure pre[class*='language-'] > code[class*='language-'] {
margin: 0;
border: none;
background: inherit;
}
figure pre[class*='language-'] {
padding: 0;
}
figure pre[class*='language-'] > code[class*='language-'] {
font-family: 'Roboto Mono Variable', 'Roboto Mono', ui-monospace, monospace;
font-size: 1em;
}
</style>
36 changes: 36 additions & 0 deletions packages/core/src/lib/highlight-code/highlight-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Action } from 'svelte/action';
import { getPrismModule } from './prism';

/**
* Highlight code in any child `<pre><code /></pre>` blocks.
* Elements should be styled according to Prism's expected HTML structure.
* highlighting only runs on mount of the element, so the element using the action should be a keyed element to force re-highlighting on updates.
*
* Usage example:
* ```svelte
* <script>
* import { highlightCode } from '@viamrobotics/prime-core';
* export let code: string;
* </script>
*
* {#key code}
* <div use:highlightCode>
* <pre><code class="language-javascript">{code}</code></pre>
* </div>
* {/key}
* ```
*
* @param node - The node to highlight.
*/
export const highlightCode: Action<HTMLElement | undefined> = (
node: HTMLElement | undefined
) => {
if (node) {
highlightContainerElement(node);
}
};

const highlightContainerElement = (element: Element) => {
const prism = getPrismModule();
prism.highlightAllUnder(element);
};
1 change: 1 addition & 0 deletions packages/core/src/lib/highlight-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { highlightCode } from './highlight-code';
25 changes: 25 additions & 0 deletions packages/core/src/lib/highlight-code/prism.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Wrapper module for Prism.js
*/

import PrismPackage from 'prismjs/package.json';
import Prism from 'prismjs';
import './viam-prism-theme.css';

/**
* We use the prism autoloader to handle loading in language grammar files. The
* default path for the grammar files is a CDN link so we don't have to include
* grammar files in our bundle. If you prefer to point to your own grammars and
* bypass the CDN, define the path to those files.
*
* See: https://prismjs.com/plugins/autoloader/
*/
import 'prismjs/plugins/autoloader/prism-autoloader';
const grammarsPath = `https://cdnjs.cloudflare.com/ajax/libs/prism/${PrismPackage.version}/components/`;

export const getPrismModule = (): typeof Prism => {
// Make sure the autoloader knows where to find our languages
(Prism.plugins.autoloader as { languages_path: string }).languages_path =
grammarsPath;
return Prism;
};
17 changes: 17 additions & 0 deletions packages/core/src/lib/highlight-code/viam-prism-theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import 'prism-themes/themes/prism-vs.min.css';

pre[class*='language-'],
pre[class*='language-'] > code[class*='language-'] {
margin: 0;
border: none;
background: inherit;
}

pre[class*='language-'] {
padding: 0;
}

pre[class*='language-'] > code[class*='language-'] {
font-family: 'Roboto Mono Variable', 'Roboto Mono', ui-monospace, monospace;
font-size: 1em;
}
1 change: 1 addition & 0 deletions packages/core/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ export * from './keyboard';
export { useTimeout } from './use-timeout';
export { uniqueId } from './unique-id';
export { default as VectorInput } from './vector-input.svelte';
export { highlightCode } from './highlight-code';
8 changes: 5 additions & 3 deletions packages/core/src/lib/modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Creates a modal overlay.

<script lang="ts">
import cx from 'classnames';
import { createEventDispatcher, onDestroy } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import IconButton from './button/icon-button.svelte';
import { clickOutside } from '$lib';
Expand Down Expand Up @@ -73,8 +73,10 @@ $: if (typeof document !== 'undefined') {
document.body.classList.toggle('overflow-hidden', isOpen);
}
onDestroy(() => {
document.body.classList.remove('overflow-hidden');
onMount(() => {
return () => {
document.body.classList.remove('overflow-hidden');
};
});
</script>

Expand Down
1 change: 0 additions & 1 deletion packages/core/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,6 @@ const onHoverDelayMsInput = (event: Event) => {
<CodeSnippet
language="cpp"
code={cppSnippet}
dependencies={['c']}
/>

<h2 class="text-lg text-subtle-1">Dart</h2>
Expand Down
1 change: 0 additions & 1 deletion packages/storybook/src/stories/code-snippet.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ log_hello_world()
<Story name="CPlusPlus">
<CodeSnippet
language="cpp"
dependencies={['c']}
code={`
#include <iostream>
Expand Down

0 comments on commit 8ec2288

Please sign in to comment.