Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 80 additions & 19 deletions lib/shared/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,10 @@ export class ThemeManagerBase extends GObject.Object {
* @param {any} selector
*/
getCssRule(selector) {
if (this.cssAst) {
const rules = this.cssAst.stylesheet.rules;
// return only the first match, Forge CSS authors should make sure class names are unique :)
const matchRules = rules.filter((r) => r.selectors.filter((s) => s === selector).length > 0);
return matchRules.length > 0 ? matchRules[0] : {};
}
return {};
const rules = this.cssAst?.stylesheet?.rules ?? [];
// return only the first match, Forge CSS authors should make sure class names are unique :)
// (comments and @-rules have no selectors)
return rules.find((r) => r.selectors?.includes(selector)) ?? null;
}

/**
Expand All @@ -115,13 +112,7 @@ export class ThemeManagerBase extends GObject.Object {
*/
getCssProperty(selector, propertyName) {
const cssRule = this.getCssRule(selector);

if (cssRule) {
const matchDeclarations = cssRule.declarations.filter((d) => d.property === propertyName);
return matchDeclarations.length > 0 ? matchDeclarations[0] : {};
}

return {};
return cssRule?.declarations?.find((d) => d.property === propertyName) ?? {};
}

/**
Expand All @@ -143,16 +134,86 @@ export class ThemeManagerBase extends GObject.Object {
* Returns the AST for stylesheet.css
*/
_importCss() {
const defaultFile = this.configMgr.defaultStylesheetFile;
let cssFile = this.configMgr.stylesheetFile;
if (!cssFile || !production) {
cssFile = this.configMgr.defaultStylesheetFile;
cssFile = defaultFile;
}

let [success, contents] = cssFile.load_contents(null);
if (success) {
const cssContents = new TextDecoder().decode(contents);
this.cssAst = parse(cssContents);
this.cssAst = cssFile ? this._parseCssFile(cssFile) : null;
if (!defaultFile || cssFile?.equal(defaultFile)) return;

// The user's stylesheet can be empty, from an older Forge or edited by hand: take the rules
// and properties Forge reads that it lacks from the default stylesheet, so that reading them
// doesn't fail and the shell styles everything (#448). If it can't be parsed at all, use the
// default stylesheet. Either way the stylesheet is saved completed, and the user's own file
// is kept as stylesheet.css.bak.
const defaultAst = this._parseCssFile(defaultFile);
if (!defaultAst) return;
let completed = false;
if (!this.cssAst) {
this.cssAst = defaultAst;
completed = true;
} else {
const rules = this.cssAst.stylesheet.rules;
for (const defaultRule of defaultAst.stylesheet.rules) {
if (!defaultRule.selectors) continue;
const rule = this.getCssRule(defaultRule.selectors[0]);
if (!rule) {
rules.push(defaultRule);
completed = true;
continue;
}
rule.declarations ??= [];
for (const declaration of defaultRule.declarations ?? []) {
if (declaration.type !== "declaration") continue;
if (!rule.declarations.some((d) => d.property === declaration.property)) {
rule.declarations.push(declaration);
completed = true;
}
}
}
}
if (completed) this._saveCompletedCss(cssFile);
}

/**
* Writes the completed AST to the user's stylesheet, after copying the file to
* stylesheet.css.bak.
* @param {Gio.File} cssFile
*/
_saveCompletedCss(cssFile) {
try {
const backup = Gio.File.new_for_path(`${cssFile.get_path()}.bak`);
const flags = Gio.FileCopyFlags.OVERWRITE | Gio.FileCopyFlags.TARGET_DEFAULT_PERMS;
cssFile.copy(backup, flags, null, null);
cssFile.replace_contents(
stringify(this.cssAst),
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null
);
console.warn(
`Forge: completed the stylesheet ${cssFile.get_path()} (the previous one is ${backup.get_path()})`
);
} catch (e) {
console.warn(`Forge: can't save the stylesheet ${cssFile.get_path()}: ${e.message}`);
}
}

/**
* Parses a stylesheet file, or returns null (with a warning) if it can't be read or parsed.
* @param {Gio.File} cssFile
*/
_parseCssFile(cssFile) {
try {
const [success, contents] = cssFile.load_contents(null);
if (success) return parse(new TextDecoder().decode(contents));
} catch (e) {
console.warn(`Forge: can't use the stylesheet ${cssFile.get_path()}: ${e.message}`);
}
return null;
}

/**
Expand Down