-
-
Notifications
You must be signed in to change notification settings - Fork 90
Description
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
slugifyLayoutForCssconverts "Layout Copy (1)" to "layout-copy-(1)".- CSS
insertRulefails because(and)are reserved characters in CSS selectors unless escaped. .replace(/[^\w-]/g, '')removes anything that isn't a letter, number, underscore, or hyphen.- "layout-copy-(1)" becomes "layout-copy-1".
- 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.