From 73fc6a46e5622c5b73eab26e8550b0724f7caca3 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 22 Sep 2024 22:31:09 +0300 Subject: [PATCH] feat(code-block): server-side prerendered prism --- .changeset/great-adults-hear.md | 14 ++ .../rh-code-block/demo/callout-badges.html | 4 +- .../demo/prerendered-prism-highlighting.html | 75 ++++++++++ elements/rh-code-block/prism.css.ts | 133 ++++++++++-------- elements/rh-code-block/prism.ts | 4 +- elements/rh-code-block/rh-code-block.css | 8 ++ elements/rh-code-block/rh-code-block.ts | 16 ++- 7 files changed, 185 insertions(+), 69 deletions(-) create mode 100644 .changeset/great-adults-hear.md create mode 100644 elements/rh-code-block/demo/prerendered-prism-highlighting.html diff --git a/.changeset/great-adults-hear.md b/.changeset/great-adults-hear.md new file mode 100644 index 0000000000..6ff5b8feaf --- /dev/null +++ b/.changeset/great-adults-hear.md @@ -0,0 +1,14 @@ +--- +"@rhds/elements": minor +--- +``: 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 + +
a {
+  color: var(--rh-color-interactive-primary-default);
+  }
+
+``` diff --git a/elements/rh-code-block/demo/callout-badges.html b/elements/rh-code-block/demo/callout-badges.html index 4c899f8349..fc0f285a7f 100644 --- a/elements/rh-code-block/demo/callout-badges.html +++ b/elements/rh-code-block/demo/callout-badges.html @@ -1,9 +1,9 @@ - 1 + 1 22
1
diff --git a/elements/rh-code-block/demo/prerendered-prism-highlighting.html b/elements/rh-code-block/demo/prerendered-prism-highlighting.html new file mode 100644 index 0000000000..e8a0820a9a --- /dev/null +++ b/elements/rh-code-block/demo/prerendered-prism-highlighting.html @@ -0,0 +1,75 @@ + + +
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width">
+    <title>Cards Galore!</title>
+  </head>
+  <body>
+    <main>
+      <rh-card>
+        <h2 slot="header">Card</h2>
+        <p>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.</p>
+        <rh-cta slot="footer" priority="primary">
+          <a href="#">Call to action</a>
+        </rh-cta>
+      </rh-card>
+    </main>
+  </body>
+</html>
+
+ + +
rh-card.avatar-card {
+  width: 360px;
+  &::part(body) {
+    margin-block-start: var(--rh-space-lg, 16px);
+  }
+
+  & p {
+    margin-block-start: 0;
+  }
+
+  & h4 {
+    font-weight: var(--rh-font-weight-heading-regular, 300);
+    font-size: var(--rh-font-size-body-text-md, 1rem);
+    font-family: var(--rh-font-family-body-text);
+    line-height: var(--rh-line-height-body-text, 1.5);
+  }
+}
+
+ + +
extends:
+  - stylelint-config-standard
+  - '@stylistic/stylelint-config'
+
+plugins:
+  - ./node_modules/@rhds/tokens/plugins/stylelint.js
+  - '@stylistic/stylelint-plugin'
+
+rules:
+  rhds/token-values: true
+  rhds/no-unknown-token-name:
+    - true
+    - allowed:
+      - --rh-icon-size
+
+
+ + + + diff --git a/elements/rh-code-block/prism.css.ts b/elements/rh-code-block/prism.css.ts index 3477c2ab69..248bef9261 100644 --- a/elements/rh-code-block/prism.css.ts +++ b/elements/rh-code-block/prism.css.ts @@ -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; } +}`; diff --git a/elements/rh-code-block/prism.ts b/elements/rh-code-block/prism.ts index d4fb472428..f4512eafeb 100644 --- a/elements/rh-code-block/prism.ts +++ b/elements/rh-code-block/prism.ts @@ -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`${unsafeHTML(highlighted)}`; + return html`${unsafeHTML(highlighted)}`; } -export { prismStyles } from './prism.css.js'; +export { prismStyles, preRenderedLightDomStyles } from './prism.css.js'; diff --git a/elements/rh-code-block/rh-code-block.css b/elements/rh-code-block/rh-code-block.css index 426f581d1a..ebf30cb42d 100644 --- a/elements/rh-code-block/rh-code-block.css +++ b/elements/rh-code-block/rh-code-block.css @@ -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; diff --git a/elements/rh-code-block/rh-code-block.ts b/elements/rh-code-block/rh-code-block.ts index b3a1ead522..d688123f16 100644 --- a/elements/rh-code-block/rh-code-block.ts +++ b/elements/rh-code-block/rh-code-block.ts @@ -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 @@ -97,7 +96,7 @@ export class RhCodeBlock extends LitElement { }) actions: ('copy' | 'wrap')[] = []; /** When set to "client", `` 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?: @@ -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 =