Repository: marp-team/marp-core (ts)

Issue:
Code block in HTML presentation issues Uncaught DOMException: Operation is not supported
### Version of Marp Tool

marp-vscode 2.8.0

### Operating System

macOS

### Environment

- VS Code (version 1.88)]
- MacOS Sonoma 14.4.1
- Mozilla Firefox (125.0.2 (64-bit))

### How to reproduce

Any code block defined in presentation:

`---`
`marp: true`
`---`

` ``` `
` ``` `

Presentation exported to HTML and viewed in mozilla.

### Expected behavior

No loading/console errors.

### Actual behavior

Presentation loads, but with errors. This is especially visible with a larger presentation and a mobile browser.

Browser console output:
```
Uncaught DOMException: Operation is not supported
    <anonymous> file:///Users/user/slides/test.html:14
    w file:///Users/vdangovas/user/slides/test.html:14
    w file:///Users/vdangovas/user/slides/test.html:14
    <anonymous> file:///Users/user/slides/test.html:14
    <anonymous> file:///Users/user/slides/[test.html:14](file:///Users/user/slides/test.html) test.html:14
    <anonymous> file:///Users/user/slides/test.html:14
    w file:///Users/user/slides/test.html:14
    w file:///Users/user/slides/test.html:14
    <anonymous> file:///Users/user/slides/test.html:14
    <anonymous> file:///Users/user/slides/test.html:14
```

Line 14 is `<script>!function()[...]`

### Additional information

Public instance with these errors: https://vainiusd.github.io/marp-cheatslides/

Pull request description:
Suppress uncaught `DOMException` error while upgrading `<marp-pre>` Web Component elements in Firefox
Fix #370.

This error is the error brought by Firefox while upgrading web components, and that is not affected to the subsequent browser script running on JavaScript main threads. Nevertheless, supressing errors is well-mannered.

- When updating the shadow root, we will check whether that is prepared.
- Known error `NotSupportedError` in Firefox will be simply suppressed.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c146256..4ce62f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,10 @@
 * Upgrade dependent packages to the latest version ([#380](https://github.com/marp-team/marp-core/pull/380))
 * Switch package manager from yarn to npm ([#379](https://github.com/marp-team/marp-core/pull/379))
 
+### Fixed
+
+- Suppress uncaught `DOMException` error while upgrading `<marp-pre>` Web Component elements in Firefox ([#370](https://github.com/marp-team/marp-core/issues/370), [#384](https://github.com/marp-team/marp-core/pull/384))
+
 ## v3.9.0 - 2023-10-15
 
 ### Added
diff --git a/jest.config.js b/jest.config.mjs
similarity index 70%
rename from jest.config.js
rename to jest.config.mjs
index 6c8e071..ee8f9b5 100644
--- a/jest.config.js
+++ b/jest.config.mjs
@@ -1,6 +1,9 @@
-const { defaults: tsjPreset } = require('ts-jest/presets')
+import { createDefaultPreset } from 'ts-jest'
 
-module.exports = {
+const tsjPreset = createDefaultPreset()
+
+/** @type {import('jest').Config} */
+const config = {
   collectCoverageFrom: ['src/**/*.{j,t}s'],
   coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'],
   coverageThreshold: { global: { lines: 95 } },
@@ -12,3 +15,5 @@ module.exports = {
   },
   prettierPath: null,
 }
+
+export default config
diff --git a/src/custom-elements/browser/marp-custom-element.ts b/src/custom-elements/browser/marp-custom-element.ts
index 020ca74..9152184 100644
--- a/src/custom-elements/browser/marp-custom-element.ts
+++ b/src/custom-elements/browser/marp-custom-element.ts
@@ -5,7 +5,7 @@ export const createMarpCustomElement = <T extends Constructor<HTMLElement>>(
   { attrs = {}, style }: { attrs?: Record<string, string>; style?: string },
 ) =>
   class MarpCustomElement extends Base {
-    declare shadowRoot: ShadowRoot
+    declare readonly shadowRoot: ShadowRoot | null
 
     constructor(...args: any[]) {
       super(...args)
@@ -14,7 +14,7 @@ export const createMarpCustomElement = <T extends Constructor<HTMLElement>>(
         if (!this.hasAttribute(key)) this.setAttribute(key, value)
       }
 
-      this.attachShadow({ mode: 'open' })
+      this._shadow()
     }
 
     static get observedAttributes() {
@@ -29,19 +29,34 @@ export const createMarpCustomElement = <T extends Constructor<HTMLElement>>(
       this._update()
     }
 
+    _shadow() {
+      if (!this.shadowRoot) {
+        try {
+          this.attachShadow({ mode: 'open' })
+        } catch (e) {
+          if (!(e instanceof Error && e.name === 'NotSupportedError')) throw e
+        }
+      }
+      return this.shadowRoot
+    }
+
     _update() {
-      const styleTag = style ? `<style>:host { ${style} }</style>` : ''
-      let slotTag = '<slot></slot>'
+      const shadowRoot = this._shadow()
 
-      const { autoScaling } = this.dataset
+      if (shadowRoot) {
+        const styleTag = style ? `<style>:host { ${style} }</style>` : ''
+        let slotTag = '<slot></slot>'
 
-      if (autoScaling !== undefined) {
-        const downscale =
-          autoScaling === 'downscale-only' ? 'data-downscale-only' : ''
+        const { autoScaling } = this.dataset
 
-        slotTag = `<marp-auto-scaling exportparts="svg:auto-scaling" ${downscale}>${slotTag}</marp-auto-scaling>`
-      }
+        if (autoScaling !== undefined) {
+          const downscale =
+            autoScaling === 'downscale-only' ? 'data-downscale-only' : ''
+
+          slotTag = `<marp-auto-scaling exportparts="svg:auto-scaling" ${downscale}>${slotTag}</marp-auto-scaling>`
+        }
 
-      this.shadowRoot.innerHTML = styleTag + slotTag
+        shadowRoot.innerHTML = styleTag + slotTag
+      }
     }
   }


Test patch:
diff --git a/test/custom-elements/browser.ts b/test/custom-elements/browser.ts
index ebb35b9..0176c5a 100644
--- a/test/custom-elements/browser.ts
+++ b/test/custom-elements/browser.ts
@@ -84,6 +84,34 @@ describe('The hydration script for custom elements', () => {
       )
     })
 
+    it('does not throw known DOMException error while upgrading <pre is="marp-pre"> to <marp-pre> (for Firefox)', () => {
+      document.body.innerHTML = '<pre is="marp-pre">1</pre>'
+
+      jest
+        .spyOn(HTMLElement.prototype, 'attachShadow')
+        .mockImplementationOnce(() => {
+          throw new DOMException(
+            'Element.attachShadow: Unable to attach ShadowDOM',
+            'NotSupportedError',
+          )
+        })
+
+      expect(() => browser.applyCustomElements()).not.toThrow()
+    })
+
+    it.skip('throws error if unknown error occured while upgrading <pre is="marp-pre"> to <marp-pre>', () => {
+      document.body.innerHTML = '<pre is="marp-pre">1</pre>'
+
+      jest.spyOn(console, 'error').mockImplementation(() => {})
+      jest
+        .spyOn(HTMLElement.prototype, 'attachShadow')
+        .mockImplementationOnce(() => {
+          throw new Error('Unknown error while attaching shadow')
+        })
+
+      expect(() => browser.applyCustomElements()).toThrow()
+    })
+
     it('does not replace <h1 is="marp-h1"> to <marp-h1>', () => {
       const html = '<h1 is="marp-h1">test</h1>'
       document.body.innerHTML = html