Skip to content

🐞 Crash and Data Loss If Layout Name uses parentheses #511

@matthttam

Description

@matthttam

Check for existing bug reports before submitting.

  • I searched for existing Bug Reports and found no similar reports.

Expected Behavior

Saving the appearance of a layout should work even if the layout has either ( or ) in the name.

Current behaviour

If the layout is named with a ( or ) and you try to update the appearance the app crashes mid-save and corrupts the layout. Subsequent reloads of fantasy statblock will fail.

Reproduction

Launch obsidian with the fantasy statblock extension installed.
Navigate to Settings > Fantasy Statblocks
Under the layout section, download a layout.
On windows you can copy/past it and it will renamed with layout_name (1).json otherwise you can just add ( and ) to it manually.
Back in the layout section perform an import of this layout.
Restart obsidian so it is fully loaded. You can test the new layout, it works as expected.
Now, attempt to modify that layout by adjusting the appearance section in any way.

The extension crashes on this:

generateStyleSheet(e, t=`FS_CSS_PROPERTIES_${e.id}`) {

            if (!e.cssProperties)

                return null;

            let i = document.head.createEl("style", {

                attr: {

                    id: t

                }

            })

              , a = this.getSheetRules(e);

            for (let s of a)

                i.sheet.insertRule(s, i.sheet.cssRules.length);

            return i

        }

The error is something like:

This is failing on the i.sheet.insertRule.

DOMException: Failed to execute 'insertRule' on 'CSSStyleSheet': Failed to parse the rule '.layout-copy-(1) { --statblock-primary-color: #7a200d; --statblock-rule-color: #922610; --statblock-background-color: #fdf1dc; --statblock-border-size: 1px; --statblock-border-color: #ddd; --statblock-bar-color: #e69a28; --statblock-bar-border-size: 1px; --statblock-bar-border-color: #000; --statblock-image-width: 75px; --statblock-image-height: 75px; --statblock-image-border-size: 2px; --statblock-image-border-color: var(--statblock-primary-color); --statblock-box-shadow-color: #ddd; --statblock-box-shadow-x-offset: 0; --statblock-box-shadow-y-offset: 0; --statblock-box-shadow-blur: 1.5em; --statblock-font-color: var(--statblock-primary-color); --statblock-font-weight: 700; --statblock-content-font: "Noto Sans", "Myriad Pro", Calibri, Helvetica, Arial, sans-serif; --statblock-content-font-size: 14px; --statblock-heading-font: "Libre Baskerville", "Lora", "Calisto MT", "Bookman Old Style", Bookman, "Goudy Old Style", Garamond, "Hoefler Text", "Bitstream Charter", Georgia, serif; --statblock-heading-font-color: var(--statblock-font-color); --statblock-heading-font-size: 23px; --statblock-heading-font-variant: small-caps; --statblock-heading-font-weight: var(--statblock-font-weight); --statblock-section-heading-border-size: 2px; --statblock-section-heading-border-color: var(--statblock-primary-color); --statblock-section-heading-font: null; --statblock-section-heading-font-color: var(--statblock-font-color); --statblock-section-heading-font-size: 21px; --statblock-section-heading-font-variant: small-caps; --statblock-section-heading-font-weight: normal; --statblock-subheading-font: var(--statblock-content-font); --statblock-subheading-font-color: var(--statblock-font-color); --statblock-subheading-font-size: 12px; --statblock-subheading-font-style: italic; --statblock-subheading-font-weight: normal; --statblock-property-font: var(--statblock-content-font); --statblock-property-font-color: var(--statblock-font-color); --statblock-property-font-variant: normal; --statblock-property-font-size: var(--statblock-content-font-size); --statblock-property-font-weight: null; --statblock-property-name-font: var(--statblock-content-font); --statblock-property-name-font-color: var(--statblock-font-color); --statblock-property-name-font-weight: null; --statblock-property-name-font-variant: null; --statblock-property-name-font-size: var(--statblock-content-font-size); --statblock-traits-font: var(--statblock-content-font); --statblock-traits-font-color: var(--statblock-font-color); --statblock-traits-font-size: var(--statblock-content-font-size); --statblock-traits-font-weight: null; --statblock-traits-font-style: null; --statblock-traits-name-font: var(--statblock-content-font); --statblock-traits-name-font-color: var(--statblock-font-color); --statblock-traits-name-font-size: var(--statblock-content-font-size); --statblock-traits-name-font-weight: bold; --statblock-traits-name-font-style: italic; }'.

Why it crashes here: The insertRule method is a native browser command that is extremely strict. It tries to inject a text string directly into the browser's CSS engine.

The Rule Being Injected: .layout-copy-(1) { ... }

The Problem: The browser sees .layout-copy-(1) and rejects it immediately because parentheses ( ) are not allowed in CSS class names unless they are "escaped" (written as (1)).

The Result: The browser throws a DOMException: Failed to parse the rule, causing the Javascript to stop running (crash).

Yes, you have found the exact location where the crash occurs, but the root cause is slightly upstream.

The Crash Location
The error happens specifically at this line inside generateStyleSheet:

TypeScript

stylesheet.sheet.insertRule(rule, stylesheet.sheet.cssRules.length);
Why it crashes here: The insertRule method is a native browser command that is extremely strict. It tries to inject a text string directly into the browser's CSS engine.

The Rule Being Injected: .layout-copy-(1) { ... }

The Problem: The browser sees .layout-copy-(1) and rejects it immediately because parentheses ( ) are not allowed in CSS class names unless they are "escaped" (written as (1)).

The Result: The browser throws a DOMException: Failed to parse the rule, causing the Javascript to stop running (crash).

The Root Cause (Logic Bug)
While generateStyleSheet is where it dies, the logic error is likely in getSheetRules (which calls #buildSheetRule).

The plugin is taking your layout's name ("Layout Copy (1)") and trying to convert it into a CSS class name (".layout-copy-(1)") without removing or escaping the parentheses. This is why renaming the layout to "Layout Copy 1" (removing the special characters) fixed the issue.

Summary:

Is this the bug location? Yes, this is where the code explodes.

Is this code wrong? No, insertRule is standard. The data fed into it (the rule string with parentheses) is what is "poisoned."

Which Operating Systems are you using?

  • Android
  • iPhone/iPad
  • Linux
  • macOS
  • Windows

Obsidian Version Check

1.10.6 and 1.6.7

Plugin Version

4.10.2

Confirmation

  • I have disabled all other plugins and the issue still persists.

Possible solution

To fix the crash caused by invalid CSS selectors (like .layout-copy-(1)), you need to modify how the class name is generated in the getSheetRules method.

This method is responsible for building the CSS rule string. Currently, it takes the layout name and just adds a dot, which creates invalid CSS if the name has parentheses.

Here is where you need to make the change in LayoutManager:

1. Locate getSheetRules

Find this method inside the LayoutManager class (around line 43 in your snippet).

    getSheetRules(layout: Layout): string[] {
        if (!layout.cssProperties) return [];
        const layoutName = `.${slugifyLayoutForCss(layout.name)}`; // <--- THIS IS THE BUG
        const rules: string[] = [
            this.#buildSheetRule(layoutName, {
                ...DefaultLayoutCSSProperties,
                ...layout.cssProperties
            })
        ];
        // ...

2. The Fix

Change the layoutName definition to strip out any parentheses or special characters that slugifyLayoutForCss missed.

Replace this line:

const layoutName = `.${slugifyLayoutForCss(layout.name)}`;

With this:

// Fix: Remove parentheses and other invalid chars from the CSS class name
const layoutName = `.${slugifyLayoutForCss(layout.name).replace(/[^\w-]/g, '')}`;

Why this fixes it

  1. slugifyLayoutForCss converts "Layout Copy (1)" to "layout-copy-(1)".
  2. CSS insertRule fails because ( and ) are reserved characters in CSS selectors unless escaped.
  3. .replace(/[^\w-]/g, '') removes anything that isn't a letter, number, underscore, or hyphen.
    • "layout-copy-(1)" becomes "layout-copy-1".
  4. The CSS selector becomes .layout-copy-1, which is valid, and the plugin stops crashing.

That being said, I am not sure if this would effect functionality elsewhere in the plugin.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions