Skip to content

Commit

Permalink
feat(code-block): server-side prerendered prism
Browse files Browse the repository at this point in the history
  • Loading branch information
bennypowers committed Sep 22, 2024
1 parent 9010ba8 commit 73fc6a4
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 69 deletions.
14 changes: 14 additions & 0 deletions .changeset/great-adults-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@rhds/elements": minor
---
`<rh-code-block>`: syntax highlighting via prerendered prismjs code-blocks. Use
the `highlighting="prism-prerendered"` attribute when rendering code blocks using
server side prism, e.g. in a markdown fenced code block.

```html
<rh-code-block highlighting="prism-prerendered">
<pre class="language-css"><code class="language-css"><span class="token selector">a</span> <span class="token punctuation">{</span>
<span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--rh-color-interactive-primary-default<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></pre>
</rh-code-block>
```
4 changes: 2 additions & 2 deletions elements/rh-code-block/demo/callout-badges.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<rh-code-block>
<script type="text/html"><p>Script tags in HTML must be escaped</p></script>
<rh-badge state="info"">1</rh-badge>
<rh-badge state="info">1</rh-badge>
<script type="text/html">
<p>other tags do not need to be escaped</p>
<marquee>Callouts can be placed </script><rh-badge state="info"">2</rh-badge><script type="text/html">inline</marquee>
<marquee>Callouts can be placed </script><rh-badge state="info">2</rh-badge><script type="text/html">inline</marquee>
</script>
<dl slot="legend">
<dt><rh-badge state="info">1</rh-badge></dt>
Expand Down
75 changes: 75 additions & 0 deletions elements/rh-code-block/demo/prerendered-prism-highlighting.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<rh-context-demo>
<rh-code-block highlighting="prism-prerendered">
<pre class="language-html"><code class="language-html"><span class="token doctype"><span class="token punctuation">&lt;!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>utf-8<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">&gt;</span></span>Cards Galore!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>rh-card</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span> <span class="token attr-name">slot</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>header<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Card<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">&gt;</span></span>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam eleifend elit sed est egestas, a sollicitudin mauris
tincidunt. Pellentesque vel dapibus risus. Nullam aliquam
felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>rh-cta</span> <span class="token attr-name">slot</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>footer<span class="token punctuation">"</span></span> <span class="token attr-name">priority</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>primary<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Call to action<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>rh-cta</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>rh-card</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">&gt;</span></span></code></pre>
</rh-code-block>

<rh-code-block highlighting="prism-prerendered">
<pre class="language-css"><code class="language-css"><span class="token selector">rh-card.avatar-card</span> <span class="token punctuation">{</span>
<span class="token property">width</span><span class="token punctuation">:</span> 360px<span class="token punctuation">;</span>
<span class="token selector">&amp;::part(body)</span> <span class="token punctuation">{</span>
<span class="token property">margin-block-start</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--rh-space-lg<span class="token punctuation">,</span> 16px<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token selector">&amp; p</span> <span class="token punctuation">{</span>
<span class="token property">margin-block-start</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token selector">&amp; h4</span> <span class="token punctuation">{</span>
<span class="token property">font-weight</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--rh-font-weight-heading-regular<span class="token punctuation">,</span> 300<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--rh-font-size-body-text-md<span class="token punctuation">,</span> 1rem<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--rh-font-family-body-text<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token property">line-height</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--rh-line-height-body-text<span class="token punctuation">,</span> 1.5<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
</rh-code-block>

<rh-code-block highlighting="prism-prerendered">
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">extends</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> stylelint<span class="token punctuation">-</span>config<span class="token punctuation">-</span>standard
<span class="token punctuation">-</span> <span class="token string">'@stylistic/stylelint-config'</span>

<span class="token key atrule">plugins</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> ./node_modules/@rhds/tokens/plugins/stylelint.js
<span class="token punctuation">-</span> <span class="token string">'@stylistic/stylelint-plugin'</span>

<span class="token key atrule">rules</span><span class="token punctuation">:</span>
<span class="token key atrule">rhds/token-values</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">rhds/no-unknown-token-name</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token boolean important">true</span>
<span class="token punctuation">-</span> <span class="token key atrule">allowed</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token punctuation">-</span><span class="token punctuation">-</span>rh<span class="token punctuation">-</span>icon<span class="token punctuation">-</span>size</code></pre>
</rh-code-block>
</rh-context-demo>

<script type="module">
import '@rhds/elements/lib/elements/rh-context-demo/rh-context-demo.js';
import '@rhds/elements/rh-code-block/rh-code-block.js';
</script>

<style>
rh-context-demo::part(demo) {
display: grid;
gap: var(--rh-space-sm, 6px);
}
</style>
133 changes: 71 additions & 62 deletions elements/rh-code-block/prism.css.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,82 @@
import { css } from 'lit';

export const prismStyles = css` code[class*="language-"],
pre[class*="language-"] {
color: var(--_code-color);
font-family: var(--rh-font-family-code, RedHatMono, "Red Hat Mono", "Courier New", Courier, monospace);
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: var(--rh-line-height-code, 1.5);
tab-size: 4;
hyphens: none;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: var(--_selected-text-background);
}
const styles = css`
& code[class*="language-"],
& pre[class*="language-"] {
color: var(--_code-color);
font-family: var(--rh-font-family-code, RedHatMono, "Red Hat Mono", "Courier New", Courier, monospace);
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: var(--rh-line-height-code, 1.5);
tab-size: 4;
hyphens: none;
background: transparent;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
& pre[class*="language-"]::selection,
& pre[class*="language-"] ::selection,
& code[class*="language-"]::selection,
& code[class*="language-"] ::selection {
text-shadow: none;
background: var(--_selected-text-background);
}
@media print {
& code[class*="language-"],
& pre[class*="language-"] {
text-shadow: none;
}
}
}
.token.atrule { color: var(--_at-rule-color); }
.token.attr-name { color: var(--_attr-name-color); }
.token.attr-value { color: var(--_attr-value-color); }
.token.bold { font-weight: var(--_important-color); }
.token.boolean { color: var(--_boolean-color); }
.token.builtin { color: var(--_built-in-color); }
.token.cdata { color: var(--_cdata-color); }
.token.char { color: var(--_character-color); }
.token.class-name { color: var(--_class-name-color); }
.token.comment { color: var(--_comment-color); }
.token.constant { color: var(--_constant-color); }
.token.deleted { color: var(--_deleted-color); }
.token.function { color: var(--_function-name-color); }
.token.important { color: var(--_important-color); }
.token.inserted { color: var(--_inserted-color); }
.token.keyword { color: var(--_keyword-color); }
.token.namespace { color: var(--_namespace-color); }
.token.number { color: var(--_number-color); }
.token.operator { color: var(--_operator-color); }
.token.property { color: var(--_property-color); }
.token.punctuation { color: var(--_punctuation-color); }
.token.regex { color: var(--_regex-color); }
.token.selector { color: var(--_selector-color); }
.token.string { color: var(--_string-color); }
.token.symbol { color: var(--_symbol-color); }
.token.tag { color: var(--_tag-color); }
.token.url { color: var(--_url-color); }
.token.variable { color: var(--_variable-color); }
& .token.atrule { color: var(--_at-rule-color); }
& .token.attr-name { color: var(--_attr-name-color); }
& .token.attr-value { color: var(--_attr-value-color); }
& .token.bold { font-weight: var(--_important-color); }
& .token.boolean { color: var(--_boolean-color); }
& .token.builtin { color: var(--_built-in-color); }
& .token.cdata { color: var(--_cdata-color); }
& .token.char { color: var(--_character-color); }
& .token.class-name { color: var(--_class-name-color); }
& .token.comment { color: var(--_comment-color); }
& .token.constant { color: var(--_constant-color); }
& .token.deleted { color: var(--_deleted-color); }
& .token.function { color: var(--_function-name-color); }
& .token.important { color: var(--_important-color); }
& .token.inserted { color: var(--_inserted-color); }
& .token.keyword { color: var(--_keyword-color); }
& .token.namespace { color: var(--_namespace-color); }
& .token.number { color: var(--_number-color); }
& .token.operator { color: var(--_operator-color); }
& .token.property { color: var(--_property-color); }
& .token.punctuation { color: var(--_punctuation-color); }
& .token.regex { color: var(--_regex-color); }
& .token.selector { color: var(--_selector-color); }
& .token.string { color: var(--_string-color); }
& .token.symbol { color: var(--_symbol-color); }
& .token.tag { color: var(--_tag-color); }
& .token.url { color: var(--_url-color); }
& .token.variable { color: var(--_variable-color); }
.token.italic { font-style: italic; }
& .token.italic { font-style: italic; }
.token.entity {
color: var(--_entity-color);
cursor: help;
}
& .token.entity {
color: var(--_entity-color);
cursor: help;
}
.token.prolog,
.token.doctype { color: var(--_doctype-color); }
& .token.prolog,
& .token.doctype { color: var(--_doctype-color); }
.language-css .token.string,
.style .token.string { color: var(--_operator-color); }
& .language-css .token.string,
& .style .token.string { color: var(--_operator-color); }
`;

export const prismStyles = css`#prism-output {${styles}}`;
export const preRenderedLightDomStyles = css`rh-code-block {
--_styles-applied: true;
${styles}
& > pre { opacity: 1; }
}`;
4 changes: 2 additions & 2 deletions elements/rh-code-block/prism.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ async function autoloader(language: RhCodeBlock['language']) {
export async function highlight(textContent: string, language: RhCodeBlock['language']) {
await autoloader(language);
const highlighted = prism.highlight(textContent, prism.languages[language!], language!);
return html`<code>${unsafeHTML(highlighted)}</code>`;
return html`<code class="language-${language}">${unsafeHTML(highlighted)}</code>`;
}

export { prismStyles } from './prism.css.js';
export { prismStyles, preRenderedLightDomStyles } from './prism.css.js';

8 changes: 8 additions & 0 deletions elements/rh-code-block/rh-code-block.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
border: none !important;
}

:host([highlighting='prism-prerendered']) ::slotted(pre) {
opacity: 0;
transition:
opacity
var(--rh-animation-speed)
var(--rh-animation-timing, cubic-bezier(0.465, 0.183, 0.153, 0.946));
}

.shadow-fab {
display: flex;
align-items: center;
Expand Down
16 changes: 13 additions & 3 deletions elements/rh-code-block/rh-code-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { type ColorTheme, colorContextConsumer } from '../../lib/context/color/c

import style from './rh-code-block.css';


/* TODO
* - style slotted and shadow fake-fabs
* - manage state of copy and wrap, including if they are slotted. see actions.html
Expand Down Expand Up @@ -97,7 +96,7 @@ export class RhCodeBlock extends LitElement {
}) actions: ('copy' | 'wrap')[] = [];

/** When set to "client", `<rh-code-block>` will automatically highlight the source code using Prism.js */
@property() highlighting?: 'client';
@property() highlighting?: 'client' | 'prism-prerendered';

/** When set along with `highlighting="client"`, this grammar will be used to highlight source code */
@property() language?:
Expand Down Expand Up @@ -229,11 +228,22 @@ export class RhCodeBlock extends LitElement {

async #onSlotChange() {
switch (this.highlighting) {
case 'client': await this.#highlightWithPrism();
case 'client': await this.#highlightWithPrism(); break;
case 'prism-prerendered': await this.#applyPrismPrerenderedStyles(); break;
}
this.#computeLineNumbers();
}

async #applyPrismPrerenderedStyles() {
if (getComputedStyle(this).getPropertyValue('--_styles-applied') !== 'true') {
const root = this.getRootNode();
if (root instanceof Document || root instanceof ShadowRoot) {
const { preRenderedLightDomStyles: { styleSheet } } = await import('./prism.js');
root.adoptedStyleSheets = [...root.adoptedStyleSheets, styleSheet!];
}
}
}

async #highlightWithPrism() {
const { highlight, prismStyles } = await import('./prism.js');
const styleSheet =
Expand Down

0 comments on commit 73fc6a4

Please sign in to comment.