diff --git a/.eslintrc.react.yml b/.eslintrc.react.yml index afd0f5284c..f50647a56a 100644 --- a/.eslintrc.react.yml +++ b/.eslintrc.react.yml @@ -96,7 +96,7 @@ rules: react-hooks/rules-of-hooks: error react-hooks/exhaustive-deps: - warn - - additionalHooks: ^(useLiveRegion|useMemoized)$ + - additionalHooks: ^(useLiveRegion|useMemoized|use(\w+)Updater)$ # Conflicts with Adaptive Card schema. # react/forbid-prop-types: error diff --git a/CHANGELOG.md b/CHANGELOG.md index b29c7b505d..d6b6f54327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/ - Configure HTML sanitizer via `request.allowedTags` - Added support for math blocks using `$$` delimiter alongside existing `\[...\]` and `\(...\)` notations, in PR [#5381](https://github.com/microsoft/BotFramework-WebChat/pull/5381), by [@OEvgeny](https://github.com/OEvgeny) - Added support for speech recognition initial silence timeout when using Azure Speech, in PR [#5400](https://github.com/microsoft/BotFramework/WebChat/pull/5400), by [@compulim](https://github.com/compulim) +- Introduced syntax highlighting for markdown code blocks, in PR [#5389](https://github.com/microsoft/BotFramework-WebChat/pull/5389), by [@OEvgeny](https://github.com/OEvgeny) ### Changed diff --git a/__tests__/__image_snapshots__/chrome-docker/adaptive-cards-js-inputs-card-with-custom-style-options-and-submit-action-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/adaptive-cards-js-inputs-card-with-custom-style-options-and-submit-action-1-snap.png index 3e513a86aa..cbf0ae473a 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/adaptive-cards-js-inputs-card-with-custom-style-options-and-submit-action-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/adaptive-cards-js-inputs-card-with-custom-style-options-and-submit-action-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-no-text-from-user-on-messageback-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-no-text-from-user-on-messageback-1-snap.png index 87fb9d1678..2dd20bcb8a 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-no-text-from-user-on-messageback-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-no-text-from-user-on-messageback-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-messageback-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-messageback-1-snap.png index 87fb9d1678..2dd20bcb8a 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-messageback-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-messageback-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-postback-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-postback-1-snap.png index 7a2d519f46..3d0016c3eb 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-postback-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-should-show-response-from-bot-and-text-from-user-on-postback-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/use-send-message-back-js-calling-send-message-back-should-send-a-message-back-activity-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/use-send-message-back-js-calling-send-message-back-should-send-a-message-back-activity-1-snap.png index 637d4ba473..6b57970302 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/use-send-message-back-js-calling-send-message-back-should-send-a-message-back-activity-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/use-send-message-back-js-calling-send-message-back-should-send-a-message-back-activity-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/use-send-post-back-js-calling-send-post-back-should-send-a-post-back-activity-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/use-send-post-back-js-calling-send-post-back-should-send-a-post-back-activity-1-snap.png index 582916132f..9ef261cefe 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/use-send-post-back-js-calling-send-post-back-should-send-a-post-back-activity-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/use-send-post-back-js-calling-send-post-back-should-send-a-post-back-activity-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/accessibility-adaptive-card-disabled-inputs-js-accessibility-requirement-disabling-adaptive-card-with-inputs-field-3-snap.png b/__tests__/__image_snapshots__/html/accessibility-adaptive-card-disabled-inputs-js-accessibility-requirement-disabling-adaptive-card-with-inputs-field-3-snap.png index db753f2c01..11fab007d5 100644 Binary files a/__tests__/__image_snapshots__/html/accessibility-adaptive-card-disabled-inputs-js-accessibility-requirement-disabling-adaptive-card-with-inputs-field-3-snap.png and b/__tests__/__image_snapshots__/html/accessibility-adaptive-card-disabled-inputs-js-accessibility-requirement-disabling-adaptive-card-with-inputs-field-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/auto-scroll-with-suggested-actions-submit-adaptive-cards-js-auto-scroll-with-suggested-actions-shown-should-stick-to-bottom-if-submitting-an-adaptive-card-1-snap.png b/__tests__/__image_snapshots__/html/auto-scroll-with-suggested-actions-submit-adaptive-cards-js-auto-scroll-with-suggested-actions-shown-should-stick-to-bottom-if-submitting-an-adaptive-card-1-snap.png index 3572966578..99ad40e929 100644 Binary files a/__tests__/__image_snapshots__/html/auto-scroll-with-suggested-actions-submit-adaptive-cards-js-auto-scroll-with-suggested-actions-shown-should-stick-to-bottom-if-submitting-an-adaptive-card-1-snap.png and b/__tests__/__image_snapshots__/html/auto-scroll-with-suggested-actions-submit-adaptive-cards-js-auto-scroll-with-suggested-actions-shown-should-stick-to-bottom-if-submitting-an-adaptive-card-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/conversation-start-properties-no-locale-is-sent-js-conversation-start-properties-with-no-locale-is-sent-should-get-hello-and-welcome-message-1-snap.png b/__tests__/__image_snapshots__/html/conversation-start-properties-no-locale-is-sent-js-conversation-start-properties-with-no-locale-is-sent-should-get-hello-and-welcome-message-1-snap.png index 5d05853fda..a99fee92e3 100644 Binary files a/__tests__/__image_snapshots__/html/conversation-start-properties-no-locale-is-sent-js-conversation-start-properties-with-no-locale-is-sent-should-get-hello-and-welcome-message-1-snap.png and b/__tests__/__image_snapshots__/html/conversation-start-properties-no-locale-is-sent-js-conversation-start-properties-with-no-locale-is-sent-should-get-hello-and-welcome-message-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/conversation-start-properties-send-en-us-js-conversation-start-properties-with-locale-of-en-us-should-get-hello-and-welcome-message-1-snap.png b/__tests__/__image_snapshots__/html/conversation-start-properties-send-en-us-js-conversation-start-properties-with-locale-of-en-us-should-get-hello-and-welcome-message-1-snap.png index fb28c403d4..cc9b4a22f5 100644 Binary files a/__tests__/__image_snapshots__/html/conversation-start-properties-send-en-us-js-conversation-start-properties-with-locale-of-en-us-should-get-hello-and-welcome-message-1-snap.png and b/__tests__/__image_snapshots__/html/conversation-start-properties-send-en-us-js-conversation-start-properties-with-locale-of-en-us-should-get-hello-and-welcome-message-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/conversation-start-properties-send-invalid-type-js-conversation-start-properties-with-locale-of-invalid-type-should-get-hello-and-welcome-message-1-snap.png b/__tests__/__image_snapshots__/html/conversation-start-properties-send-invalid-type-js-conversation-start-properties-with-locale-of-invalid-type-should-get-hello-and-welcome-message-1-snap.png index 5d05853fda..a99fee92e3 100644 Binary files a/__tests__/__image_snapshots__/html/conversation-start-properties-send-invalid-type-js-conversation-start-properties-with-locale-of-invalid-type-should-get-hello-and-welcome-message-1-snap.png and b/__tests__/__image_snapshots__/html/conversation-start-properties-send-invalid-type-js-conversation-start-properties-with-locale-of-invalid-type-should-get-hello-and-welcome-message-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-existing-js-conversation-start-properties-with-non-existing-locale-should-get-hello-and-welcome-message-1-snap.png b/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-existing-js-conversation-start-properties-with-non-existing-locale-should-get-hello-and-welcome-message-1-snap.png index 177944312b..d5b137405c 100644 Binary files a/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-existing-js-conversation-start-properties-with-non-existing-locale-should-get-hello-and-welcome-message-1-snap.png and b/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-existing-js-conversation-start-properties-with-non-existing-locale-should-get-hello-and-welcome-message-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-iso-format-js-conversation-start-properties-with-non-iso-format-locale-should-get-hello-and-welcome-message-1-snap.png b/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-iso-format-js-conversation-start-properties-with-non-iso-format-locale-should-get-hello-and-welcome-message-1-snap.png index d0ebe3c9e6..af2d64e183 100644 Binary files a/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-iso-format-js-conversation-start-properties-with-non-iso-format-locale-should-get-hello-and-welcome-message-1-snap.png and b/__tests__/__image_snapshots__/html/conversation-start-properties-send-non-iso-format-js-conversation-start-properties-with-non-iso-format-locale-should-get-hello-and-welcome-message-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/conversation-start-properties-send-zh-cn-js-conversation-start-properties-with-locale-of-zh-cn-should-get-hello-and-welcome-in-simplified-chinese-1-snap.png b/__tests__/__image_snapshots__/html/conversation-start-properties-send-zh-cn-js-conversation-start-properties-with-locale-of-zh-cn-should-get-hello-and-welcome-in-simplified-chinese-1-snap.png index 34722ca9f3..9e01d9c30a 100644 Binary files a/__tests__/__image_snapshots__/html/conversation-start-properties-send-zh-cn-js-conversation-start-properties-with-locale-of-zh-cn-should-get-hello-and-welcome-in-simplified-chinese-1-snap.png and b/__tests__/__image_snapshots__/html/conversation-start-properties-send-zh-cn-js-conversation-start-properties-with-locale-of-zh-cn-should-get-hello-and-welcome-in-simplified-chinese-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-display-text-text-value-1-snap.png b/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-display-text-text-value-1-snap.png index e8e632734b..9c39265c37 100644 Binary files a/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-display-text-text-value-1-snap.png and b/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-display-text-text-value-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-value-1-snap.png b/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-value-1-snap.png index 5748c049b2..9ebb978d1b 100644 Binary files a/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-value-1-snap.png and b/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-message-back-value-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-post-back-json-1-snap.png b/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-post-back-json-1-snap.png index fef354879d..456f64914c 100644 Binary files a/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-post-back-json-1-snap.png and b/__tests__/__image_snapshots__/html/hero-card-actions-js-hero-card-actions-post-back-json-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/scroll-to-end-button-tab-order-js-scroll-to-end-button-should-have-correct-tab-order-1-snap.png b/__tests__/__image_snapshots__/html/scroll-to-end-button-tab-order-js-scroll-to-end-button-should-have-correct-tab-order-1-snap.png index 7632d7f0c3..4b0f3fafc9 100644 Binary files a/__tests__/__image_snapshots__/html/scroll-to-end-button-tab-order-js-scroll-to-end-button-should-have-correct-tab-order-1-snap.png and b/__tests__/__image_snapshots__/html/scroll-to-end-button-tab-order-js-scroll-to-end-button-should-have-correct-tab-order-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png index 0b6193e6fb..0b4481cba6 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png index 96fc77ff62..0e2dd4fd8e 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png index ea87fed004..8f57bdcfae 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png index 8f96e8baf7..57c4290e20 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-5-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-5-snap.png new file mode 100644 index 0000000000..0ece4b5bd6 Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-5-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png index 0b6193e6fb..6f8afea954 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png index 96fc77ff62..ef5f3ccb32 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png index 851f9a1e76..dc360da5a9 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png index 8f96e8baf7..cb2b12160b 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-5-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-5-snap.png new file mode 100644 index 0000000000..c971c36549 Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-dark-js-fluent-theme-applied-dark-theme-applied-side-by-side-left-transcript-right-codeblock-dark-5-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png index 4668432512..d53638ac94 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png index cf31297d70..ec207a73cf 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-2-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png index 7fb56a3e8e..f9f4a8a8e5 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png index 1cf411ee19..b6ebcc5c51 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-4-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-5-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-5-snap.png new file mode 100644 index 0000000000..0285d4d121 Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-5-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png index 4668432512..4514567bc2 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png index cf31297d70..ce4e8fa109 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-2-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png index 67626f3be8..901dfeaf64 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-3-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png index 1cf411ee19..58d5f33439 100644 Binary files a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-4-snap.png differ diff --git a/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-5-snap.png b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-5-snap.png new file mode 100644 index 0000000000..034dccb460 Binary files /dev/null and b/__tests__/__image_snapshots__/html/side-by-side-wide-js-fluent-theme-applied-side-by-side-left-transcript-right-codeblock-dark-5-snap.png differ diff --git a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-43-1-snap.png b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-43-1-snap.png index 4d564de036..10a12ef1e0 100644 Binary files a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-43-1-snap.png and b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-43-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-44-1-snap.png b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-44-1-snap.png index 11915994a5..f77156dcb5 100644 Binary files a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-44-1-snap.png and b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-44-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-45-1-snap.png b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-45-1-snap.png index fa0e93d569..1ca03e0804 100644 Binary files a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-45-1-snap.png and b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-45-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-46-1-snap.png b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-46-1-snap.png index 977f6b0e1e..435c89ec3a 100644 Binary files a/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-46-1-snap.png and b/__tests__/__image_snapshots__/html/transcript-activity-grouping-js-transcript-with-activity-grouping-test-46-1-snap.png differ diff --git a/__tests__/html/fluentTheme/side-by-side.wide.dark.html b/__tests__/html/fluentTheme/side-by-side.wide.dark.html index 2485974b95..77e8176886 100644 --- a/__tests__/html/fluentTheme/side-by-side.wide.dark.html +++ b/__tests__/html/fluentTheme/side-by-side.wide.dark.html @@ -102,7 +102,7 @@ }; const waveSvg = `data:image/svg+xml;utf8,${encodeURIComponent(` - + @@ -540,6 +540,17 @@ } } ], [ + { + timestamp: timestamp(), + from: { "role": "user" }, + id: "6.0", + text: `Help me to create a beautiful visualization of harmonic waves using Python, complete the following code: +\`\`\`python +def plot_sine_waves(): + t = np.linspace(0, 10, 1000) +\`\`\``, + type: "message" + }, { timestamp: timestamp(), from: { role: 'bot' }, @@ -594,7 +605,12 @@ }], id: "a4c0c01d-c06e-4dde-9278-265c607b545b-82", type: "message", - text: `This example demonstrates creating a beautiful visualization of harmonic waves using Python's Matplotlib library. The code generates three sine waves with different frequencies and phases, then combines them to show wave interference patterns.\nwave plot`, + text: `This example demonstrates creating a visualization of harmonic waves using Python's Matplotlib library. The code generates three sine waves with different frequencies and phases, then combines them to show wave interference patterns. +wave plot + +Use the command to install required dependencies: + + $ pip install numpy matplotlib` } ]]; @@ -704,6 +720,8 @@ await host.sendKeys('ARROW_UP'); await host.sendKeys('ENTER'); await host.snapshot(); + await host.sendKeys('TAB'); + await host.snapshot(); await host.sendKeys('ENTER'); await host.snapshot(); await host.sendKeys('ENTER'); diff --git a/__tests__/html/fluentTheme/side-by-side.wide.html b/__tests__/html/fluentTheme/side-by-side.wide.html index b1a165a264..9b907e4f81 100644 --- a/__tests__/html/fluentTheme/side-by-side.wide.html +++ b/__tests__/html/fluentTheme/side-by-side.wide.html @@ -112,7 +112,7 @@ }; const waveSvg = `data:image/svg+xml;utf8,${encodeURIComponent(` - + @@ -550,6 +550,17 @@ } } ], [ + { + timestamp: timestamp(), + from: { "role": "user" }, + id: "6.0", + text: `Help me to create a beautiful visualization of harmonic waves using Python, complete the following code: +\`\`\`python +def plot_sine_waves(): + t = np.linspace(0, 10, 1000) +\`\`\``, + type: "message" + }, { timestamp: timestamp(), from: { role: 'bot' }, @@ -604,7 +615,12 @@ }], id: "a4c0c01d-c06e-4dde-9278-265c607b545b-82", type: "message", - text: `This example demonstrates creating a beautiful visualization of harmonic waves using Python's Matplotlib library. The code generates three sine waves with different frequencies and phases, then combines them to show wave interference patterns.\nwave plot`, + text: `This example demonstrates creating a visualization of harmonic waves using Python's Matplotlib library. The code generates three sine waves with different frequencies and phases, then combines them to show wave interference patterns. +wave plot + +Use the command to install required dependencies: + + $ pip install numpy matplotlib` } ]]; @@ -687,6 +703,8 @@ await host.sendKeys('ARROW_UP'); await host.sendKeys('ENTER'); await host.snapshot(); + await host.sendKeys('TAB'); + await host.snapshot(); await host.sendKeys('ENTER'); await host.snapshot(); await host.sendKeys('ENTER'); diff --git a/__tests__/html/toast.customizeDebounceTimeout.js b/__tests__/html/toast.customizeDebounceTimeout.js deleted file mode 100644 index 72f72755b4..0000000000 --- a/__tests__/html/toast.customizeDebounceTimeout.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('notification toast', () => { - test('customize toast debounce time on update and dismiss', () => runHTML('toast.customizeDebounceTimeout.html')); -}); diff --git a/__tests__/html2/activity/viewCodeButton.html.snap-2.png b/__tests__/html2/activity/viewCodeButton.html.snap-2.png index 894fc34b17..5b30a9429f 100644 Binary files a/__tests__/html2/activity/viewCodeButton.html.snap-2.png and b/__tests__/html2/activity/viewCodeButton.html.snap-2.png differ diff --git a/__tests__/html2/markdown/codeBlock/layout.code-block-dark.html b/__tests__/html2/markdown/codeBlock/layout.code-block-dark.html new file mode 100644 index 0000000000..c8a9cbe861 --- /dev/null +++ b/__tests__/html2/markdown/codeBlock/layout.code-block-dark.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/__tests__/html2/markdown/codeBlock/layout.code-block-dark.html.snap-1.png b/__tests__/html2/markdown/codeBlock/layout.code-block-dark.html.snap-1.png new file mode 100644 index 0000000000..ce8a8a5adb Binary files /dev/null and b/__tests__/html2/markdown/codeBlock/layout.code-block-dark.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlock/layout.html b/__tests__/html2/markdown/codeBlock/layout.html new file mode 100644 index 0000000000..56ff32f18f --- /dev/null +++ b/__tests__/html2/markdown/codeBlock/layout.html @@ -0,0 +1,68 @@ + + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/markdown/codeBlock/layout.html.snap-1.png b/__tests__/html2/markdown/codeBlock/layout.html.snap-1.png new file mode 100644 index 0000000000..0ded90396d Binary files /dev/null and b/__tests__/html2/markdown/codeBlock/layout.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-1.png b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-1.png index aa67233d96..2e96fa533f 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-1.png and b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-2.png b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-2.png index ef7a89d084..cce0c5693d 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-2.png and b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-2.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-3.png b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-3.png index 272dcf8d14..59c8545cd2 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-3.png and b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/behavior.html.snap-3.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/layout.html.snap-1.png b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/layout.html.snap-1.png index 04e5bbe7b4..12df84876f 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/layout.html.snap-1.png and b/__tests__/html2/markdown/codeBlockCopyButton/adaptiveCards/layout.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-1.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-1.png index 427ab18314..29420e464c 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-1.png and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-2.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-2.png index d1b38017dc..6959bcde3e 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-2.png and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-2.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-3.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-3.png index dcff19f613..a2cf6cf71d 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-3.png and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.denied.html.snap-3.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html new file mode 100644 index 0000000000..4e29519a3f --- /dev/null +++ b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html @@ -0,0 +1,115 @@ + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-1.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-1.png new file mode 100644 index 0000000000..a7731f5da0 Binary files /dev/null and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-2.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-2.png new file mode 100644 index 0000000000..74202247dd Binary files /dev/null and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-2.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-3.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-3.png new file mode 100644 index 0000000000..cd9877433e Binary files /dev/null and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.highlighted.html.snap-3.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-1.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-1.png index 427ab18314..29420e464c 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-1.png and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-2.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-2.png index 6bf6af2cb6..92c33b6372 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-2.png and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-2.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-3.png b/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-3.png index 9a3b422df6..ed29f30633 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-3.png and b/__tests__/html2/markdown/codeBlockCopyButton/behavior.html.snap-3.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/fluent/layout.html.snap-1.png b/__tests__/html2/markdown/codeBlockCopyButton/fluent/layout.html.snap-1.png index e4f3592079..17cdddaeb1 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/fluent/layout.html.snap-1.png and b/__tests__/html2/markdown/codeBlockCopyButton/fluent/layout.html.snap-1.png differ diff --git a/__tests__/html2/markdown/codeBlockCopyButton/layout.html.snap-1.png b/__tests__/html2/markdown/codeBlockCopyButton/layout.html.snap-1.png index 6448d9eb02..728ca9a662 100644 Binary files a/__tests__/html2/markdown/codeBlockCopyButton/layout.html.snap-1.png and b/__tests__/html2/markdown/codeBlockCopyButton/layout.html.snap-1.png differ diff --git a/__tests__/html2/markdown/math/layout.spec.html.snap-1.png b/__tests__/html2/markdown/math/layout.spec.html.snap-1.png index 80948d0800..ff36d595ce 100644 Binary files a/__tests__/html2/markdown/math/layout.spec.html.snap-1.png and b/__tests__/html2/markdown/math/layout.spec.html.snap-1.png differ diff --git a/__tests__/html2/markdown/math/layout.spec.html.snap-2.png b/__tests__/html2/markdown/math/layout.spec.html.snap-2.png index 0e330f8bfa..e98f4a4295 100644 Binary files a/__tests__/html2/markdown/math/layout.spec.html.snap-2.png and b/__tests__/html2/markdown/math/layout.spec.html.snap-2.png differ diff --git a/__tests__/html2/markdown/math/layout.spec.html.snap-3.png b/__tests__/html2/markdown/math/layout.spec.html.snap-3.png index a85ab1db0c..7a4cb0f259 100644 Binary files a/__tests__/html2/markdown/math/layout.spec.html.snap-3.png and b/__tests__/html2/markdown/math/layout.spec.html.snap-3.png differ diff --git a/__tests__/html/toast.customizeDebounceTimeout.html b/__tests__/html2/toast/customDebounceTimeout.html similarity index 68% rename from __tests__/html/toast.customizeDebounceTimeout.html rename to __tests__/html2/toast/customDebounceTimeout.html index 15eb947127..80972608d6 100644 --- a/__tests__/html/toast.customizeDebounceTimeout.html +++ b/__tests__/html2/toast/customDebounceTimeout.html @@ -10,16 +10,12 @@
diff --git a/__tests__/html2/toast/customDebounceTimeout.html.snap-1.png b/__tests__/html2/toast/customDebounceTimeout.html.snap-1.png new file mode 100644 index 0000000000..c9725922b6 Binary files /dev/null and b/__tests__/html2/toast/customDebounceTimeout.html.snap-1.png differ diff --git a/__tests__/html2/toast/customDebounceTimeout.html.snap-2.png b/__tests__/html2/toast/customDebounceTimeout.html.snap-2.png new file mode 100644 index 0000000000..b664173c7d Binary files /dev/null and b/__tests__/html2/toast/customDebounceTimeout.html.snap-2.png differ diff --git a/__tests__/html2/toast/customDebounceTimeout.html.snap-3.png b/__tests__/html2/toast/customDebounceTimeout.html.snap-3.png new file mode 100644 index 0000000000..ef517bd036 Binary files /dev/null and b/__tests__/html2/toast/customDebounceTimeout.html.snap-3.png differ diff --git a/package-lock.json b/package-lock.json index 5aaa1b8a91..eb368c1c5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4379,44 +4379,44 @@ "license": "MIT" }, "node_modules/@shikijs/core": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.1.tgz", - "integrity": "sha512-bqAhT/Ri5ixV4oYsvJNH8UJjpjbINWlWyXY6tBTsP4OmD6XnFv43nRJ+lTdxd2rmG5pgam/x+zGR6kLRXrpEKA==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.2.tgz", + "integrity": "sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==", "license": "MIT", "dependencies": { - "@shikijs/engine-javascript": "1.22.1", - "@shikijs/engine-oniguruma": "1.22.1", - "@shikijs/types": "1.22.1", + "@shikijs/engine-javascript": "1.22.2", + "@shikijs/engine-oniguruma": "1.22.2", + "@shikijs/types": "1.22.2", "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.3" } }, "node_modules/@shikijs/engine-javascript": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.1.tgz", - "integrity": "sha512-540pyoy0LWe4jj2BVbgELwOFu1uFvRI7lg4hdsExrSXA9x7gqfzZ/Nnh4RfX86aDAgJ647gx4TCmRwACbnQSvw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.2.tgz", + "integrity": "sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==", "license": "MIT", "dependencies": { - "@shikijs/types": "1.22.1", + "@shikijs/types": "1.22.2", "@shikijs/vscode-textmate": "^9.3.0", "oniguruma-to-js": "0.4.3" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.1.tgz", - "integrity": "sha512-L+1Vmd+a2kk8HtogUFymQS6BjUfJnzcWoUp1BUgxoDiklbKSMvrsMuLZGevTOP1m0rEjgnC5MsDmsr8lX1lC+Q==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.2.tgz", + "integrity": "sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==", "license": "MIT", "dependencies": { - "@shikijs/types": "1.22.1", + "@shikijs/types": "1.22.2", "@shikijs/vscode-textmate": "^9.3.0" } }, "node_modules/@shikijs/types": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.1.tgz", - "integrity": "sha512-+45f8mu/Hxqs6Kyhfm98Nld5n7Q7lwhjU8UtdQwrOPs7BnM4VAb929O3IQ2ce+4D7SlNFlZGd8CnKRSnwbQreQ==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.2.tgz", + "integrity": "sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^9.3.0", @@ -19301,9 +19301,9 @@ } }, "node_modules/regex": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", - "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.4.0.tgz", + "integrity": "sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==", "license": "MIT" }, "node_modules/regexp-tree": { @@ -20399,15 +20399,15 @@ "license": "MIT" }, "node_modules/shiki": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.1.tgz", - "integrity": "sha512-PbJ6XxrWLMwB2rm3qdjIHNm3zq4SfFnOx0B3rEoi4AN8AUngsdyZ1tRe5slMPtn6jQkbUURLNZPpLR7Do3k78g==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.2.tgz", + "integrity": "sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==", "license": "MIT", "dependencies": { - "@shikijs/core": "1.22.1", - "@shikijs/engine-javascript": "1.22.1", - "@shikijs/engine-oniguruma": "1.22.1", - "@shikijs/types": "1.22.1", + "@shikijs/core": "1.22.2", + "@shikijs/engine-javascript": "1.22.2", + "@shikijs/engine-oniguruma": "1.22.2", + "@shikijs/types": "1.22.2", "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } @@ -24294,6 +24294,7 @@ "prop-types": "15.8.1", "punycode": "2.3.1", "sanitize-html": "2.13.0", + "shiki": "1.22.2", "swiper": "8.4.7", "url-search-params-polyfill": "8.2.5", "uuid": "8.3.2", @@ -24474,7 +24475,6 @@ "react-say": "2.1.0", "react-scroll-to-bottom": "4.2.1-main.53844f5", "redux": "5.0.1", - "shiki": "^1.22.1", "simple-update-in": "2.2.0", "use-propagate": "^0.2.0-main.fb24772", "use-ref-from": "0.1.0", diff --git a/packages/bundle/package.json b/packages/bundle/package.json index db147cec9a..ddc676692d 100644 --- a/packages/bundle/package.json +++ b/packages/bundle/package.json @@ -138,6 +138,7 @@ "prop-types": "15.8.1", "punycode": "2.3.1", "sanitize-html": "2.13.0", + "shiki": "1.22.2", "swiper": "8.4.7", "url-search-params-polyfill": "8.2.5", "uuid": "8.3.2", diff --git a/packages/bundle/src/AddFullBundle.tsx b/packages/bundle/src/AddFullBundle.tsx index f2212fddd4..c273bc0426 100644 --- a/packages/bundle/src/AddFullBundle.tsx +++ b/packages/bundle/src/AddFullBundle.tsx @@ -5,10 +5,11 @@ import { } from 'botframework-webchat-api'; import { type HTMLContentTransformMiddleware } from 'botframework-webchat-component'; import { singleToArray, warnOnce, type OneOrMany } from 'botframework-webchat-core'; -import React, { type ReactNode } from 'react'; +import React, { memo, type ReactNode } from 'react'; import AdaptiveCardsComposer from './adaptiveCards/AdaptiveCardsComposer'; import { type AdaptiveCardsStyleOptions } from './adaptiveCards/AdaptiveCardsStyleOptions'; +import ShikiComposer from './codeHighlighter/ShikiComposer'; import { type AdaptiveCardsPackage } from './types/AdaptiveCardsPackage'; import { type StrictFullBundleStyleOptions } from './types/FullBundleStyleOptions'; import useComposerProps from './useComposerProps'; @@ -36,7 +37,7 @@ const adaptiveCardHostConfigDeprecation = warnOnce( '"adaptiveCardHostConfig" is deprecated. Please use "adaptiveCardsHostConfig" instead. "adaptiveCardHostConfig" will be removed on or after 2022-01-01.' ); -const AddFullBundle = ({ +function AddFullBundle({ adaptiveCardHostConfig, adaptiveCardsHostConfig, adaptiveCardsPackage, @@ -47,7 +48,7 @@ const AddFullBundle = ({ renderMarkdown, styleOptions, styleSet -}: AddFullBundleProps) => { +}: AddFullBundleProps) { adaptiveCardHostConfig && adaptiveCardHostConfigDeprecation(); const patchedProps = useComposerProps({ @@ -60,15 +61,17 @@ const AddFullBundle = ({ }); return ( - - {children(patchedProps)} - + + + {children(patchedProps)} + + ); -}; +} -export default AddFullBundle; +export default memo(AddFullBundle); export type { AddFullBundleProps }; diff --git a/packages/bundle/src/bundle/index.ts b/packages/bundle/src/bundle/index.ts index 531e2e35f0..4cd64fda44 100644 --- a/packages/bundle/src/bundle/index.ts +++ b/packages/bundle/src/bundle/index.ts @@ -1,3 +1,5 @@ +import './modern-polyfill'; + import addVersion from './addVersion'; import * as full from '../module/exports'; diff --git a/packages/bundle/src/bundle/modern-polyfill.ts b/packages/bundle/src/bundle/modern-polyfill.ts new file mode 100644 index 0000000000..6eaa475bff --- /dev/null +++ b/packages/bundle/src/bundle/modern-polyfill.ts @@ -0,0 +1,3 @@ +// Set APIs such as .union and .difference are only available +// starting from Chrome 122 while we need to support Chrome 110. +import 'core-js/features/set/index.js'; diff --git a/packages/bundle/src/codeHighlighter/ShikiComposer.tsx b/packages/bundle/src/codeHighlighter/ShikiComposer.tsx new file mode 100644 index 0000000000..4ddad77b7f --- /dev/null +++ b/packages/bundle/src/codeHighlighter/ShikiComposer.tsx @@ -0,0 +1,35 @@ +import { CodeHighlighterComposer, type HighlightCodeFn } from 'botframework-webchat-component/internal'; +import React, { memo, ReactNode, useEffect, useState } from 'react'; +import { type HighlighterCore } from 'shiki'; + +import createHighlighter from './shiki'; + +const createHighligtCodeWithShiki = + (shikiHiglighter: HighlighterCore): HighlightCodeFn => + (code, language, options) => + shikiHiglighter.codeToHtml(code, { + lang: language, + theme: options.theme + }); + +function ShikiComposer({ children }: { readonly children: ReactNode | undefined }) { + const [highlightProps, setHighlightProps] = useState>(); + + useEffect(() => { + let isMounted = true; + (async () => + isMounted && + setHighlightProps( + Object.freeze({ + highlightCode: createHighligtCodeWithShiki(await createHighlighter()) + }) + ))(); + return () => { + isMounted = false; + }; + }, []); + + return {children}; +} + +export default memo(ShikiComposer); diff --git a/packages/component/src/Attachment/Text/private/shiki.ts b/packages/bundle/src/codeHighlighter/shiki.ts similarity index 91% rename from packages/component/src/Attachment/Text/private/shiki.ts rename to packages/bundle/src/codeHighlighter/shiki.ts index 51da75ac95..b4cd2c0f0a 100644 --- a/packages/component/src/Attachment/Text/private/shiki.ts +++ b/packages/bundle/src/codeHighlighter/shiki.ts @@ -15,7 +15,7 @@ function addjustTheme(theme: ThemeRegistrationRaw): ThemeRegistrationRaw { ...theme, colors: { ...theme.colors, - 'editor.background': `var(--webchat__code-block--background, ${theme.colors['editor.background']})` + 'editor.background': `transparent` } }; } diff --git a/packages/bundle/src/markdown/createHTMLContentTransformMiddleware.ts b/packages/bundle/src/markdown/createHTMLContentTransformMiddleware.ts index e4691dbcc8..0af941051a 100644 --- a/packages/bundle/src/markdown/createHTMLContentTransformMiddleware.ts +++ b/packages/bundle/src/markdown/createHTMLContentTransformMiddleware.ts @@ -1,8 +1,8 @@ import { type HTMLContentTransformMiddleware } from 'botframework-webchat-component'; -import createCodeBlockCopyButtonMiddleware from './middleware/createCodeBlockCopyButtonMiddleware'; +import createCodeBlockMiddleware from './middleware/createCodeBlockMiddleware'; import createSanitizeMiddleware from './middleware/createSanitizeMiddleware'; export default function createHTMLContentTransformMiddleware(): readonly HTMLContentTransformMiddleware[] { - return Object.freeze([createCodeBlockCopyButtonMiddleware(), createSanitizeMiddleware()]); + return Object.freeze([createCodeBlockMiddleware(), createSanitizeMiddleware()]); } diff --git a/packages/bundle/src/markdown/middleware/createCodeBlockCopyButtonMiddleware.ts b/packages/bundle/src/markdown/middleware/createCodeBlockCopyButtonMiddleware.ts deleted file mode 100644 index 41888f50ed..0000000000 --- a/packages/bundle/src/markdown/middleware/createCodeBlockCopyButtonMiddleware.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { type HTMLContentTransformMiddleware } from 'botframework-webchat-component'; - -import codeBlockCopyButtonDocumentMod from '../private/codeBlockCopyButtonDocumentMod'; - -export default function createCodeBlockCopyButtonMiddleware(): HTMLContentTransformMiddleware { - return () => next => request => - next( - Object.freeze({ - ...request, - allowedTags: Object.freeze( - new Map(request.allowedTags).set( - request.codeBlockCopyButtonTagName, - Object.freeze({ - attributes: Object.freeze( - new Set(['class', 'data-alt-copy', 'data-alt-copied', 'data-testid', 'data-value']) - ) - }) - ) - ), - documentFragment: codeBlockCopyButtonDocumentMod(request.documentFragment, { - codeBlockCopyButtonAltCopied: request.codeBlockCopyButtonAltCopied, - codeBlockCopyButtonAltCopy: request.codeBlockCopyButtonAltCopy, - codeBlockCopyButtonClassName: request.codeBlockCopyButtonClassName, - codeBlockCopyButtonTagName: request.codeBlockCopyButtonTagName - }) - }) - ); -} diff --git a/packages/bundle/src/markdown/middleware/createCodeBlockMiddleware.ts b/packages/bundle/src/markdown/middleware/createCodeBlockMiddleware.ts new file mode 100644 index 0000000000..8f23b51863 --- /dev/null +++ b/packages/bundle/src/markdown/middleware/createCodeBlockMiddleware.ts @@ -0,0 +1,21 @@ +import { type HTMLContentTransformMiddleware } from 'botframework-webchat-component'; + +import codeBlockDocumentMod from '../private/codeBlockDocumentMod'; + +export default function createCodeBlockMiddleware(): HTMLContentTransformMiddleware { + return () => next => request => + next( + Object.freeze({ + ...request, + allowedTags: Object.freeze( + new Map(request.allowedTags).set( + request.codeBlockTagName, + Object.freeze({ + attributes: Object.freeze(new Set(['class', 'theme', 'language'])) + }) + ) + ), + documentFragment: codeBlockDocumentMod(request.documentFragment, request.codeBlockTagName) + }) + ); +} diff --git a/packages/bundle/src/markdown/private/codeBlockCopyButtonDocumentMod.ts b/packages/bundle/src/markdown/private/codeBlockCopyButtonDocumentMod.ts deleted file mode 100644 index c8d34bc292..0000000000 --- a/packages/bundle/src/markdown/private/codeBlockCopyButtonDocumentMod.ts +++ /dev/null @@ -1,34 +0,0 @@ -export default function codeBlockCopyButtonDocumentMod( - documentFragment: T, - { - codeBlockCopyButtonAltCopied, - codeBlockCopyButtonAltCopy, - codeBlockCopyButtonClassName, - codeBlockCopyButtonTagName - }: Readonly<{ - codeBlockCopyButtonAltCopied: string; - codeBlockCopyButtonAltCopy: string; - codeBlockCopyButtonClassName: string; - codeBlockCopyButtonTagName: string; - }> -): T { - for (const preElement of [...documentFragment.querySelectorAll('pre')]) { - if (preElement.children.length === 1) { - const [firstChild] = preElement.children; - - if (firstChild.matches('code')) { - const codeBlockCopyButtonElement = documentFragment.ownerDocument.createElement(codeBlockCopyButtonTagName); - - codeBlockCopyButtonElement.className = codeBlockCopyButtonClassName; - codeBlockCopyButtonElement.dataset.altCopied = codeBlockCopyButtonAltCopied; - codeBlockCopyButtonElement.dataset.altCopy = codeBlockCopyButtonAltCopy; - codeBlockCopyButtonElement.dataset.value = preElement.textContent; - - preElement.classList.add('webchat__render-markdown__code-block'); - preElement.prepend(codeBlockCopyButtonElement); - } - } - } - - return documentFragment; -} diff --git a/packages/bundle/src/markdown/private/codeBlockDocumentMod.ts b/packages/bundle/src/markdown/private/codeBlockDocumentMod.ts new file mode 100644 index 0000000000..c204b6bb7e --- /dev/null +++ b/packages/bundle/src/markdown/private/codeBlockDocumentMod.ts @@ -0,0 +1,24 @@ +const languageClassPrefix = 'language-'; + +export default function codeBlockDocumentMod( + documentFragment: T, + codeBlockTagName: string +): T { + for (const preElement of [...documentFragment.querySelectorAll('pre:has(code:only-child)')]) { + const [codeElement] = preElement.children; + + const language = [...codeElement.classList.values()] + .find(cls => cls.startsWith(languageClassPrefix)) + ?.substring(languageClassPrefix.length); + + const codeBlockRoot = document.createElement(codeBlockTagName); + + language && codeBlockRoot.setAttribute('language', language); + + codeBlockRoot.append(codeElement); + + preElement.replaceWith(codeBlockRoot); + } + + return documentFragment; +} diff --git a/packages/bundle/src/markdown/renderMarkdown.ts b/packages/bundle/src/markdown/renderMarkdown.ts index ade29c2c16..3bb9a182ba 100644 --- a/packages/bundle/src/markdown/renderMarkdown.ts +++ b/packages/bundle/src/markdown/renderMarkdown.ts @@ -1,6 +1,7 @@ import { parseDocumentFragmentFromString, - serializeDocumentFragmentIntoString + serializeDocumentFragmentIntoString, + type HighlightCodeFn } from 'botframework-webchat-component/internal'; import { onErrorResumeNext } from 'botframework-webchat-core'; import katex from 'katex'; @@ -15,6 +16,7 @@ import { pre as respectCRLFPre } from './private/respectCRLF'; type RenderInit = Readonly<{ codeBlockCopyButtonTagName: string; externalLinkAlt: string; + highlightCode: HighlightCodeFn; }>; const ALLOWED_SCHEMES = ['data', 'http', 'https', 'ftp', 'mailto', 'sip', 'tel']; diff --git a/packages/bundle/tsup.config.ts b/packages/bundle/tsup.config.ts index d5b500ef48..322933aef5 100644 --- a/packages/bundle/tsup.config.ts +++ b/packages/bundle/tsup.config.ts @@ -43,7 +43,10 @@ const config: typeof baseConfig = { '@babel/runtime', 'memoize-one', 'microsoft-cognitiveservices-speech-sdk', - 'web-speech-cognitive-services' + 'web-speech-cognitive-services', + // Belows are the dependency chain related to "regex" where it is named export-only and does not work on Webpack 4/PPUX (CJS cannot import named export). + // Webpack 4: "Can't import the named export 'rewrite' from non EcmaScript module (only default export is available)" + 'shiki' // shiki -> @shikijs/core -> @shikijs/engine-javascript -> regex ] }; diff --git a/packages/component/package.json b/packages/component/package.json index 6f7acc9e05..1edef023d6 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -157,7 +157,6 @@ "react-say": "2.1.0", "react-scroll-to-bottom": "4.2.1-main.53844f5", "redux": "5.0.1", - "shiki": "^1.22.1", "simple-update-in": "2.2.0", "use-propagate": "^0.2.0-main.fb24772", "use-ref-from": "0.1.0", diff --git a/packages/component/src/Attachment/Text/private/CodeContent.tsx b/packages/component/src/Attachment/Text/private/CodeContent.tsx index 04c961e086..3a720909c9 100644 --- a/packages/component/src/Attachment/Text/private/CodeContent.tsx +++ b/packages/component/src/Attachment/Text/private/CodeContent.tsx @@ -1,82 +1,26 @@ -import { hooks } from 'botframework-webchat-api'; import classNames from 'classnames'; -import React, { Fragment, memo, ReactNode, useEffect, useState } from 'react'; -import { useStyleSet } from '../../../hooks'; -import CodeBlockCopyButton from '../../../providers/CustomElements/customElements/CodeBlockCopyButton'; -import createHighlighter from './shiki'; +import React, { Fragment, memo, ReactNode } from 'react'; + +import useCodeBlockTag from '../../../providers/CustomElements/useCodeBlockTagName'; type Props = Readonly<{ children?: ReactNode | undefined; className?: string | undefined; code: string; - language?: string | undefined; + language: string; title: string; }>; -const { useLocalizer, useStyleOptions } = hooks; - -const highlighterPromise = createHighlighter(); - const CodeContent = memo(({ children, className, code, language, title }: Props) => { - const [{ codeBlockTheme }] = useStyleOptions(); - const [highlightedCode, setHighlightedCode] = useState(''); - const localize = useLocalizer(); - - const copiedAlt = localize('COPY_BUTTON_COPIED_TEXT'); - const copyAlt = localize('COPY_BUTTON_TEXT'); - const [{ codeBlockCopyButton: codeBlockCopyButtonClassName }] = useStyleSet(); - - useEffect(() => { - let mounted = true; - (async function highlight() { - const highlighter = await highlighterPromise; - if (!mounted) { - return; - } - try { - const html = highlighter.codeToHtml(code, { - lang: language, - theme: codeBlockTheme - }); - - setHighlightedCode(html); - } catch (error) { - console.error('botframework-webchat: Failed to highlight code:', error); - - const pre = document.createElement('pre'); - pre.textContent = code; - - setHighlightedCode(pre.outerHTML); - } - })(); - - return () => { - mounted = false; - }; - }, [code, codeBlockTheme, language]); - - if (!highlightedCode) { - return null; - } - + const [, CodeBlock] = useCodeBlockTag(); return (

{title}

-
- -
-
+ + {code} + {children} ); diff --git a/packages/component/src/Styles/CustomPropertyNames.ts b/packages/component/src/Styles/CustomPropertyNames.ts index 897d69e8f2..22ba7e9323 100644 --- a/packages/component/src/Styles/CustomPropertyNames.ts +++ b/packages/component/src/Styles/CustomPropertyNames.ts @@ -1,9 +1,11 @@ const CustomPropertyNames = Object.freeze({ // Make sure key names does not have JavaScript forbidden names. + BackgroundCodeBlock: '--webchat__background--code-block', BorderAnimationColor1: '--webchat__border-animation--color-1', BorderAnimationColor2: '--webchat__border-animation--color-2', BorderAnimationColor3: '--webchat__border-animation--color-3', ColorAccent: '--webchat__color--accent', + ColorCodeBlock: '--webchat__color--code-block', ColorSubtle: '--webchat__color--subtle', ColorTimestamp: '--webchat__color--timestamp', FontPrimary: '--webchat__font--primary', @@ -11,16 +13,13 @@ const CustomPropertyNames = Object.freeze({ IconURLExternalLink: '--webchat__icon-url--external-link', MaxHeightImageBubble: '--webchat__max-height--image-bubble', MaxWidthAttachmentBubble: '--webchat__max-width--attachment-bubble', - MinWidthAttachmentBubble: '--webchat__min-width--attachment-bubble', MaxWidthMessageBubble: '--webchat__max-width--message-bubble', - MinWidthMessageBubble: '--webchat__min-width--message-bubble', MinHeightBubble: '--webchat__min-height--bubble', MinHeightImageBubble: '--webchat__min-height--image-bubble', + MinWidthAttachmentBubble: '--webchat__min-width--attachment-bubble', + MinWidthMessageBubble: '--webchat__min-width--message-bubble', PaddingRegular: '--webchat__padding--regular', SizeAvatar: '--webchat__size--avatar' -}); - -// This is for type-checking only to make sure the CSS custom property names is `--webchat__${string}`. -const _TypeChecking: Readonly> = CustomPropertyNames; +}) satisfies Readonly>; export default CustomPropertyNames; diff --git a/packages/component/src/Styles/StyleSet/CodeBlock.ts b/packages/component/src/Styles/StyleSet/CodeBlock.ts new file mode 100644 index 0000000000..d3289a64ca --- /dev/null +++ b/packages/component/src/Styles/StyleSet/CodeBlock.ts @@ -0,0 +1,39 @@ +import CustomPropertyNames from '../CustomPropertyNames'; + +export default function createCodeBlockStyle() { + return { + '&.webchat__code-block': { + background: `var(${CustomPropertyNames.BackgroundCodeBlock}, inherit)`, + border: '1px solid #d1d1d1', + borderRadius: '4px', + color: `var(${CustomPropertyNames.ColorCodeBlock}, currentColor)`, + display: 'block', + margin: '16px 0', + overflow: 'hidden', + padding: '4px 4px 4px 8px', + + ':has(> .webchat__code-block__theme--github-dark-default)': { + background: `var(${CustomPropertyNames.BackgroundCodeBlock}, #0d1117)`, + color: `var(${CustomPropertyNames.ColorCodeBlock}, #e6edf3)` + }, + + ':has(> .webchat__code-block__theme--github-light-default)': { + background: `var(${CustomPropertyNames.BackgroundCodeBlock}, #ffffff)`, + color: `var(${CustomPropertyNames.ColorCodeBlock}, #1f2328)` + }, + + ':has(.webchat__code-block__body:focus-visible):focus-within': { + outline: '2px solid #000', + outlineOffset: '-2px' + }, + + '.webchat__code-block__body': { + display: 'contents', + lineHeight: 'normal', + margin: '0', + outline: 'none', + whiteSpace: 'pre-wrap' + } + } + }; +} diff --git a/packages/component/src/Styles/StyleSet/RenderMarkdown.ts b/packages/component/src/Styles/StyleSet/RenderMarkdown.ts index 9268bf6663..3ba43386ce 100644 --- a/packages/component/src/Styles/StyleSet/RenderMarkdown.ts +++ b/packages/component/src/Styles/StyleSet/RenderMarkdown.ts @@ -91,10 +91,6 @@ export default function createMarkdownStyle() { '& :is([data-math-type=block], [data-math-type=inline]) > span': { display: 'contents' - }, - - '& .webchat__render-markdown__code-block': { - whiteSpace: 'pre-wrap' } } }; diff --git a/packages/component/src/Styles/StyleSet/ViewCodeDialog.ts b/packages/component/src/Styles/StyleSet/ViewCodeDialog.ts index 2e3c808731..d3791dfb9c 100644 --- a/packages/component/src/Styles/StyleSet/ViewCodeDialog.ts +++ b/packages/component/src/Styles/StyleSet/ViewCodeDialog.ts @@ -27,8 +27,8 @@ export default function createViewCodeDialogStyle() { '& .webchat__modal-dialog__body': { display: 'flex', flexDirection: 'column', - overflow: 'hidden', - gap: CSSTokens.PaddingRegular + gap: CSSTokens.PaddingRegular, + overflow: 'hidden' }, '& .webchat__view-code-dialog__header': { @@ -43,14 +43,16 @@ export default function createViewCodeDialogStyle() { }, '& .webchat__view-code-dialog__body': { - display: 'flex', - overflow: 'hidden', - position: 'relative' - }, + border: 'none', + borderRadius: 0, + overflow: 'auto', + height: '100%', - '& .webchat__view-code-dialog__code-body': { - lineHeight: '20px', - overflow: 'auto' + '.webchat__code-block__body': { + display: 'block', + margin: 0, + padding: '16px 0' + } }, '& .webchat__view-code-dialog__footer': { diff --git a/packages/component/src/Styles/createStyleSet.ts b/packages/component/src/Styles/createStyleSet.ts index a7ef9d91de..ecf70b65e1 100644 --- a/packages/component/src/Styles/createStyleSet.ts +++ b/packages/component/src/Styles/createStyleSet.ts @@ -14,6 +14,7 @@ import createCarouselFilmStripAttachment from './StyleSet/CarouselFilmStripAttac import createCarouselFlipper from './StyleSet/CarouselFlipper'; import createCitationModalDialogStyle from './StyleSet/CitationModalDialog'; import createCodeBlockCopyButtonStyle from './StyleSet/CodeBlockCopyButton'; +import createCodeBlockStyle from './StyleSet/CodeBlock'; import createConnectivityNotification from './StyleSet/ConnectivityNotification'; import createDictationInterimsStyle from './StyleSet/DictationInterims'; import createErrorBoxStyle from './StyleSet/ErrorBox'; @@ -28,19 +29,19 @@ import createMonochromeImageMaskerStyleSet from './StyleSet/MonochromeImageMaske import createRenderMarkdownStyle from './StyleSet/RenderMarkdown'; import createRootStyle from './StyleSet/Root'; import createScrollToEndButtonStyle from './StyleSet/ScrollToEndButton'; -import createSendBoxStyle from './StyleSet/SendBox'; import createSendBoxButtonStyle from './StyleSet/SendBoxButton'; +import createSendBoxStyle from './StyleSet/SendBox'; import createSendBoxTextBoxStyle from './StyleSet/SendBoxTextBox'; import createSendStatusStyle from './StyleSet/SendStatus'; import createSlottedActivityStatusStyle from './StyleSet/SlottedActivityStatus'; import createSpinnerAnimationStyle from './StyleSet/SpinnerAnimation'; import createStackedLayoutStyle from './StyleSet/StackedLayout'; -import createSuggestedActionStyle from './StyleSet/SuggestedAction'; import createSuggestedActionsStyle from './StyleSet/SuggestedActions'; +import createSuggestedActionStyle from './StyleSet/SuggestedAction'; import createTextContentStyle from './StyleSet/TextContent'; import createThumbButtonStyle from './StyleSet/ThumbButton'; -import createToastStyle from './StyleSet/Toast'; import createToasterStyle from './StyleSet/Toaster'; +import createToastStyle from './StyleSet/Toast'; import createTypingAnimationStyle from './StyleSet/TypingAnimation'; import createTypingIndicatorStyle from './StyleSet/TypingIndicator'; import createUploadButtonStyle from './StyleSet/UploadButton'; @@ -71,6 +72,7 @@ export default function createStyleSet(styleOptions: StyleOptions) { carouselFilmStrip: createCarouselFilmStrip(strictStyleOptions), carouselFilmStripAttachment: createCarouselFilmStripAttachment(strictStyleOptions), carouselFlipper: createCarouselFlipper(strictStyleOptions), + codeBlock: createCodeBlockStyle(), codeBlockCopyButton: createCodeBlockCopyButtonStyle(), connectivityNotification: createConnectivityNotification(strictStyleOptions), dictationInterims: createDictationInterimsStyle(strictStyleOptions), diff --git a/packages/component/src/hooks/internal/codeHighlighter/index.ts b/packages/component/src/hooks/internal/codeHighlighter/index.ts new file mode 100644 index 0000000000..034b10f051 --- /dev/null +++ b/packages/component/src/hooks/internal/codeHighlighter/index.ts @@ -0,0 +1,3 @@ +export { CodeHighlighterComposer, useCodeHighlighter, type HighlightCodeFn } from './private/CodeHighlighterComposer'; + +export { defaultHighlightCode } from './private/defaultHighlightCode'; diff --git a/packages/component/src/hooks/internal/codeHighlighter/private/CodeHighlighterComposer.tsx b/packages/component/src/hooks/internal/codeHighlighter/private/CodeHighlighterComposer.tsx new file mode 100644 index 0000000000..3393890021 --- /dev/null +++ b/packages/component/src/hooks/internal/codeHighlighter/private/CodeHighlighterComposer.tsx @@ -0,0 +1,32 @@ +import { createContext, useContext, memo } from 'react'; + +import createCodeHighlighterComposer from './createCodeHighlighterComposer'; +import { defaultHighlightCode } from './defaultHighlightCode'; + +export type HighlightCodeFn = ( + code: string, + language?: string | undefined, + options?: + | { + theme: string; + } + | undefined +) => string | DocumentFragment; + +export type CodeHighlighterContextType = { + highlightCode: HighlightCodeFn; +}; + +const CodeHighlighterContext = createContext( + Object.freeze({ + highlightCode: defaultHighlightCode + }) +); + +export function useCodeHighlighter() { + return useContext(CodeHighlighterContext).highlightCode; +} + +export const CodeHighlighterComposer = memo(createCodeHighlighterComposer(CodeHighlighterContext)); + +CodeHighlighterComposer.displayName = 'CodeHighlighterComposer'; diff --git a/packages/component/src/hooks/internal/codeHighlighter/private/createCodeHighlighterComposer.tsx b/packages/component/src/hooks/internal/codeHighlighter/private/createCodeHighlighterComposer.tsx new file mode 100644 index 0000000000..49a358bd8b --- /dev/null +++ b/packages/component/src/hooks/internal/codeHighlighter/private/createCodeHighlighterComposer.tsx @@ -0,0 +1,32 @@ +import React, { type ReactNode, useCallback, useMemo } from 'react'; + +import { useCodeHighlighter, type CodeHighlighterContextType, type HighlightCodeFn } from './CodeHighlighterComposer'; + +const createCodeHighlighterComposer = + ({ Provider }: React.Context) => + ({ children, highlightCode }: Readonly<{ children: ReactNode; highlightCode: HighlightCodeFn }>) => { + const previousCodeHighlighter = useCodeHighlighter(); + + const safeHighlightCode = useCallback( + (...args) => { + try { + return highlightCode(...args); + } catch (error) { + console.warn(`botframework-webchat: Failed to highlight code using ${highlightCode.name}`, error); + return previousCodeHighlighter(...args); + } + }, + [highlightCode, previousCodeHighlighter] + ); + + const CodeHighlighterContextValue = useMemo( + () => ({ + highlightCode: safeHighlightCode + }), + [safeHighlightCode] + ); + + return {children}; + }; + +export default createCodeHighlighterComposer; diff --git a/packages/component/src/hooks/internal/codeHighlighter/private/defaultHighlightCode.tsx b/packages/component/src/hooks/internal/codeHighlighter/private/defaultHighlightCode.tsx new file mode 100644 index 0000000000..dca0631d2b --- /dev/null +++ b/packages/component/src/hooks/internal/codeHighlighter/private/defaultHighlightCode.tsx @@ -0,0 +1,22 @@ +import type { HighlightCodeFn } from './CodeHighlighterComposer'; + +export const defaultHighlightCode: HighlightCodeFn = (source, language) => { + try { + const fragment = document.createDocumentFragment(); + const pre = document.createElement('pre'); + const code = document.createElement('code'); + + code.textContent = source; + + // Follow commonmark convention + language && code.classList.add(`language-${language}`); + + pre.append(code); + fragment.append(pre); + + return fragment; + } catch (error) { + console.warn(`botframework-webchat: Failed to display code`, error); + return '
';
+  }
+};
diff --git a/packages/component/src/internal.ts b/packages/component/src/internal.ts
index 11883ff09d..04fcfb4737 100644
--- a/packages/component/src/internal.ts
+++ b/packages/component/src/internal.ts
@@ -1,6 +1,20 @@
 import parseDocumentFragmentFromString from './Utils/parseDocumentFragmentFromString';
 import serializeDocumentFragmentIntoString from './Utils/serializeDocumentFragmentIntoString';
+import {
+  useCodeHighlighter,
+  CodeHighlighterComposer,
+  type HighlightCodeFn
+} from './hooks/internal/codeHighlighter/index';
 import useInjectStyles from './hooks/internal/useInjectStyles';
 import { useLiveRegion } from './providers/LiveRegionTwin/index';
 
-export { parseDocumentFragmentFromString, serializeDocumentFragmentIntoString, useInjectStyles, useLiveRegion };
+export {
+  CodeHighlighterComposer,
+  parseDocumentFragmentFromString,
+  serializeDocumentFragmentIntoString,
+  useCodeHighlighter,
+  useInjectStyles,
+  useLiveRegion
+};
+
+export type { HighlightCodeFn };
diff --git a/packages/component/src/providers/CustomElements/CustomElementsComposer.tsx b/packages/component/src/providers/CustomElements/CustomElementsComposer.tsx
index b1c7babf48..1c53e71821 100644
--- a/packages/component/src/providers/CustomElements/CustomElementsComposer.tsx
+++ b/packages/component/src/providers/CustomElements/CustomElementsComposer.tsx
@@ -1,5 +1,7 @@
 import mathRandom from 'math-random';
 import React, { memo, useCallback, useMemo, type ReactNode } from 'react';
+
+import useReactCodeBlockClass from './customElements/CodeBlock';
 import { CodeBlockCopyButtonElement } from './customElements/CodeBlockCopyButton';
 import CustomElementsContext from './private/CustomElementsContext';
 
@@ -38,7 +40,17 @@ const CustomElementsComposer = ({ children }: CustomElementsComposerProps) => {
     [registerCustomElement]
   );
 
-  const context = useMemo(() => Object.freeze({ codeBlockCopyButtonTagName }), [codeBlockCopyButtonTagName]);
+  const CodeBlockClass = useReactCodeBlockClass(codeBlockCopyButtonTagName);
+
+  const codeBlockTagName = useMemo(
+    () => registerCustomElement('code-block', CodeBlockClass),
+    [CodeBlockClass, registerCustomElement]
+  );
+
+  const context = useMemo(
+    () => Object.freeze({ codeBlockTagName, codeBlockCopyButtonTagName }),
+    [codeBlockTagName, codeBlockCopyButtonTagName]
+  );
 
   return {children};
 };
diff --git a/packages/component/src/providers/CustomElements/customElements/CodeBlock.ts b/packages/component/src/providers/CustomElements/customElements/CodeBlock.ts
new file mode 100644
index 0000000000..e8942e68d6
--- /dev/null
+++ b/packages/component/src/providers/CustomElements/customElements/CodeBlock.ts
@@ -0,0 +1,224 @@
+/* eslint-disable class-methods-use-this */
+import { hooks } from 'botframework-webchat-api';
+import { ReactNode, useMemo, useRef } from 'react';
+
+import { useStyleSet } from '../../../hooks';
+import { defaultHighlightCode, HighlightCodeFn } from '../../../hooks/internal/codeHighlighter';
+import { parseDocumentFragmentFromString, useCodeHighlighter } from '../../../internal';
+
+const { useStyleOptions, useLocalizer } = hooks;
+
+class CodeBlock extends HTMLElement {
+  static observedAttributes = ['theme', 'language'];
+
+  #connected = false;
+  #originalFragment: DocumentFragment = undefined;
+  #updateTask?: Promise;
+
+  copyButtonElement: HTMLElement;
+
+  get code() {
+    return this.querySelector('code')?.textContent ?? '';
+  }
+
+  get theme() {
+    return this.getAttribute('theme');
+  }
+
+  get language() {
+    return this.getAttribute('language');
+  }
+
+  get options() {
+    const { theme } = this;
+    return theme ? { theme } : undefined;
+  }
+
+  connectedCallback() {
+    this.#connected = true;
+    this.scheduleUpdate();
+  }
+
+  disconnectedCallback() {
+    this.#connected = false;
+  }
+
+  attributeChangedCallback(_name: string, oldValue: string, newValue: string) {
+    const updated = !Object.is(oldValue, newValue);
+
+    this.#connected && updated && this.scheduleUpdate();
+  }
+
+  scheduleUpdate() {
+    if (this.#updateTask) {
+      return;
+    }
+
+    this.#updateTask = Promise.resolve().then(() => {
+      this.#updateTask = undefined;
+      this.update();
+    });
+  }
+
+  update() {
+    const { code, language, options, ownerDocument: document } = this;
+
+    if (!code) {
+      return;
+    }
+
+    if (!this.#originalFragment) {
+      this.#originalFragment = document.createDocumentFragment();
+      this.#originalFragment.replaceChildren(...this.children);
+    }
+
+    const result = this.highlightCode(code, language, options);
+    const highlightedCodeFragment =
+      result instanceof DocumentFragment ? result : parseDocumentFragmentFromString(result);
+
+    const body = highlightedCodeFragment.querySelector('pre');
+
+    body?.classList.add('webchat__code-block__body');
+    options?.theme && body?.classList.add(`webchat__code-block__theme--${options.theme}`);
+
+    highlightedCodeFragment.insertBefore(this.copyButtonElement, highlightedCodeFragment.firstChild);
+
+    this.replaceChildren(highlightedCodeFragment);
+
+    if (this.copyButtonElement) {
+      this.copyButtonElement.dataset.value = code;
+    }
+  }
+
+  highlightCode(...args: Parameters) {
+    return defaultHighlightCode(...args);
+  }
+}
+
+export type CodeBlockProps = Readonly<{
+  children?: ReactNode | undefined;
+  className?: string | undefined;
+  language?: string | undefined;
+  theme?: string | undefined;
+}>;
+
+type CodeBlockReactProps = Readonly<{
+  codeBlockClass: string | undefined;
+  codeBlockTheme: 'github-light-default' | 'github-dark-default';
+  copyButtonAltCopied: string;
+  copyButtonAltCopy: string;
+  copyButtonClassName: string;
+  copyButtonTagName: string;
+  highlightCode: HighlightCodeFn;
+}>;
+
+const useCodeBlockProps = (copyButtonTagName: string) => {
+  const highlightCode = useCodeHighlighter();
+  const localize = useLocalizer();
+  const [{ codeBlock: codeBlockClass, codeBlockCopyButton: copyButtonClassName }] = useStyleSet();
+  const [{ codeBlockTheme }] = useStyleOptions();
+  const copyButtonAltCopied = localize('COPY_BUTTON_COPIED_TEXT');
+  const copyButtonAltCopy = localize('COPY_BUTTON_TEXT');
+  const propsChangedEventTarget = useMemo(() => new EventTarget(), []);
+  const propsRef = useRef();
+
+  useMemo(() => {
+    propsRef.current = Object.freeze({
+      codeBlockClass,
+      codeBlockTheme,
+      copyButtonAltCopied,
+      copyButtonAltCopy,
+      copyButtonClassName,
+      copyButtonTagName,
+      highlightCode
+    });
+
+    propsChangedEventTarget.dispatchEvent(new CustomEvent('change'));
+  }, [
+    codeBlockClass,
+    codeBlockTheme,
+    copyButtonAltCopied,
+    copyButtonAltCopy,
+    copyButtonClassName,
+    copyButtonTagName,
+    highlightCode,
+    propsChangedEventTarget
+  ]);
+
+  return useMemo(
+    () => Object.freeze([propsChangedEventTarget, propsRef] as const),
+    [propsChangedEventTarget, propsRef]
+  );
+};
+
+export default function useReactCodeBlockClass(copyButtonTagName: string) {
+  const [codeBlockTarget, codeBlockPropsRef] = useCodeBlockProps(copyButtonTagName);
+
+  return useMemo(
+    () =>
+      class ReactCodeBlock extends CodeBlock {
+        static observedAttributes = CodeBlock.observedAttributes;
+
+        #prevProps: CodeBlockReactProps | undefined;
+
+        get #props() {
+          return codeBlockPropsRef.current;
+        }
+
+        #handlePropsChange = () => {
+          const props = this.#props;
+          const prevProps = this.#prevProps;
+
+          this.#prevProps = props;
+
+          if (prevProps?.codeBlockClass !== props?.codeBlockClass) {
+            prevProps?.codeBlockClass && this.classList.remove(prevProps.codeBlockClass);
+            props?.codeBlockClass && this.classList.add(props.codeBlockClass);
+          }
+
+          this.setAttribute('theme', props.codeBlockTheme);
+
+          if (prevProps?.highlightCode !== props.highlightCode) {
+            this.scheduleUpdate();
+          }
+
+          if (prevProps?.copyButtonTagName !== props.copyButtonTagName || !this.copyButtonElement) {
+            const { ownerDocument: document } = this;
+
+            this.copyButtonElement = document.createElement(props.copyButtonTagName);
+            this.scheduleUpdate();
+          }
+
+          this.copyButtonElement.className = props.copyButtonClassName;
+          this.copyButtonElement.dataset.altCopy = props.copyButtonAltCopy;
+          this.copyButtonElement.dataset.altCopied = props.copyButtonAltCopied;
+        };
+
+        connectedCallback(): void {
+          this.classList.add('webchat__code-block');
+
+          codeBlockTarget.addEventListener('change', this.#handlePropsChange);
+
+          this.#handlePropsChange();
+          super.connectedCallback();
+        }
+
+        disconnectedCallback(): void {
+          codeBlockTarget.removeEventListener('change', this.#handlePropsChange);
+
+          super.disconnectedCallback();
+        }
+
+        highlightCode(...args: Parameters) {
+          const [, language, options] = args;
+
+          if (!language || !options) {
+            return defaultHighlightCode(...args);
+          }
+
+          return this.#props.highlightCode(...args);
+        }
+      },
+    [codeBlockPropsRef, codeBlockTarget]
+  );
+}
diff --git a/packages/component/src/providers/CustomElements/customElements/wrapAsCustomElement.ts b/packages/component/src/providers/CustomElements/customElements/wrapAsCustomElement.ts
index cf37f79141..145f9293e9 100644
--- a/packages/component/src/providers/CustomElements/customElements/wrapAsCustomElement.ts
+++ b/packages/component/src/providers/CustomElements/customElements/wrapAsCustomElement.ts
@@ -1,6 +1,12 @@
 import { createElement, type ComponentType } from 'react';
 import { render, unmountComponentAtNode } from 'react-dom';
 
+const ConnectionState = {
+  DISCONNECTED: 0,
+  CONNECTED: 1,
+  PENDING: 2
+};
+
 export default function wrapAsCustomElement(
   component: ComponentType,
   propKeys: (keyof Props)[]
@@ -14,6 +20,8 @@ export default function wrapAsCustomElement
+          Object.is(prevProps.get(key), this.#propMap.get(key))
+        );
+
       // For every attribute change, browser will call this function again. It is not batched.
-      render(createElement(component, this.#getProps()), this);
+      !areEqual &&
+        this.#connected === ConnectionState.CONNECTED &&
+        render(createElement(component, this.#getProps()), this);
     }
 
     connectedCallback() {
-      render(createElement(component, this.#getProps()), this);
+      this.#connected !== ConnectionState.PENDING && render(createElement(component, this.#getProps()), this);
+      this.#connected = ConnectionState.CONNECTED;
     }
 
-    disconnectedCallback() {
-      unmountComponentAtNode(this);
+    async disconnectedCallback() {
+      this.#connected = ConnectionState.PENDING;
+      await Promise.resolve();
+      if (this.#connected === ConnectionState.PENDING) {
+        this.#connected = ConnectionState.DISCONNECTED;
+        unmountComponentAtNode(this);
+        this.replaceChildren();
+      }
     }
   };
 }
diff --git a/packages/component/src/providers/CustomElements/private/CustomElementsContext.ts b/packages/component/src/providers/CustomElements/private/CustomElementsContext.ts
index bb3bebfb7a..755a16cada 100644
--- a/packages/component/src/providers/CustomElements/private/CustomElementsContext.ts
+++ b/packages/component/src/providers/CustomElements/private/CustomElementsContext.ts
@@ -1,6 +1,6 @@
 import { createContext } from 'react';
 
-export type CustomElementsContextType = Readonly<{ codeBlockCopyButtonTagName: string }>;
+export type CustomElementsContextType = Readonly<{ codeBlockTagName: string; codeBlockCopyButtonTagName: string }>;
 
 const CustomElementsContext = createContext(
   new Proxy({} as CustomElementsContextType, {
diff --git a/packages/component/src/providers/CustomElements/useCodeBlockTagName.ts b/packages/component/src/providers/CustomElements/useCodeBlockTagName.ts
new file mode 100644
index 0000000000..3313272d3a
--- /dev/null
+++ b/packages/component/src/providers/CustomElements/useCodeBlockTagName.ts
@@ -0,0 +1,19 @@
+import { createElement, memo, useMemo } from 'react';
+
+import { CodeBlockProps } from './customElements/CodeBlock';
+import useCustomElementsContext from './private/useCustomElementsContext';
+
+export default function useCodeBlockTag() {
+  const { codeBlockTagName } = useCustomElementsContext();
+
+  return useMemo(
+    () =>
+      Object.freeze([
+        codeBlockTagName,
+        memo((props: CodeBlockProps) =>
+          createElement(codeBlockTagName, { ...props, class: props.className, className: undefined })
+        )
+      ] as const),
+    [codeBlockTagName]
+  );
+}
diff --git a/packages/component/src/providers/HTMLContentTransformCOR/private/HTMLContentTransformContext.ts b/packages/component/src/providers/HTMLContentTransformCOR/private/HTMLContentTransformContext.ts
index ddffab2248..184c0cc578 100644
--- a/packages/component/src/providers/HTMLContentTransformCOR/private/HTMLContentTransformContext.ts
+++ b/packages/component/src/providers/HTMLContentTransformCOR/private/HTMLContentTransformContext.ts
@@ -8,10 +8,7 @@ export type HTMLContentTransformRequest = Readonly<{
       attributes: ReadonlySet;
     }>
   >;
-  codeBlockCopyButtonAltCopied: string;
-  codeBlockCopyButtonAltCopy: string;
-  codeBlockCopyButtonClassName: string;
-  codeBlockCopyButtonTagName: string;
+  codeBlockTagName: string;
   documentFragment: DocumentFragment;
   externalLinkAlt: string;
 }>;
diff --git a/packages/component/src/providers/HTMLContentTransformCOR/useTransformHTMLContent.ts b/packages/component/src/providers/HTMLContentTransformCOR/useTransformHTMLContent.ts
index d006a83703..d518693549 100644
--- a/packages/component/src/providers/HTMLContentTransformCOR/useTransformHTMLContent.ts
+++ b/packages/component/src/providers/HTMLContentTransformCOR/useTransformHTMLContent.ts
@@ -1,8 +1,7 @@
 import { hooks } from 'botframework-webchat-api';
 import { useCallback } from 'react';
 
-import { useStyleSet } from '../../hooks/index';
-import useCodeBlockCopyButtonTagName from '../CustomElements/useCodeBlockCopyButtonTagName';
+import useCodeBlockTag from '../CustomElements/useCodeBlockTagName';
 import useHTMLContentTransformContext from './private/useHTMLContentTransformContext';
 
 const { useLocalizer } = hooks;
@@ -169,33 +168,20 @@ const DEFAULT_ALLOWED_TAGS: ReadonlyMap DocumentFragment {
-  const [{ codeBlockCopyButton: codeBlockCopyButtonClassName }] = useStyleSet();
-  const [codeBlockCopyButtonTagName] = useCodeBlockCopyButtonTagName();
+  const [codeBlockTagName] = useCodeBlockTag();
   const { transform } = useHTMLContentTransformContext();
 
   const localize = useLocalizer();
-  const codeBlockCopyButtonAltCopied = localize('COPY_BUTTON_COPIED_TEXT');
-  const codeBlockCopyButtonAltCopy = localize('COPY_BUTTON_TEXT');
   const externalLinkAlt = localize('MARKDOWN_EXTERNAL_LINK_ALT');
 
   return useCallback(
     documentFragment =>
       transform({
         allowedTags: DEFAULT_ALLOWED_TAGS,
-        codeBlockCopyButtonAltCopied,
-        codeBlockCopyButtonAltCopy,
-        codeBlockCopyButtonClassName,
-        codeBlockCopyButtonTagName,
+        codeBlockTagName,
         documentFragment,
         externalLinkAlt
       }),
-    [
-      codeBlockCopyButtonAltCopied,
-      codeBlockCopyButtonAltCopy,
-      codeBlockCopyButtonClassName,
-      codeBlockCopyButtonTagName,
-      externalLinkAlt,
-      transform
-    ]
+    [codeBlockTagName, externalLinkAlt, transform]
   );
 }
diff --git a/packages/component/tsup.config.ts b/packages/component/tsup.config.ts
index e925f68869..9d894eb453 100644
--- a/packages/component/tsup.config.ts
+++ b/packages/component/tsup.config.ts
@@ -18,12 +18,7 @@ const config: typeof baseConfig = {
   loader: {
     ...baseConfig.loader,
     '.css': 'local-css'
-  },
-  noExternal: [
-    // Belows are the dependency chain related to "regex" where it is named export-only and does not work on Webpack 4/PPUX (CJS cannot import named export).
-    // Webpack 4: "Can't import the named export 'rewrite' from non EcmaScript module (only default export is available)"
-    'shiki', // shiki -> @shikijs/core -> @shikijs/engine-javascript -> regex
-  ]
+  }
 };
 
 export default defineConfig([
diff --git a/packages/fluent-theme/src/components/theme/Theme.module.css b/packages/fluent-theme/src/components/theme/Theme.module.css
index 8668a78f9a..351edb8c2a 100644
--- a/packages/fluent-theme/src/components/theme/Theme.module.css
+++ b/packages/fluent-theme/src/components/theme/Theme.module.css
@@ -74,6 +74,8 @@
 
   /* New greys from the link above not exposed by Fluent */
   --webchat-colorGrey8: #141414;
+  --webchat-colorGrey14: #242424;
+  --webchat-colorGrey92: #ebebeb;
   --webchat-colorGrey98: #fafafa;
 
   /* https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/utils/shadows.ts */
@@ -225,7 +227,8 @@
 /* Scrollbars */
 :global(.webchat-fluent).theme :global(.webchat__basic-transcript .webchat__basic-transcript__scrollable),
 :global(.webchat-fluent).theme :global(.webchat__view-code-dialog__code-body),
-:global(.webchat-fluent).theme :global(.webchat__render-markdown [data-math-type='block']) {
+:global(.webchat-fluent).theme :global(.webchat__render-markdown [data-math-type='block']),
+:global(.webchat-fluent).theme :global(.webchat__view-code-dialog__body) {
   /* Edge uses -webkit-scrollbar if scrollbar-* is not set */
   scrollbar-color: unset;
   scrollbar-width: unset;
@@ -391,6 +394,72 @@
   }
 }
 
+/* Code block */
+:global(.webchat-fluent).theme :global(.webchat__code-block) {
+  border: none;
+  font-family: var(--webchat-fontFamilyMonospace);
+  font-size: var(--webchat-fontSizeBase300);
+
+  --webchat__code-block__copy-button--color: var(--webchat-colorNeutralForeground1);
+  --webchat__code-block__copy-button--background: var(--webchat-colorNeutralBackground3);
+
+  :global(.webchat__code-block-copy-button) {
+    background: transparent;
+    border: none;
+    color: var(--webchat__code-block__copy-button--color);
+    height: 20px;
+    margin-inline-start: var(--webchat-spacingHorizontalS);
+    padding: 0;
+    width: 20px;
+    transition: background-color var(--webchat-durationNormal) var(--webchat-curveDecelerateMid);
+
+    :global(.webchat__code-block-copy-button__icon) {
+      background-color: currentColor;
+    }
+
+    &:hover {
+      background: var(--webchat__code-block__copy-button--background);
+      color: var(--webchat__code-block__copy-button--color);
+    }
+
+    &:active {
+      background: var(--webchat__code-block__copy-button--background);
+      color: var(--webchat__code-block__copy-button--color);
+    }
+
+    &:focus {
+      color: var(--webchat__code-block__copy-button--color);
+      outline-color: currentColor;
+    }
+  }
+
+  &:global(:has(.webchat__code-block__body:focus-visible)):focus-within {
+    outline: var(--webchat-strokeWidthThick) solid var(--webchat-colorStrokeFocus2);
+    outline-offset: calc(var(--webchat-strokeWidthThick) * -1);
+
+    :global(.webchat__code-block__body) {
+      outline: none;
+    }
+  }
+
+  &:global(:has(> .webchat__code-block__theme--github-dark-default)) {
+    --webchat__background--code-block: var(--codeBlockBackground, var(--webchat-colorGrey8));
+    --webchat__code-block__copy-button--background: var(
+      --codeBlockCopyButtonBackgroundPressed,
+      var(--webchat-colorGrey14)
+    );
+    --webchat__code-block__copy-button--color: var(--codeBlockCopyButtonForeground, var(--webchat__color--code-block));
+    --webchat__color--code-block: var(--codeBlockForeground, var(--webchat-colorGrey98));
+  }
+
+  &:global(:has(> .webchat__code-block__theme--github-light-default)) {
+    --webchat__background--code-block: var(--codeBlockBackground, var(--webchat-colorGrey98));
+    --webchat__code-block__copy-button--background: var(--codeBlockBackgroundPressed, var(--webchat-colorGrey92));
+    --webchat__code-block__copy-button--color: var(--codeBlockCopyButtonForeground, var(--webchat__color--code-block));
+    --webchat__color--code-block: var(--codeBlockForeground, var(--webchat-colorGrey8));
+  }
+}
+
 /* View Code dialog */
 :global(.webchat-fluent).theme :global(.webchat__modal-dialog.webchat__view-code-dialog) {
   :global(.webchat__modal-dialog__close-button-layout) {
@@ -408,34 +477,14 @@
     width: unset;
   }
 
-  :global(.webchat__view-code-dialog__copy-button) {
-    background: transparent;
-    border: none;
-    color: var(--webchat-colorNeutralForeground1);
-    height: 20px;
+  :global(.webchat__code-block-copy-button) {
+    --webchat__code-block__copy-button--color: var(--webchat-colorNeutralForeground1);
+    --webchat__code-block__copy-button--background: var(--webchat-colorNeutralBackground3);
+
     margin-block-start: var(--webchat-spacingVerticalS);
-    padding: 0;
     position: absolute;
     right: 32px;
     top: 0;
-    transition: background-color var(--webchat-durationNormal) var(--webchat-curveDecelerateMid);
-    width: 20px;
-
-    :global(.webchat__code-block-copy-button__icon) {
-      background-color: currentColor;
-    }
-
-    &:hover {
-      background: var(--webchat-colorNeutralBackground3);
-    }
-
-    &:active {
-      background: var(--webchat-colorNeutralBackground3);
-    }
-
-    &:focus {
-      outline-color: var(--webchat-colorStrokeFocus2);
-    }
   }
 
   :global(.webchat__modal-dialog__box) {
@@ -454,33 +503,10 @@
   }
 
   :global(.webchat__view-code-dialog__body) {
-    display: contents;
-  }
-
-  :global(.webchat__view-code-dialog__code-body) {
-    background: var(--webchat__code-block--background);
     font-family: var(--webchat-fontFamilyMonospace);
     font-size: var(--webchat-fontSizeBase300);
-    line-height: var(--webchat-fontSizeBase300);
-    margin-inline: calc(var(--webchat-spacingHorizontalM) * -1);
-    padding-inline: var(--webchat-spacingHorizontalM);
-  }
-
-  :global(.webchat__view-code-dialog__code-body:has(.shiki:focus-visible)):focus-within {
-    outline: var(--webchat-strokeWidthThick) solid var(--webchat-colorStrokeFocus2);
-    outline-offset: calc(var(--webchat-strokeWidthThick) * -1);
-
-    :global(.shiki) {
-      outline: none;
-    }
-  }
-
-  :global(.webchat__view-code-dialog__code-body:has(.shiki.github-light-default)) {
-    --webchat__code-block--background: var(--codeBlockBackground, var(--webchat-colorGrey98));
-  }
-
-  :global(.webchat__view-code-dialog__code-body:has(.shiki.github-dark-default)) {
-    --webchat__code-block--background: var(--codeBlockBackground, var(--webchat-colorGrey8));
+    margin: 0 calc(var(--webchat-spacingHorizontalM) * -1);
+    padding: 0 var(--webchat-spacingHorizontalM);
   }
 
   :global(.webchat__view-code-dialog__footer) {