From 9010ba833a513f43cebc8d7882a63874956fe87d Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 22 Sep 2024 13:18:24 +0300 Subject: [PATCH 1/4] fix(code-block): client-side line numbers --- elements/rh-code-block/prism.ts | 5 ++- elements/rh-code-block/rh-code-block.css | 17 ++++++++++ elements/rh-code-block/rh-code-block.ts | 43 +++++++++++------------- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/elements/rh-code-block/prism.ts b/elements/rh-code-block/prism.ts index bd749670e2..d4fb472428 100644 --- a/elements/rh-code-block/prism.ts +++ b/elements/rh-code-block/prism.ts @@ -1,10 +1,9 @@ import type { RhCodeBlock } from './rh-code-block.js'; import { Prism } from 'prism-esm'; -import { Plugin as LineNumbers } from 'prism-esm/plugins/line-numbers/prism-line-numbers.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { html } from 'lit'; const prism = new Prism({ manual: true }); -LineNumbers(prism); /** * Autoload a supported language @@ -37,7 +36,7 @@ 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 unsafeHTML(highlighted); + return html`${unsafeHTML(highlighted)}`; } export { prismStyles } 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 95e5cf4723..426f581d1a 100644 --- a/elements/rh-code-block/rh-code-block.css +++ b/elements/rh-code-block/rh-code-block.css @@ -63,6 +63,13 @@ #prism-output { margin: 0; + + & code { + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + } } #container { @@ -143,6 +150,7 @@ opacity: 0; pointer-events: none; z-index: -10000; + line-height: var(--rh-line-height-code, 1.5); } #line-numbers { @@ -157,6 +165,11 @@ color: var(--rh-color-text-secondary); font-weight: var(--rh-font-weight-code-regular, 400); border-inline-end: var(--rh-border-width-sm, 1px) solid var(--rh-color-border-subtle); + + li { + line-height: var(--rh-line-height-code, 1.5); + display: block; + } } #actions { @@ -330,3 +343,7 @@ --_important-color: var(--rh-color-purple-30, #b6a6e9); --_variable-color: var(--rh-color-green-40, #87bb62); } + +:host([highlighting='client']) #content::slotted(:is(script, pre)) { + display: none; +} diff --git a/elements/rh-code-block/rh-code-block.ts b/elements/rh-code-block/rh-code-block.ts index 4cb6c202bc..b3a1ead522 100644 --- a/elements/rh-code-block/rh-code-block.ts +++ b/elements/rh-code-block/rh-code-block.ts @@ -25,7 +25,8 @@ import style from './rh-code-block.css'; function dedent(str: string) { const stripped = str.replace(/^\n/, ''); const match = stripped.match(/^\s+/); - return match ? stripped.replace(new RegExp(`^${match[0]}`, 'gm'), '') : str; + const out = match ? stripped.replace(new RegExp(`^${match[0]}`, 'gm'), '') : str; + return out.trim(); } interface CodeLineHeightsInfo { @@ -273,37 +274,33 @@ export class RhCodeBlock extends LitElement { : []); } - #getPrismCodeElements() { - const container = this.shadowRoot?.getElementById('prism-output') as HTMLSlotElement; - return Array.from(container.children); - } - /** * Clone the text content and connect it to the document, in order to calculate the number of lines * @license MIT * Portions copyright prism.js authors (MIT license) */ async #computeLineNumbers() { - if (this.#prismOutput) { - return; - } - await this.updateComplete; - const codes = this.#prismOutput ? this.#getPrismCodeElements() : this.#getSlottedCodeElements(); + const codes = + this.#prismOutput ? [this.shadowRoot?.getElementById('prism-output')].filter(x => !!x) + : this.#getSlottedCodeElements(); const infos: CodeLineHeightsInfo[] = codes.map(element => { - const sizer = document.createElement('span'); - sizer.className = 'sizer'; - sizer.innerText = '0'; - sizer.style.display = 'block'; - this.shadowRoot?.getElementById('sizers')?.appendChild(sizer); - return { - lines: element.textContent?.split(/\n(?!$)/g) ?? [], - lineHeights: [], - sizer, - oneLinerHeight: sizer.getBoundingClientRect().height, - }; - }); + const codeElement = this.#prismOutput ? element.querySelector('code') : element; + if (codeElement) { + const sizer = document.createElement('span'); + sizer.className = 'sizer'; + sizer.innerText = '0'; + sizer.style.display = 'block'; + this.shadowRoot?.getElementById('sizers')?.appendChild(sizer); + return { + lines: element.textContent?.split(/\n(?!$)/g) ?? [], + lineHeights: [], + sizer, + oneLinerHeight: sizer.getBoundingClientRect().height, + }; + } + }).filter(x => !!x); for (const { lines, lineHeights, sizer, oneLinerHeight } of infos) { lineHeights[lines.length - 1] = undefined; // why? From 73fc6a46e5622c5b73eab26e8550b0724f7caca3 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 22 Sep 2024 22:31:09 +0300 Subject: [PATCH 2/4] 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 = From 10dc69d89205a650ba70ee350540bfba84a0593b Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 22 Sep 2024 22:31:32 +0300 Subject: [PATCH 3/4] fix(code-block): badge padding --- elements/rh-badge/rh-badge.css | 2 +- elements/rh-code-block/rh-code-block.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/elements/rh-badge/rh-badge.css b/elements/rh-badge/rh-badge.css index ca5a4c6d0e..6e491ff2e2 100644 --- a/elements/rh-badge/rh-badge.css +++ b/elements/rh-badge/rh-badge.css @@ -13,7 +13,7 @@ span { font-size: var(--rh-font-size-body-text-xs, 0.75rem); font-weight: 700; line-height: var(--rh-line-height-body-text, 1.5); - padding-inline: var(--rh-space-md, 8px); + padding-inline: var(--_badge-padding, var(--rh-space-md, 8px)); } .on.dark { background-color: var(--rh-color-surface-darker, #1f1f1f); } diff --git a/elements/rh-code-block/rh-code-block.css b/elements/rh-code-block/rh-code-block.css index ebf30cb42d..faf1fcd385 100644 --- a/elements/rh-code-block/rh-code-block.css +++ b/elements/rh-code-block/rh-code-block.css @@ -2,6 +2,7 @@ --rh-code-block-callout-size: var(--rh-size-icon-02, 24px); --_aspect-ratio: 1; --_badge-size: var(--rh-code-block-callout-size); + --_badge-padding: 0; display: block; max-width: 1000px; From 50ba39204dbd947a51591d7b64fa8e7d91a45e47 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 23 Sep 2024 16:01:35 +0300 Subject: [PATCH 4/4] fix: change prism-prerendered to prerendered --- .changeset/great-adults-hear.md | 4 ++-- .../demo/prerendered-prism-highlighting.html | 6 +++--- elements/rh-code-block/rh-code-block.css | 2 +- elements/rh-code-block/rh-code-block.ts | 12 +++++++++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.changeset/great-adults-hear.md b/.changeset/great-adults-hear.md index 6ff5b8feaf..704268400c 100644 --- a/.changeset/great-adults-hear.md +++ b/.changeset/great-adults-hear.md @@ -2,11 +2,11 @@ "@rhds/elements": minor --- ``: syntax highlighting via prerendered prismjs code-blocks. Use -the `highlighting="prism-prerendered"` attribute when rendering code blocks using +the `highlighting="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/prerendered-prism-highlighting.html b/elements/rh-code-block/demo/prerendered-prism-highlighting.html index e8a0820a9a..9fa73818bb 100644 --- a/elements/rh-code-block/demo/prerendered-prism-highlighting.html +++ b/elements/rh-code-block/demo/prerendered-prism-highlighting.html @@ -1,5 +1,5 @@ - +
<!DOCTYPE html>
 <html lang="en">
   <head>
@@ -24,7 +24,7 @@
 </html>
- +
rh-card.avatar-card {
   width: 360px;
   &::part(body) {
@@ -44,7 +44,7 @@
 }
- +
extends:
   - stylelint-config-standard
   - '@stylistic/stylelint-config'
diff --git a/elements/rh-code-block/rh-code-block.css b/elements/rh-code-block/rh-code-block.css
index faf1fcd385..c602d9c3d8 100644
--- a/elements/rh-code-block/rh-code-block.css
+++ b/elements/rh-code-block/rh-code-block.css
@@ -26,7 +26,7 @@
   border: none !important;
 }
 
-:host([highlighting='prism-prerendered']) ::slotted(pre) {
+:host([highlighting='prerendered']) ::slotted(pre) {
   opacity: 0;
   transition:
     opacity
diff --git a/elements/rh-code-block/rh-code-block.ts b/elements/rh-code-block/rh-code-block.ts
index d688123f16..3469cc543c 100644
--- a/elements/rh-code-block/rh-code-block.ts
+++ b/elements/rh-code-block/rh-code-block.ts
@@ -95,8 +95,12 @@ export class RhCodeBlock extends LitElement {
     },
   }) actions: ('copy' | 'wrap')[] = [];
 
-  /** When set to "client", `` will automatically highlight the source code using Prism.js */
-  @property() highlighting?: 'client' | 'prism-prerendered';
+  /**
+   * When set to "client", `` will automatically highlight the source using Prism.js
+   * When set to "Prerendered", `` will apply supported RHDS styles to children with
+   * prismjs classnames in the element's root.
+   */
+  @property() highlighting?: 'client' | 'prerendered';
 
   /** When set along with `highlighting="client"`, this grammar will be used to highlight source code */
   @property() language?:
@@ -229,7 +233,9 @@ export class RhCodeBlock extends LitElement {
   async #onSlotChange() {
     switch (this.highlighting) {
       case 'client': await this.#highlightWithPrism(); break;
-      case 'prism-prerendered': await this.#applyPrismPrerenderedStyles(); break;
+      // TODO: if we ever support other tokenizers e.g. highlightjs,
+      // dispatch here off of some supplemental attribute like `tokenizer="highlightjs"`
+      case 'prerendered': await this.#applyPrismPrerenderedStyles(); break;
     }
     this.#computeLineNumbers();
   }