From d5e24bf6e8513b2768c61cd0f5837ce72c1c1fb2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 11 Jul 2022 12:03:07 +0530 Subject: [PATCH 01/43] Convert color.js to color.mjs --- scripts/postcss/{color.js => color.mjs} | 5 ++--- vite.common-config.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) rename scripts/postcss/{color.js => color.mjs} (91%) diff --git a/scripts/postcss/color.js b/scripts/postcss/color.mjs similarity index 91% rename from scripts/postcss/color.js rename to scripts/postcss/color.mjs index b1ef7073..bd2ea3ea 100644 --- a/scripts/postcss/color.js +++ b/scripts/postcss/color.mjs @@ -13,10 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import {offColor} from 'off-color'; -const offColor = require("off-color").offColor; - -module.exports.derive = function (value, operation, argument, isDark) { +export function derive(value, operation, argument, isDark) { const argumentAsNumber = parseInt(argument); if (isDark) { // For dark themes, invert the operation diff --git a/vite.common-config.js b/vite.common-config.js index 5d65f8e2..7f0e57a8 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -8,8 +8,8 @@ const path = require("path"); const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); -const derive = require("./scripts/postcss/color").derive; const replacer = require("./scripts/postcss/svg-colorizer").buildColorizedSVG; +import {derive} from "./scripts/postcss/color.mjs"; const commonOptions = { logLevel: "warn", From 599e519f229031e4966c446e1be097af4a156883 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 11 Jul 2022 12:17:33 +0530 Subject: [PATCH 02/43] Convert color code to use es6 module --- .../{svg-colorizer.js => svg-colorizer.mjs} | 18 +++++++++--------- vite.common-config.js | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) rename scripts/postcss/{svg-colorizer.js => svg-colorizer.mjs} (79%) diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.mjs similarity index 79% rename from scripts/postcss/svg-colorizer.js rename to scripts/postcss/svg-colorizer.mjs index 06b7b14b..fbe48b55 100644 --- a/scripts/postcss/svg-colorizer.js +++ b/scripts/postcss/svg-colorizer.mjs @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -const fs = require("fs"); -const path = require("path"); -const xxhash = require('xxhashjs'); +import {readFileSync, mkdirSync, writeFileSync} from "fs"; +import {resolve} from "path"; +import {h32} from "xxhashjs"; function createHash(content) { - const hasher = new xxhash.h32(0); + const hasher = new h32(0); hasher.update(content); return hasher.digest(); } @@ -30,8 +30,8 @@ function createHash(content) { * @param {string} primaryColor Primary color for the new svg * @param {string} secondaryColor Secondary color for the new svg */ -module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondaryColor) { - const svgCode = fs.readFileSync(svgLocation, { encoding: "utf8"}); +export function buildColorizedSVG(svgLocation, primaryColor, secondaryColor) { + const svgCode = readFileSync(svgLocation, { encoding: "utf8"}); let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor); coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); if (svgCode === coloredSVGCode) { @@ -39,9 +39,9 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar } const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1]; const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`; - const outputPath = path.resolve(__dirname, "../../.tmp"); + const outputPath = resolve(__dirname, "../../.tmp"); try { - fs.mkdirSync(outputPath); + mkdirSync(outputPath); } catch (e) { if (e.code !== "EEXIST") { @@ -49,6 +49,6 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar } } const outputFile = `${outputPath}/${outputName}`; - fs.writeFileSync(outputFile, coloredSVGCode); + writeFileSync(outputFile, coloredSVGCode); return outputFile; } diff --git a/vite.common-config.js b/vite.common-config.js index 7f0e57a8..aaea47a9 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -8,7 +8,7 @@ const path = require("path"); const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); -const replacer = require("./scripts/postcss/svg-colorizer").buildColorizedSVG; +import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-colorizer.mjs"; import {derive} from "./scripts/postcss/color.mjs"; const commonOptions = { From 8c02541b6971aec5fd15c65fe0048962538c1551 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 28 Jun 2022 12:28:19 +0530 Subject: [PATCH 03/43] WIP - 1 --- src/platform/web/ThemeBuilder.ts | 155 +++++++++++++++++++++++++++++++ src/platform/web/ThemeLoader.ts | 40 +++++--- theme.json | 51 ++++++++++ 3 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 src/platform/web/ThemeBuilder.ts create mode 100644 theme.json diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts new file mode 100644 index 00000000..313a8183 --- /dev/null +++ b/src/platform/web/ThemeBuilder.ts @@ -0,0 +1,155 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import type {ThemeInformation} from "./ThemeLoader"; +import {ColorSchemePreference} from "./ThemeLoader"; +import {offColor} from 'off-color'; + +function derive(value, operation, argument, isDark) { + const argumentAsNumber = parseInt(argument); + if (isDark) { + // For dark themes, invert the operation + if (operation === 'darker') { + operation = "lighter"; + } + else if (operation === 'lighter') { + operation = "darker"; + } + } + switch (operation) { + case "darker": { + const newColorString = offColor(value).darken(argumentAsNumber / 100).hex(); + return newColorString; + } + case "lighter": { + const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex(); + return newColorString; + } + } +} + +export class ThemeBuilder { + // todo: replace any with manifest type when PR is merged + private _idToManifest: Map; + private _themeMapping: Record = {}; + private _themeToVariables: Record = {}; + private _preferredColorScheme?: ColorSchemePreference; + + constructor(manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { + this._idToManifest = manifestMap; + this._preferredColorScheme = preferredColorScheme; + } + + populateDerivedTheme(manifest) { + const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends); + const runtimeCssLocation = baseManifest.source?.["runtime-asset"]; + const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href; + const derivedVariables = baseManifest.source?.["derived-variables"]; + const themeName = manifest.name; + let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; + for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { + const themeId = `${manifest.id}-${variant}`; + const { name: variantName, default: isDefault, dark, variables } = variantDetails; + const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark); + console.log("resolved", resolvedVariables); + Object.assign(variables, resolvedVariables); + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + /** + * This is a default variant! + * We'll add these to the themeMapping (separately) keyed with just the + * theme-name (i.e "Element" instead of "Element Dark"). + * We need to be able to distinguish them from other variants! + * + * This allows us to render radio-buttons with "dark" and + * "light" options. + */ + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + defaultVariant.variantName = variantName; + defaultVariant.id = themeId; + defaultVariant.cssLocation = cssLocation; + defaultVariant.variables = variables; + continue; + } + // Non-default variants are keyed in themeMapping with "theme_name variant_name" + // eg: "Element Dark" + this._themeMapping[themeDisplayName] = { + cssLocation, + id: themeId, + variables: variables, + }; + } + if (defaultDarkVariant.id && defaultLightVariant.id) { + /** + * As mentioned above, if there's both a default dark and a default light variant, + * add them to themeMapping separately. + */ + const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; + } + else { + /** + * If only one default variant is found (i.e only dark default or light default but not both), + * treat it like any other variant. + */ + const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; + } + } + + get themeMapping() { + return this._themeMapping; + } + + injectCSSVariables(variables: Record) { + const root = document.documentElement; + for (const [variable, value] of Object.entries(variables)) { + root.style.setProperty(`--${variable}`, value); + } + } + + removeCSSVariables(variables: string[]) { + const root = document.documentElement; + for (const variable of variables) { + root.style.removeProperty(`--${variable}`); + } + } + + deriveVariables(variables: Record, derivedVariables: string[], isDark: boolean) { + const aliases: any = {}; + const resolvedVariables: any = {}; + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + for (const variable of derivedVariables) { + // If this is an alias, store it for processing later + const [alias, value] = variable.split("="); + if (value) { + aliases[alias] = value; + continue; + } + // Resolve derived variables + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = variables[baseVariable]; + const resolvedValue = derive(value, operation, argument, isDark); + resolvedVariables[variable] = resolvedValue; + } + } + for (const [alias, variable] of Object.entries(aliases) as any) { + resolvedVariables[alias] = variables[variable] ?? resolvedVariables[variable]; + } + return resolvedVariables; + } +} diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 89430663..9806dd10 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -14,33 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type {ILogItem} from "../../logging/types.js"; +import type {ILogItem} from "../../logging/types"; import type {Platform} from "./Platform.js"; +import {ThemeBuilder} from "./ThemeBuilder"; type NormalVariant = { id: string; cssLocation: string; + variables?: any; }; type DefaultVariant = { - dark: { - id: string; - cssLocation: string; + dark: NormalVariant & { variantName: string; }; - light: { - id: string; - cssLocation: string; + light: NormalVariant & { variantName: string; }; - default: { - id: string; - cssLocation: string; + default: NormalVariant & { variantName: string; }; } -type ThemeInformation = NormalVariant | DefaultVariant; +export type ThemeInformation = NormalVariant | DefaultVariant; export enum ColorSchemePreference { Dark, @@ -50,18 +46,31 @@ export enum ColorSchemePreference { export class ThemeLoader { private _platform: Platform; private _themeMapping: Record; + private _themeBuilder: ThemeBuilder; constructor(platform: Platform) { this._platform = platform; } async init(manifestLocations: string[], log?: ILogItem): Promise { + const idToManifest = new Map(); await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { this._themeMapping = {}; const results = await Promise.all( manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); - results.forEach(({ body }, i) => this._populateThemeMap(body, manifestLocations[i], log)); + results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); + this._themeBuilder = new ThemeBuilder(idToManifest, this.preferredColorScheme); + results.forEach(({ body }, i) => { + if (body.extends) { + this._themeBuilder.populateDerivedTheme(body); + } + else { + this._populateThemeMap(body, manifestLocations[i], log); + } + }); + Object.assign(this._themeMapping, this._themeBuilder.themeMapping); + console.log("derived theme mapping", this._themeBuilder.themeMapping); }); } @@ -144,18 +153,23 @@ export class ThemeLoader { setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) { this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => { - let cssLocation: string; + let cssLocation: string, variables: Record; let themeDetails = this._themeMapping[themeName]; if ("id" in themeDetails) { cssLocation = themeDetails.cssLocation; + variables = themeDetails.variables; } else { if (!themeVariant) { throw new Error("themeVariant is undefined!"); } cssLocation = themeDetails[themeVariant].cssLocation; + variables = themeDetails[themeVariant].variables; } this._platform.replaceStylesheet(cssLocation); + if (variables) { + this._themeBuilder.injectCSSVariables(variables); + } this._platform.settingsStorage.setString("theme-name", themeName); if (themeVariant) { this._platform.settingsStorage.setString("theme-variant", themeVariant); diff --git a/theme.json b/theme.json new file mode 100644 index 00000000..aee95368 --- /dev/null +++ b/theme.json @@ -0,0 +1,51 @@ +{ + "name": "Customer", + "extends": "element", + "id": "customer", + "values": { + "variants": { + "dark": { + "dark": true, + "default": true, + "name": "Dark", + "variables": { + "background-color-primary": "#21262b", + "background-color-secondary": "#2D3239", + "text-color": "#fff", + "accent-color": "#F03F5B", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#61708b", + "link-color": "#238cf5" + } + }, + "light": { + "default": true, + "name": "Dark", + "variables": { + "background-color-primary": "#21262b", + "background-color-secondary": "#2D3239", + "text-color": "#fff", + "accent-color": "#F03F5B", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#61708b", + "link-color": "#238cf5" + } + }, + "red": { + "name": "Red", + "variables": { + "background-color-primary": "#1F1F1F", + "background-color-secondary": "#2B243E", + "text-color": "#fff", + "accent-color": "#F23041", + "error-color": "#FF4B55", + "fixed-white": "#fff", + "room-badge": "#F23030", + "link-color": "#238cf5" + } + } + } + } +} From bf87ed7eae420ef905ceb47a48b0377a015ae8d9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 16:41:06 +0530 Subject: [PATCH 04/43] Do not add variables to root for runtime theme --- scripts/postcss/css-url-to-variables.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/postcss/css-url-to-variables.js b/scripts/postcss/css-url-to-variables.js index f9588434..9988a10e 100644 --- a/scripts/postcss/css-url-to-variables.js +++ b/scripts/postcss/css-url-to-variables.js @@ -81,7 +81,8 @@ module.exports = (opts = {}) => { const urlVariables = new Map(); const counter = createCounter(); root.walkDecls(decl => findAndReplaceUrl(decl, urlVariables, counter)); - if (urlVariables.size) { + const cssFileLocation = root.source.input.from; + if (urlVariables.size && !cssFileLocation.includes("type=runtime")) { addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables); } if (opts.compiledVariables){ From 43e8cc9e522413406d6431d36acd407e715ef64c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 16:42:02 +0530 Subject: [PATCH 05/43] Add svgo for optimizing svgs as dev dependency --- package.json | 1 + yarn.lock | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 82588f10..dd1a1554 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "postcss-flexbugs-fixes": "^5.0.2", "postcss-value-parser": "^4.2.0", "regenerator-runtime": "^0.13.7", + "svgo": "^2.8.0", "text-encoding": "^0.7.0", "typescript": "^4.3.5", "vite": "^2.9.8", diff --git a/yarn.lock b/yarn.lock index 28efc3bc..1543f8d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,6 +77,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@types/json-schema@^7.0.7": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -347,6 +352,11 @@ commander@^6.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -382,11 +392,26 @@ css-select@^4.1.3: domutils "^2.6.0" nth-check "^2.0.0" +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + css-what@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg== +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + cuint@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" @@ -1197,6 +1222,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + mdn-polyfills@^5.20.0: version "5.20.0" resolved "https://registry.yarnpkg.com/mdn-polyfills/-/mdn-polyfills-5.20.0.tgz#ca8247edf20a4f60dec6804372229812b348260b" @@ -1500,7 +1530,7 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map@~0.6.1: +source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -1510,6 +1540,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -1550,6 +1585,19 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svgo@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + table@^6.0.9: version "6.7.1" resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" From c8738045435e0335489b401c1b9b397ad564d7b3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 16:42:56 +0530 Subject: [PATCH 06/43] produce asset hashed icons --- .../rollup-plugin-build-themes.js | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 6b8fdae0..b523778b 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ const path = require('path').posix; +const {optimize} = require('svgo'); async function readCSSSource(location) { const fs = require("fs").promises; - const path = require("path"); const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`); const data = await fs.readFile(resolvedLocation); return data; @@ -43,6 +43,28 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) { } } +/** + * Returns an object where keys are the svg file names and the values + * are the svg code (optimized) + * @param {*} icons Object where keys are css variable names and values are locations of the svg + * @param {*} manifestLocation Location of manifest used for resolving path + */ +async function generateIconSourceMap(icons, manifestLocation) { + const sources = {}; + const fs = require("fs").promises; + for (const icon of Object.values(icons)) { + const [location] = icon.split("?"); + const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); + const iconData = await fs.readFile(resolvedLocation); + const svgString = iconData.toString(); + const result = optimize(svgString); + const optimizedSvgString = result.data; + const fileName = path.basename(resolvedLocation); + sources[fileName] = optimizedSvgString; + } + return sources; +} + /** * Returns a mapping from location (of manifest file) to an array containing all the chunks (of css files) generated from that location. * To understand what chunk means in this context, see https://rollupjs.org/guide/en/#generatebundle. @@ -283,7 +305,7 @@ module.exports = function buildThemes(options) { ]; }, - generateBundle(_, bundle) { + async generateBundle(_, bundle) { const assetMap = getMappingFromFileNameToAssetInfo(bundle); const chunkMap = getMappingFromLocationToChunkArray(bundle); const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle); @@ -304,13 +326,28 @@ module.exports = function buildThemes(options) { const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot); builtAssets[`${name}-${variant}`] = locationRelativeToManifest; } + // Emit the base svg icons as asset + const nameToAssetHashedLocation = []; + const nameToSource = await generateIconSourceMap(icon, location); + for (const [name, source] of Object.entries(nameToSource)) { + const ref = this.emitFile({ type: "asset", name, source }); + const assetHashedName = this.getFileName(ref); + nameToAssetHashedLocation[name] = assetHashedName; + } + for (const [variable, location] of Object.entries(icon)) { + const [locationWithoutQueryParameters, queryParameters] = location.split("?"); + const name = path.basename(locationWithoutQueryParameters); + const locationRelativeToBuildRoot = nameToAssetHashedLocation[name]; + const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot); + icon[variable] = `${locationRelativeToManifest}?${queryParameters}`; + } const runtimeThemeChunk = runtimeThemeChunkMap.get(location); const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName); manifest.source = { "built-assets": builtAssets, "runtime-asset": runtimeAssetLocation, "derived-variables": derivedVariables, - "icon": icon + "icon": icon, }; const name = `theme-${themeKey}.json`; manifestLocations.push(`${manifestLocation}/${name}`); From 2947f9f6ff84834c4190616356d40f40460fff73 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 4 Jul 2022 17:14:10 +0530 Subject: [PATCH 07/43] Remove console.log --- src/platform/web/ThemeBuilder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index 313a8183..a192424c 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -63,7 +63,6 @@ export class ThemeBuilder { const themeId = `${manifest.id}-${variant}`; const { name: variantName, default: isDefault, dark, variables } = variantDetails; const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark); - console.log("resolved", resolvedVariables); Object.assign(variables, resolvedVariables); const themeDisplayName = `${themeName} ${variantName}`; if (isDefault) { From 161e29b36e6d3864561ea08aa5ee4a30208875e8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 11 Jul 2022 16:58:22 +0530 Subject: [PATCH 08/43] Use existing code --- src/platform/web/ThemeBuilder.ts | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index a192424c..b8e7274b 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -15,30 +15,7 @@ limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; import {ColorSchemePreference} from "./ThemeLoader"; -import {offColor} from 'off-color'; - -function derive(value, operation, argument, isDark) { - const argumentAsNumber = parseInt(argument); - if (isDark) { - // For dark themes, invert the operation - if (operation === 'darker') { - operation = "lighter"; - } - else if (operation === 'lighter') { - operation = "darker"; - } - } - switch (operation) { - case "darker": { - const newColorString = offColor(value).darken(argumentAsNumber / 100).hex(); - return newColorString; - } - case "lighter": { - const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex(); - return newColorString; - } - } -} +import {derive} from "../../../scripts/postcss/color.mjs"; export class ThemeBuilder { // todo: replace any with manifest type when PR is merged From 1ef382f3a95e85cf03829fc8d674e82a63f45d59 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:55:06 +0530 Subject: [PATCH 09/43] Add gruvbox color scheme --- theme.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/theme.json b/theme.json index aee95368..f5fbf3ed 100644 --- a/theme.json +++ b/theme.json @@ -36,14 +36,14 @@ "red": { "name": "Red", "variables": { - "background-color-primary": "#1F1F1F", - "background-color-secondary": "#2B243E", - "text-color": "#fff", - "accent-color": "#F23041", - "error-color": "#FF4B55", + "background-color-primary": "#282828", + "background-color-secondary": "#3c3836", + "text-color": "#fbf1c7", + "accent-color": "#8ec07c", + "error-color": "#fb4934", "fixed-white": "#fff", - "room-badge": "#F23030", - "link-color": "#238cf5" + "room-badge": "#cc241d", + "link-color": "#fe8019" } } } From 2f3db89e0a43db1e1d0cd7f37deb1506539eaf2d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:55:21 +0530 Subject: [PATCH 10/43] Let ts know that we can use replaceAll() --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index f46cc7eb..8f591a6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "noEmit": true, "target": "ES2020", "module": "ES2020", + "lib": ["ES2021", "DOM"], "moduleResolution": "node", "esModuleInterop": true }, From c5f4a75d4b7b3bee0218cfe7b6527fb66d812fb2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:57:00 +0530 Subject: [PATCH 11/43] Split code so that it can be reused --- .../{svg-colorizer.mjs => svg-builder.mjs} | 7 ++---- scripts/postcss/svg-colorizer.js | 24 +++++++++++++++++++ vite.common-config.js | 2 +- 3 files changed, 27 insertions(+), 6 deletions(-) rename scripts/postcss/{svg-colorizer.mjs => svg-builder.mjs} (82%) create mode 100644 scripts/postcss/svg-colorizer.js diff --git a/scripts/postcss/svg-colorizer.mjs b/scripts/postcss/svg-builder.mjs similarity index 82% rename from scripts/postcss/svg-colorizer.mjs rename to scripts/postcss/svg-builder.mjs index fbe48b55..1bbc4010 100644 --- a/scripts/postcss/svg-colorizer.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,6 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; +import {getColoredSvgString} from "./svg-colorizer.js"; function createHash(content) { const hasher = new h32(0); @@ -32,11 +33,7 @@ function createHash(content) { */ export function buildColorizedSVG(svgLocation, primaryColor, secondaryColor) { const svgCode = readFileSync(svgLocation, { encoding: "utf8"}); - let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor); - coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); - if (svgCode === coloredSVGCode) { - throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive)."); - } + const coloredSVGCode = getColoredSvgString(svgCode, primaryColor, secondaryColor); const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1]; const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`; const outputPath = resolve(__dirname, "../../.tmp"); diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.js new file mode 100644 index 00000000..cb291726 --- /dev/null +++ b/scripts/postcss/svg-colorizer.js @@ -0,0 +1,24 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export function getColoredSvgString(svgString, primaryColor, secondaryColor) { + let coloredSVGCode = svgString.replaceAll("#ff00ff", primaryColor); + coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor); + if (svgString === coloredSVGCode) { + throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive)."); + } + return coloredSVGCode; +} diff --git a/vite.common-config.js b/vite.common-config.js index aaea47a9..bcf17115 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -8,7 +8,7 @@ const path = require("path"); const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); -import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-colorizer.mjs"; +import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs"; import {derive} from "./scripts/postcss/color.mjs"; const commonOptions = { From 5ba74b1d7588582081f934f3de4e45d3045bff8e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:58:07 +0530 Subject: [PATCH 12/43] Use script to copy over runtime theme after build --- scripts/test-theme.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 scripts/test-theme.sh diff --git a/scripts/test-theme.sh b/scripts/test-theme.sh new file mode 100755 index 00000000..9f94d3c3 --- /dev/null +++ b/scripts/test-theme.sh @@ -0,0 +1,5 @@ +#!/bin/zsh +cp theme.json target/assets/theme-customer.json +cat target/config.json | jq '.themeManifests += ["assets/theme-customer.json"]' | cat > target/config.temp.json +rm target/config.json +mv target/config.temp.json target/config.json From f7b302d34fb4f8e82372806e49804a581bf91f95 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 14:59:50 +0530 Subject: [PATCH 13/43] Don't optimzie colors --- scripts/build-plugins/rollup-plugin-build-themes.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index b523778b..72053a1f 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -57,7 +57,16 @@ async function generateIconSourceMap(icons, manifestLocation) { const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); const iconData = await fs.readFile(resolvedLocation); const svgString = iconData.toString(); - const result = optimize(svgString); + const result = optimize(svgString, { + plugins: [ + { + name: "preset-default", + params: { + overrides: { convertColors: false, }, + }, + }, + ], + }); const optimizedSvgString = result.data; const fileName = path.basename(resolvedLocation); sources[fileName] = optimizedSvgString; From d731eab51c10e9e7499c890f35a86a9099d4945f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 15:04:54 +0530 Subject: [PATCH 14/43] Support fetching text --- src/platform/web/dom/request/fetch.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/web/dom/request/fetch.js b/src/platform/web/dom/request/fetch.js index 497ad553..eb4caab6 100644 --- a/src/platform/web/dom/request/fetch.js +++ b/src/platform/web/dom/request/fetch.js @@ -115,6 +115,9 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) { } else if (format === "buffer") { body = await response.arrayBuffer(); } + else if (format === "text") { + body = await response.text(); + } } catch (err) { // some error pages return html instead of json, ignore error if (!(err.name === "SyntaxError" && status >= 400)) { From ac7be0c7a1f66c549e634f082f7a858204d3dbaf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 15:05:50 +0530 Subject: [PATCH 15/43] WIP --- src/platform/web/DerivedVariables.ts | 54 +++++++++++++++ src/platform/web/IconColorizer.ts | 79 +++++++++++++++++++++ src/platform/web/ThemeBuilder.ts | 100 +++++++++------------------ src/platform/web/ThemeLoader.ts | 13 ++-- 4 files changed, 174 insertions(+), 72 deletions(-) create mode 100644 src/platform/web/DerivedVariables.ts create mode 100644 src/platform/web/IconColorizer.ts diff --git a/src/platform/web/DerivedVariables.ts b/src/platform/web/DerivedVariables.ts new file mode 100644 index 00000000..4e3a8b46 --- /dev/null +++ b/src/platform/web/DerivedVariables.ts @@ -0,0 +1,54 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import {derive} from "../../../scripts/postcss/color.mjs"; + +export class DerivedVariables { + private _baseVariables: Record; + private _variablesToDerive: string[] + private _isDark: boolean + + constructor(baseVariables: Record, variablesToDerive: string[], isDark: boolean) { + this._baseVariables = baseVariables; + this._variablesToDerive = variablesToDerive; + this._isDark = isDark; + } + + toVariables(): Record { + const aliases: any = {}; + const resolvedVariables: any = {}; + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + for (const variable of this._variablesToDerive) { + // If this is an alias, store it for processing later + const [alias, value] = variable.split("="); + if (value) { + aliases[alias] = value; + continue; + } + // Resolve derived variables + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = this._baseVariables[baseVariable]; + const resolvedValue = derive(value, operation, argument, this._isDark); + resolvedVariables[variable] = resolvedValue; + } + } + for (const [alias, variable] of Object.entries(aliases) as any) { + resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable]; + } + return resolvedVariables; + } +} diff --git a/src/platform/web/IconColorizer.ts b/src/platform/web/IconColorizer.ts new file mode 100644 index 00000000..81644603 --- /dev/null +++ b/src/platform/web/IconColorizer.ts @@ -0,0 +1,79 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import type {Platform} from "./Platform.js"; +import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.js"; + +type ParsedStructure = { + [variableName: string]: { + svg: Promise<{ status: number; body: string }>; + primary: string | null; + secondary: string | null; + }; +}; + +export class IconColorizer { + private _iconVariables: Record; + private _resolvedVariables: Record; + private _manifestLocation: string; + private _platform: Platform; + + constructor(platform: Platform, iconVariables: Record, resolvedVariables: Record, manifestLocation: string) { + this._platform = platform; + this._iconVariables = iconVariables; + this._resolvedVariables = resolvedVariables; + this._manifestLocation = manifestLocation; + } + + async toVariables(): Promise> { + const { parsedStructure, promises } = await this._fetchAndParseIcons(); + await Promise.all(promises); + return this._produceColoredIconVariables(parsedStructure); + } + + private async _fetchAndParseIcons(): Promise<{ parsedStructure: ParsedStructure, promises: any[] }> { + const promises: any[] = []; + const parsedStructure: ParsedStructure = {}; + for (const [variable, url] of Object.entries(this._iconVariables)) { + const urlObject = new URL(`https://${url}`); + const pathWithoutQueryParams = urlObject.hostname; + const relativePath = new URL(pathWithoutQueryParams, new URL(this._manifestLocation, window.location.origin)); + const responsePromise = this._platform.request(relativePath, { method: "GET", format: "text", cache: true, }).response() + promises.push(responsePromise); + const searchParams = urlObject.searchParams; + parsedStructure[variable] = { + svg: responsePromise, + primary: searchParams.get("primary"), + secondary: searchParams.get("secondary") + }; + } + return { parsedStructure, promises }; + } + + private async _produceColoredIconVariables(parsedStructure: ParsedStructure): Promise> { + let coloredVariables: Record = {}; + for (const [variable, { svg, primary, secondary }] of Object.entries(parsedStructure)) { + const { body: svgCode } = await svg; + if (!primary) { + throw new Error(`Primary color variable ${primary} not in list of variables!`); + } + const primaryColor = this._resolvedVariables[primary], secondaryColor = this._resolvedVariables[secondary!]; + const coloredSvgCode = getColoredSvgString(svgCode, primaryColor, secondaryColor); + const dataURI = `url('data:image/svg+xml;utf8,${encodeURIComponent(coloredSvgCode)}')`; + coloredVariables[variable] = dataURI; + } + return coloredVariables; + } +} diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index b8e7274b..f05a24b1 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -14,72 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; +import type {Platform} from "./Platform.js"; import {ColorSchemePreference} from "./ThemeLoader"; -import {derive} from "../../../scripts/postcss/color.mjs"; +import {IconColorizer} from "./IconColorizer"; +import {DerivedVariables} from "./DerivedVariables"; export class ThemeBuilder { // todo: replace any with manifest type when PR is merged private _idToManifest: Map; private _themeMapping: Record = {}; - private _themeToVariables: Record = {}; private _preferredColorScheme?: ColorSchemePreference; + private _platform: Platform; + private _injectedVariables?: Record; - constructor(manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { + constructor(platform: Platform, manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { this._idToManifest = manifestMap; this._preferredColorScheme = preferredColorScheme; + this._platform = platform; } - populateDerivedTheme(manifest) { + async populateDerivedTheme(manifest) { const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends); const runtimeCssLocation = baseManifest.source?.["runtime-asset"]; const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href; const derivedVariables = baseManifest.source?.["derived-variables"]; + const icons = baseManifest.source?.["icon"]; const themeName = manifest.name; let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { - const themeId = `${manifest.id}-${variant}`; - const { name: variantName, default: isDefault, dark, variables } = variantDetails; - const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark); - Object.assign(variables, resolvedVariables); - const themeDisplayName = `${themeName} ${variantName}`; - if (isDefault) { - /** - * This is a default variant! - * We'll add these to the themeMapping (separately) keyed with just the - * theme-name (i.e "Element" instead of "Element Dark"). - * We need to be able to distinguish them from other variants! - * - * This allows us to render radio-buttons with "dark" and - * "light" options. - */ - const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; - defaultVariant.variantName = variantName; - defaultVariant.id = themeId; - defaultVariant.cssLocation = cssLocation; - defaultVariant.variables = variables; + try { + const themeId = `${manifest.id}-${variant}`; + const { name: variantName, default: isDefault, dark, variables } = variantDetails; + const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); + Object.assign(variables, resolvedVariables); + const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); + Object.assign(variables, resolvedVariables, iconVariables); + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables }); + continue; + } + this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, }; + } + catch (e) { + console.error(e); continue; } - // Non-default variants are keyed in themeMapping with "theme_name variant_name" - // eg: "Element Dark" - this._themeMapping[themeDisplayName] = { - cssLocation, - id: themeId, - variables: variables, - }; } if (defaultDarkVariant.id && defaultLightVariant.id) { - /** - * As mentioned above, if there's both a default dark and a default light variant, - * add them to themeMapping separately. - */ const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; } else { - /** - * If only one default variant is found (i.e only dark default or light default but not both), - * treat it like any other variant. - */ const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; } @@ -94,38 +81,17 @@ export class ThemeBuilder { for (const [variable, value] of Object.entries(variables)) { root.style.setProperty(`--${variable}`, value); } + this._injectedVariables = variables; } - removeCSSVariables(variables: string[]) { + removePreviousCSSVariables() { + if (!this._injectedVariables) { + return; + } const root = document.documentElement; - for (const variable of variables) { + for (const variable of Object.keys(this._injectedVariables)) { root.style.removeProperty(`--${variable}`); } - } - - deriveVariables(variables: Record, derivedVariables: string[], isDark: boolean) { - const aliases: any = {}; - const resolvedVariables: any = {}; - const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; - for (const variable of derivedVariables) { - // If this is an alias, store it for processing later - const [alias, value] = variable.split("="); - if (value) { - aliases[alias] = value; - continue; - } - // Resolve derived variables - const matches = variable.match(RE_VARIABLE_VALUE); - if (matches) { - const [, baseVariable, operation, argument] = matches; - const value = variables[baseVariable]; - const resolvedValue = derive(value, operation, argument, isDark); - resolvedVariables[variable] = resolvedValue; - } - } - for (const [alias, variable] of Object.entries(aliases) as any) { - resolvedVariables[alias] = variables[variable] ?? resolvedVariables[variable]; - } - return resolvedVariables; + this._injectedVariables = undefined; } } diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 9806dd10..b1e13161 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -60,17 +60,17 @@ export class ThemeLoader { manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); - this._themeBuilder = new ThemeBuilder(idToManifest, this.preferredColorScheme); - results.forEach(({ body }, i) => { + this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); + for (let i = 0; i < results.length; ++i) { + const { body } = results[i]; if (body.extends) { - this._themeBuilder.populateDerivedTheme(body); + await this._themeBuilder.populateDerivedTheme(body); } else { this._populateThemeMap(body, manifestLocations[i], log); } - }); + } Object.assign(this._themeMapping, this._themeBuilder.themeMapping); - console.log("derived theme mapping", this._themeBuilder.themeMapping); }); } @@ -170,6 +170,9 @@ export class ThemeLoader { if (variables) { this._themeBuilder.injectCSSVariables(variables); } + else { + this._themeBuilder.removePreviousCSSVariables(); + } this._platform.settingsStorage.setString("theme-name", themeName); if (themeVariant) { this._platform.settingsStorage.setString("theme-variant", themeVariant); From a8cab98666002d0b8a2b80ef49842edd618a30d7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 16:07:52 +0530 Subject: [PATCH 16/43] Add mroe missing types --- src/platform/types/theme.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index 9a984277..f57432eb 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -42,6 +42,12 @@ export type ThemeManifest = Partial<{ "runtime-asset": string; // Array of derived-variables "derived-variables": Array; + /** + * Mapping from icon variable to location of icon in build output with query parameters + * indicating how it should be colored for this particular theme. + * eg: "icon-url-1": "element-logo.86bc8565.svg?primary=accent-color" + */ + icon: Record; }; values: { /** @@ -60,6 +66,8 @@ type Variant = Partial<{ default: boolean; // A user-facing string that is the name for this variant. name: string; + // A boolean indicating whether this is a dark theme or not + dark: boolean; /** * Mapping from css variable to its value. * eg: {"background-color-primary": "#21262b", ...} From f4404578759b8e7bb197c9a9f6e5dade3cc580b5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 16:08:02 +0530 Subject: [PATCH 17/43] Use ThemeManifest type where possible --- src/platform/web/ThemeBuilder.ts | 28 +++++++++++++++++++++------- src/platform/web/ThemeLoader.ts | 12 ++++++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index f05a24b1..b093deb3 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -18,10 +18,10 @@ import type {Platform} from "./Platform.js"; import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; +import {ThemeManifest} from "../types/theme"; export class ThemeBuilder { - // todo: replace any with manifest type when PR is merged - private _idToManifest: Map; + private _idToManifest: Map; private _themeMapping: Record = {}; private _preferredColorScheme?: ColorSchemePreference; private _platform: Platform; @@ -34,11 +34,8 @@ export class ThemeBuilder { } async populateDerivedTheme(manifest) { - const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends); - const runtimeCssLocation = baseManifest.source?.["runtime-asset"]; - const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href; - const derivedVariables = baseManifest.source?.["derived-variables"]; - const icons = baseManifest.source?.["icon"]; + const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends)!; + const { cssLocation, derivedVariables, icons } = this._getsourceData(baseManifest, location); const themeName = manifest.name; let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { @@ -72,6 +69,23 @@ export class ThemeBuilder { } } + private _getsourceData(manifest: ThemeManifest, location: string) { + const runtimeCSSLocation = manifest.source?.["runtime-asset"]; + if (!runtimeCSSLocation) { + throw new Error(`Run-time asset not found in source section for theme at ${location}`); + } + const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href; + const derivedVariables = manifest.source?.["derived-variables"]; + if (!derivedVariables) { + throw new Error(`Derived variables not found in source section for theme at ${location}`); + } + const icons = manifest.source?.["icon"]; + if (!icons) { + throw new Error(`Icon mapping not found in source section for theme at ${location}`); + } + return { cssLocation, derivedVariables, icons }; + } + get themeMapping() { return this._themeMapping; } diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index b1e13161..7deeee1b 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -15,6 +15,7 @@ limitations under the License. */ import type {ILogItem} from "../../logging/types"; +import { ThemeManifest } from "../types/theme"; import type {Platform} from "./Platform.js"; import {ThemeBuilder} from "./ThemeBuilder"; @@ -74,7 +75,7 @@ export class ThemeLoader { }); } - private _populateThemeMap(manifest, manifestLocation: string, log: ILogItem) { + private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { log.wrap("populateThemeMap", (l) => { /* After build has finished, the source section of each theme manifest @@ -83,6 +84,9 @@ export class ThemeLoader { */ const builtAssets: Record = manifest.source?.["built-assets"]; const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest at ${manifestLocation}`); + } let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (let [themeId, cssLocation] of Object.entries(builtAssets)) { try { @@ -96,7 +100,11 @@ export class ThemeLoader { continue; } const variant = themeId.match(/.+-(.+)/)?.[1]; - const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!]; + const variantDetails = manifest.values?.variants[variant!]; + if (!variantDetails) { + throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`); + } + const { name: variantName, default: isDefault, dark } = variantDetails; const themeDisplayName = `${themeName} ${variantName}`; if (isDefault) { /** From f15e23762ad2567f19f9ebda2acc2d65e1cdf5e3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:34:56 +0530 Subject: [PATCH 18/43] Add more missing keys to type --- src/platform/types/theme.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/platform/types/theme.ts b/src/platform/types/theme.ts index f57432eb..9dd969de 100644 --- a/src/platform/types/theme.ts +++ b/src/platform/types/theme.ts @@ -22,6 +22,13 @@ export type ThemeManifest = Partial<{ version: number; // A user-facing string that is the name for this theme-collection. name: string; + // An identifier for this theme + id: string; + /** + * Id of the theme that this theme derives from. + * Only present for derived/runtime themes. + */ + extends: string; /** * This is added to the manifest during the build process and includes data * that is needed to load themes at runtime. From 80fb9536885b6c8abb00c959c63507cf8d2c4a55 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:35:29 +0530 Subject: [PATCH 19/43] Don't fail on erros; expect the code to throw! --- src/platform/web/ThemeLoader.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 7deeee1b..ec62b2c2 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -64,11 +64,16 @@ export class ThemeLoader { this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); for (let i = 0; i < results.length; ++i) { const { body } = results[i]; - if (body.extends) { - await this._themeBuilder.populateDerivedTheme(body); + try { + if (body.extends) { + await this._themeBuilder.populateDerivedTheme(body); + } + else { + this._populateThemeMap(body, manifestLocations[i], log); + } } - else { - this._populateThemeMap(body, manifestLocations[i], log); + catch(e) { + console.error(e); } } Object.assign(this._themeMapping, this._themeBuilder.themeMapping); From 043cc9f12c2a95d97926bc612c21900a2ab31260 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:36:36 +0530 Subject: [PATCH 20/43] Use ThemeManifest type --- src/platform/web/ThemeBuilder.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index b093deb3..b6f75be4 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -33,12 +33,15 @@ export class ThemeBuilder { this._platform = platform; } - async populateDerivedTheme(manifest) { - const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends)!; + async populateDerivedTheme(manifest: ThemeManifest) { + const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends!)!; const { cssLocation, derivedVariables, icons } = this._getsourceData(baseManifest, location); const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest!`); + } let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; - for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { + for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) { try { const themeId = `${manifest.id}-${variant}`; const { name: variantName, default: isDefault, dark, variables } = variantDetails; From da0a918c180bb7aebbebb4efa568d713bd456883 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 17:44:21 +0530 Subject: [PATCH 21/43] This code should only run once --- src/platform/web/ThemeLoader.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index ec62b2c2..069be84d 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -76,6 +76,17 @@ export class ThemeLoader { console.error(e); } } + //Add the default-theme as an additional option to the mapping + const defaultThemeId = this.getDefaultTheme(); + if (defaultThemeId) { + const themeDetails = this._findThemeDetailsFromId(defaultThemeId); + if (themeDetails) { + this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation }; + } + } + log.log({ l: "Default Theme", theme: defaultThemeId}); + log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); + log.log({ l: "Result", themeMapping: this._themeMapping }); Object.assign(this._themeMapping, this._themeBuilder.themeMapping); }); } @@ -150,17 +161,6 @@ export class ThemeLoader { const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; } - //Add the default-theme as an additional option to the mapping - const defaultThemeId = this.getDefaultTheme(); - if (defaultThemeId) { - const themeDetails = this._findThemeDetailsFromId(defaultThemeId); - if (themeDetails) { - this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation }; - } - } - l.log({ l: "Default Theme", theme: defaultThemeId}); - l.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); - l.log({ l: "Result", themeMapping: this._themeMapping }); }); } From ce5db477085ae05c950fea1c6fb26a1fd0a46a47 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 19:42:26 +0530 Subject: [PATCH 22/43] Support using derived theme as default theme --- src/platform/web/ThemeLoader.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 069be84d..c04d8e8c 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -25,16 +25,14 @@ type NormalVariant = { variables?: any; }; +type Variant = NormalVariant & { + variantName: string; +}; + type DefaultVariant = { - dark: NormalVariant & { - variantName: string; - }; - light: NormalVariant & { - variantName: string; - }; - default: NormalVariant & { - variantName: string; - }; + dark: Variant; + light: Variant; + default: Variant; } export type ThemeInformation = NormalVariant | DefaultVariant; @@ -76,18 +74,22 @@ export class ThemeLoader { console.error(e); } } + Object.assign(this._themeMapping, this._themeBuilder.themeMapping); //Add the default-theme as an additional option to the mapping const defaultThemeId = this.getDefaultTheme(); if (defaultThemeId) { const themeDetails = this._findThemeDetailsFromId(defaultThemeId); if (themeDetails) { - this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation }; + this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! }; + const variables = themeDetails.themeData.variables; + if (variables) { + this._themeMapping["Default"].variables = variables; + } } } log.log({ l: "Default Theme", theme: defaultThemeId}); log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); log.log({ l: "Result", themeMapping: this._themeMapping }); - Object.assign(this._themeMapping, this._themeBuilder.themeMapping); }); } @@ -222,16 +224,16 @@ export class ThemeLoader { } } - private _findThemeDetailsFromId(themeId: string): {themeName: string, cssLocation: string, variant?: string} | undefined { + private _findThemeDetailsFromId(themeId: string): {themeName: string, themeData: Partial} | undefined { for (const [themeName, themeData] of Object.entries(this._themeMapping)) { if ("id" in themeData && themeData.id === themeId) { - return { themeName, cssLocation: themeData.cssLocation }; + return { themeName, themeData }; } else if ("light" in themeData && themeData.light?.id === themeId) { - return { themeName, cssLocation: themeData.light.cssLocation, variant: "light" }; + return { themeName, themeData: themeData.light }; } else if ("dark" in themeData && themeData.dark?.id === themeId) { - return { themeName, cssLocation: themeData.dark.cssLocation, variant: "dark" }; + return { themeName, themeData: themeData.dark }; } } } From 9e2d355573cf5d580731ef47005225836bec81c7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 20:58:58 +0530 Subject: [PATCH 23/43] Add logging --- src/platform/web/ThemeBuilder.ts | 101 ++++++++++++++++--------------- src/platform/web/ThemeLoader.ts | 3 +- 2 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index b6f75be4..8035fe84 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -19,6 +19,7 @@ import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; import {ThemeManifest} from "../types/theme"; +import {ILogItem} from "../../logging/types"; export class ThemeBuilder { private _idToManifest: Map; @@ -33,60 +34,64 @@ export class ThemeBuilder { this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest) { - const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends!)!; - const { cssLocation, derivedVariables, icons } = this._getsourceData(baseManifest, location); - const themeName = manifest.name; - if (!themeName) { - throw new Error(`Theme name not found in manifest!`); - } - let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; - for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) { - try { - const themeId = `${manifest.id}-${variant}`; - const { name: variantName, default: isDefault, dark, variables } = variantDetails; - const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); - Object.assign(variables, resolvedVariables); - const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); - Object.assign(variables, resolvedVariables, iconVariables); - const themeDisplayName = `${themeName} ${variantName}`; - if (isDefault) { - const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; - Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables }); + async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem) { + await log.wrap("ThemeBuilder.populateThemeMap", async (l) => { + const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; + const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); + const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest!`); + } + let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; + for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) { + try { + const themeId = `${manifest.id}-${variant}`; + const { name: variantName, default: isDefault, dark, variables } = variantDetails; + const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); + Object.assign(variables, resolvedVariables); + const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); + Object.assign(variables, resolvedVariables, iconVariables); + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables }); + continue; + } + this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, }; + } + catch (e) { + console.error(e); continue; } - this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, }; } - catch (e) { - console.error(e); - continue; + if (defaultDarkVariant.id && defaultLightVariant.id) { + const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; } - } - if (defaultDarkVariant.id && defaultLightVariant.id) { - const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; - } - else { - const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; - } + else { + const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; + } + }); } - private _getsourceData(manifest: ThemeManifest, location: string) { - const runtimeCSSLocation = manifest.source?.["runtime-asset"]; - if (!runtimeCSSLocation) { - throw new Error(`Run-time asset not found in source section for theme at ${location}`); - } - const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href; - const derivedVariables = manifest.source?.["derived-variables"]; - if (!derivedVariables) { - throw new Error(`Derived variables not found in source section for theme at ${location}`); - } - const icons = manifest.source?.["icon"]; - if (!icons) { - throw new Error(`Icon mapping not found in source section for theme at ${location}`); - } - return { cssLocation, derivedVariables, icons }; + private _getSourceData(manifest: ThemeManifest, location: string, log:ILogItem) { + return log.wrap("getSourceData", () => { + const runtimeCSSLocation = manifest.source?.["runtime-asset"]; + if (!runtimeCSSLocation) { + throw new Error(`Run-time asset not found in source section for theme at ${location}`); + } + const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href; + const derivedVariables = manifest.source?.["derived-variables"]; + if (!derivedVariables) { + throw new Error(`Derived variables not found in source section for theme at ${location}`); + } + const icons = manifest.source?.["icon"]; + if (!icons) { + throw new Error(`Icon mapping not found in source section for theme at ${location}`); + } + return { cssLocation, derivedVariables, icons }; + }); } get themeMapping() { diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index c04d8e8c..da5f9658 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -64,7 +64,7 @@ export class ThemeLoader { const { body } = results[i]; try { if (body.extends) { - await this._themeBuilder.populateDerivedTheme(body); + await this._themeBuilder.populateDerivedTheme(body, log); } else { this._populateThemeMap(body, manifestLocations[i], log); @@ -183,6 +183,7 @@ export class ThemeLoader { } this._platform.replaceStylesheet(cssLocation); if (variables) { + log?.log({l: "Derived Theme", variables}); this._themeBuilder.injectCSSVariables(variables); } else { From 9bdf9c500b60d5a1697f07223845228626012b31 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 21:07:04 +0530 Subject: [PATCH 24/43] Add return types --- src/platform/web/ThemeBuilder.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index 8035fe84..c87e68fb 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -34,7 +34,7 @@ export class ThemeBuilder { this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem) { + async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem): Promise { await log.wrap("ThemeBuilder.populateThemeMap", async (l) => { const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); @@ -75,7 +75,8 @@ export class ThemeBuilder { }); } - private _getSourceData(manifest: ThemeManifest, location: string, log:ILogItem) { + private _getSourceData(manifest: ThemeManifest, location: string, log: ILogItem) + : { cssLocation: string, derivedVariables: string[], icons: Record} { return log.wrap("getSourceData", () => { const runtimeCSSLocation = manifest.source?.["runtime-asset"]; if (!runtimeCSSLocation) { @@ -94,11 +95,11 @@ export class ThemeBuilder { }); } - get themeMapping() { + get themeMapping(): Record { return this._themeMapping; } - injectCSSVariables(variables: Record) { + injectCSSVariables(variables: Record): void { const root = document.documentElement; for (const [variable, value] of Object.entries(variables)) { root.style.setProperty(`--${variable}`, value); @@ -106,7 +107,7 @@ export class ThemeBuilder { this._injectedVariables = variables; } - removePreviousCSSVariables() { + removePreviousCSSVariables(): void { if (!this._injectedVariables) { return; } From b29287c47ee60323b5414bdf60cf696100ed5f41 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 17 Jul 2022 22:29:36 +0530 Subject: [PATCH 25/43] await in loop --> Promise.all() --- src/platform/web/ThemeBuilder.ts | 2 +- src/platform/web/ThemeLoader.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index c87e68fb..85d1e336 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -35,7 +35,7 @@ export class ThemeBuilder { } async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem): Promise { - await log.wrap("ThemeBuilder.populateThemeMap", async (l) => { + await log.wrap("ThemeBuilder.populateThemeMap", async () => { const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); const themeName = manifest.name; diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index da5f9658..b899ab5e 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -60,11 +60,13 @@ export class ThemeLoader { ); results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); + const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { const { body } = results[i]; try { if (body.extends) { - await this._themeBuilder.populateDerivedTheme(body, log); + const promise = this._themeBuilder.populateDerivedTheme(body, log); + runtimeThemePromises.push(promise); } else { this._populateThemeMap(body, manifestLocations[i], log); @@ -74,8 +76,9 @@ export class ThemeLoader { console.error(e); } } + await Promise.all(runtimeThemePromises); Object.assign(this._themeMapping, this._themeBuilder.themeMapping); - //Add the default-theme as an additional option to the mapping + // Add the default-theme as an additional option to the mapping const defaultThemeId = this.getDefaultTheme(); if (defaultThemeId) { const themeDetails = this._findThemeDetailsFromId(defaultThemeId); @@ -94,7 +97,7 @@ export class ThemeLoader { } private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { - log.wrap("populateThemeMap", (l) => { + log.wrap("populateThemeMap", () => { /* After build has finished, the source section of each theme manifest contains `built-assets` which is a mapping from the theme-id to From dece42dce3db0dceff48ad1e7810953554622472 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 18 Jul 2022 14:55:13 +0530 Subject: [PATCH 26/43] Do not store all the manifests in memory --- src/platform/web/ThemeBuilder.ts | 13 +++++-------- src/platform/web/ThemeLoader.ts | 18 +++++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index 85d1e336..2ff4108a 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -15,29 +15,26 @@ limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; import type {Platform} from "./Platform.js"; +import type {ThemeManifest} from "../types/theme"; import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; -import {ThemeManifest} from "../types/theme"; import {ILogItem} from "../../logging/types"; export class ThemeBuilder { - private _idToManifest: Map; private _themeMapping: Record = {}; private _preferredColorScheme?: ColorSchemePreference; private _platform: Platform; private _injectedVariables?: Record; - constructor(platform: Platform, manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { - this._idToManifest = manifestMap; + constructor(platform: Platform, preferredColorScheme?: ColorSchemePreference) { this._preferredColorScheme = preferredColorScheme; this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest, log: ILogItem): Promise { + async populateDerivedTheme(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { await log.wrap("ThemeBuilder.populateThemeMap", async () => { - const {manifest: baseManifest, location} = this._idToManifest.get(manifest.extends!)!; - const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, location, log); + const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log); const themeName = manifest.name; if (!themeName) { throw new Error(`Theme name not found in manifest!`); @@ -49,7 +46,7 @@ export class ThemeBuilder { const { name: variantName, default: isDefault, dark, variables } = variantDetails; const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); Object.assign(variables, resolvedVariables); - const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); + const iconVariables = await new IconColorizer(this._platform, icons, variables, baseManifestLocation).toVariables(); Object.assign(variables, resolvedVariables, iconVariables); const themeDisplayName = `${themeName} ${variantName}`; if (isDefault) { diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index b899ab5e..81114a54 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type {ILogItem} from "../../logging/types"; -import { ThemeManifest } from "../types/theme"; +import type {ThemeManifest} from "../types/theme"; import type {Platform} from "./Platform.js"; import {ThemeBuilder} from "./ThemeBuilder"; @@ -52,20 +52,24 @@ export class ThemeLoader { } async init(manifestLocations: string[], log?: ILogItem): Promise { - const idToManifest = new Map(); await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { this._themeMapping = {}; const results = await Promise.all( - manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) + manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); - results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); - this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); + this._themeBuilder = new ThemeBuilder(this._platform, this.preferredColorScheme); const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { const { body } = results[i]; try { if (body.extends) { - const promise = this._themeBuilder.populateDerivedTheme(body, log); + const indexOfBaseManifest = results.findIndex(manifest => manifest.body.id === body.extends); + if (indexOfBaseManifest === -1) { + throw new Error(`Base manifest for derived theme at ${manifestLocations[i]} not found!`); + } + const {body: baseManifest} = results[indexOfBaseManifest]; + const baseManifestLocation = manifestLocations[indexOfBaseManifest]; + const promise = this._themeBuilder.populateDerivedTheme(body, baseManifest, baseManifestLocation, log); runtimeThemePromises.push(promise); } else { @@ -97,7 +101,7 @@ export class ThemeLoader { } private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { - log.wrap("populateThemeMap", () => { + log.wrap("ThemeLoader.populateThemeMap", () => { /* After build has finished, the source section of each theme manifest contains `built-assets` which is a mapping from the theme-id to From 081de5afa81f5eb39b8f997c5e45febc19262677 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 14:52:53 +0530 Subject: [PATCH 27/43] .js --> .mjs --- scripts/postcss/svg-builder.mjs | 2 +- scripts/postcss/{svg-colorizer.js => svg-colorizer.mjs} | 0 src/platform/web/IconColorizer.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename scripts/postcss/{svg-colorizer.js => svg-colorizer.mjs} (100%) diff --git a/scripts/postcss/svg-builder.mjs b/scripts/postcss/svg-builder.mjs index 1bbc4010..ec94f1e5 100644 --- a/scripts/postcss/svg-builder.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,7 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; -import {getColoredSvgString} from "./svg-colorizer.js"; +import {getColoredSvgString} from "./svg-colorizer.mjs"; function createHash(content) { const hasher = new h32(0); diff --git a/scripts/postcss/svg-colorizer.js b/scripts/postcss/svg-colorizer.mjs similarity index 100% rename from scripts/postcss/svg-colorizer.js rename to scripts/postcss/svg-colorizer.mjs diff --git a/src/platform/web/IconColorizer.ts b/src/platform/web/IconColorizer.ts index 81644603..8baf63b0 100644 --- a/src/platform/web/IconColorizer.ts +++ b/src/platform/web/IconColorizer.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {Platform} from "./Platform.js"; -import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.js"; +import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.mjs"; type ParsedStructure = { [variableName: string]: { From 07db5450b7be6e22c0db3d1f9b0400fa02424a03 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 14:55:42 +0530 Subject: [PATCH 28/43] Aliases can also be derived --- src/platform/web/DerivedVariables.ts | 75 ++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/platform/web/DerivedVariables.ts b/src/platform/web/DerivedVariables.ts index 4e3a8b46..3ace908c 100644 --- a/src/platform/web/DerivedVariables.ts +++ b/src/platform/web/DerivedVariables.ts @@ -19,6 +19,8 @@ export class DerivedVariables { private _baseVariables: Record; private _variablesToDerive: string[] private _isDark: boolean + private _aliases: Record = {}; + private _derivedAliases: string[] = []; constructor(baseVariables: Record, variablesToDerive: string[], isDark: boolean) { this._baseVariables = baseVariables; @@ -27,28 +29,73 @@ export class DerivedVariables { } toVariables(): Record { - const aliases: any = {}; const resolvedVariables: any = {}; - const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + this._detectAliases(); + for (const variable of this._variablesToDerive) { + const resolvedValue = this._derive(variable); + if (resolvedValue) { + resolvedVariables[variable] = resolvedValue; + } + } + for (const [alias, variable] of Object.entries(this._aliases) as any) { + resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable]; + } + for (const variable of this._derivedAliases) { + const resolvedValue = this._deriveAlias(variable, resolvedVariables); + if (resolvedValue) { + resolvedVariables[variable] = resolvedValue; + } + } + return resolvedVariables; + } + + private _detectAliases() { + const newVariablesToDerive: string[] = []; for (const variable of this._variablesToDerive) { // If this is an alias, store it for processing later const [alias, value] = variable.split("="); if (value) { - aliases[alias] = value; - continue; + this._aliases[alias] = value; } - // Resolve derived variables - const matches = variable.match(RE_VARIABLE_VALUE); - if (matches) { - const [, baseVariable, operation, argument] = matches; - const value = this._baseVariables[baseVariable]; - const resolvedValue = derive(value, operation, argument, this._isDark); - resolvedVariables[variable] = resolvedValue; + else { + newVariablesToDerive.push(variable); } } - for (const [alias, variable] of Object.entries(aliases) as any) { - resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable]; + this._variablesToDerive = newVariablesToDerive; + } + + private _derive(variable: string): string | undefined { + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = this._baseVariables[baseVariable]; + if (!value ) { + if (this._aliases[baseVariable]) { + this._derivedAliases.push(variable); + return; + } + else { + throw new Error(`Cannot find value for base variable "${baseVariable}"!`); + } + } + const resolvedValue = derive(value, operation, argument, this._isDark); + return resolvedValue; + } + } + + + private _deriveAlias(variable: string, resolvedVariables: Record): string | undefined { + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = resolvedVariables[baseVariable]; + if (!value ) { + throw new Error(`Cannot find value for alias "${baseVariable}" when trying to derive ${variable}!`); + } + const resolvedValue = derive(value, operation, argument, this._isDark); + return resolvedValue; } - return resolvedVariables; } } From 7a1591e0ce87c8a1aae63cc89f615ee1d0bce150 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 15:05:07 +0530 Subject: [PATCH 29/43] Move code --- scripts/postcss/svg-builder.mjs | 2 +- src/platform/web/Platform.js | 2 +- src/platform/web/{ => theming}/DerivedVariables.ts | 4 ++-- src/platform/web/{ => theming}/IconColorizer.ts | 4 ++-- src/platform/web/{ => theming}/ThemeBuilder.ts | 6 +++--- src/platform/web/{ => theming}/ThemeLoader.ts | 0 .../postcss => src/platform/web/theming/actions}/color.mjs | 0 .../platform/web/theming/actions}/svg-colorizer.mjs | 0 vite.common-config.js | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) rename src/platform/web/{ => theming}/DerivedVariables.ts (98%) rename src/platform/web/{ => theming}/IconColorizer.ts (96%) rename src/platform/web/{ => theming}/ThemeBuilder.ts (97%) rename src/platform/web/{ => theming}/ThemeLoader.ts (100%) rename {scripts/postcss => src/platform/web/theming/actions}/color.mjs (100%) rename {scripts/postcss => src/platform/web/theming/actions}/svg-colorizer.mjs (100%) diff --git a/scripts/postcss/svg-builder.mjs b/scripts/postcss/svg-builder.mjs index ec94f1e5..6ef04860 100644 --- a/scripts/postcss/svg-builder.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,7 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; -import {getColoredSvgString} from "./svg-colorizer.mjs"; +import {getColoredSvgString} from "../../src/platform/web/theming/actions/svg-colorizer.mjs"; function createHash(content) { const hasher = new h32(0); diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index c2eef17e..15923a86 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -38,7 +38,7 @@ import {downloadInIframe} from "./dom/download.js"; import {Disposables} from "../../utils/Disposables"; import {parseHTML} from "./parsehtml.js"; import {handleAvatarError} from "./ui/avatar"; -import {ThemeLoader} from "./ThemeLoader"; +import {ThemeLoader} from "./theming/ThemeLoader"; function addScript(src) { return new Promise(function (resolve, reject) { diff --git a/src/platform/web/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts similarity index 98% rename from src/platform/web/DerivedVariables.ts rename to src/platform/web/theming/DerivedVariables.ts index 3ace908c..be48a989 100644 --- a/src/platform/web/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -1,6 +1,6 @@ /* Copyright 2020 The Matrix.org Foundation C.I.C. - + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import {derive} from "../../../scripts/postcss/color.mjs"; +import {derive} from "./actions/color.mjs"; export class DerivedVariables { private _baseVariables: Record; diff --git a/src/platform/web/IconColorizer.ts b/src/platform/web/theming/IconColorizer.ts similarity index 96% rename from src/platform/web/IconColorizer.ts rename to src/platform/web/theming/IconColorizer.ts index 8baf63b0..82a9fd45 100644 --- a/src/platform/web/IconColorizer.ts +++ b/src/platform/web/theming/IconColorizer.ts @@ -13,8 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import type {Platform} from "./Platform.js"; -import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.mjs"; +import type {Platform} from "../Platform.js"; +import {getColoredSvgString} from "./actions/svg-colorizer.mjs"; type ParsedStructure = { [variableName: string]: { diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/theming/ThemeBuilder.ts similarity index 97% rename from src/platform/web/ThemeBuilder.ts rename to src/platform/web/theming/ThemeBuilder.ts index 2ff4108a..d086b6fc 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/theming/ThemeBuilder.ts @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; -import type {Platform} from "./Platform.js"; -import type {ThemeManifest} from "../types/theme"; +import type {Platform} from "../Platform.js"; +import type {ThemeManifest} from "../../types/theme"; import {ColorSchemePreference} from "./ThemeLoader"; import {IconColorizer} from "./IconColorizer"; import {DerivedVariables} from "./DerivedVariables"; -import {ILogItem} from "../../logging/types"; +import {ILogItem} from "../../../logging/types"; export class ThemeBuilder { private _themeMapping: Record = {}; diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts similarity index 100% rename from src/platform/web/ThemeLoader.ts rename to src/platform/web/theming/ThemeLoader.ts diff --git a/scripts/postcss/color.mjs b/src/platform/web/theming/actions/color.mjs similarity index 100% rename from scripts/postcss/color.mjs rename to src/platform/web/theming/actions/color.mjs diff --git a/scripts/postcss/svg-colorizer.mjs b/src/platform/web/theming/actions/svg-colorizer.mjs similarity index 100% rename from scripts/postcss/svg-colorizer.mjs rename to src/platform/web/theming/actions/svg-colorizer.mjs diff --git a/vite.common-config.js b/vite.common-config.js index bcf17115..de464941 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -9,7 +9,7 @@ const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs"; -import {derive} from "./scripts/postcss/color.mjs"; +import {derive} from "./src/platform/web/theming/actions/color.mjs"; const commonOptions = { logLevel: "warn", From 83b5d3b68e6ee7e497bbdf0e1c407f2be03716d8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 15:11:05 +0530 Subject: [PATCH 30/43] Change directory name --- scripts/postcss/svg-builder.mjs | 2 +- src/platform/web/theming/DerivedVariables.ts | 2 +- src/platform/web/theming/IconColorizer.ts | 2 +- src/platform/web/theming/{actions => shared}/color.mjs | 0 src/platform/web/theming/{actions => shared}/svg-colorizer.mjs | 0 vite.common-config.js | 2 +- 6 files changed, 4 insertions(+), 4 deletions(-) rename src/platform/web/theming/{actions => shared}/color.mjs (100%) rename src/platform/web/theming/{actions => shared}/svg-colorizer.mjs (100%) diff --git a/scripts/postcss/svg-builder.mjs b/scripts/postcss/svg-builder.mjs index 6ef04860..3efff4ae 100644 --- a/scripts/postcss/svg-builder.mjs +++ b/scripts/postcss/svg-builder.mjs @@ -17,7 +17,7 @@ limitations under the License. import {readFileSync, mkdirSync, writeFileSync} from "fs"; import {resolve} from "path"; import {h32} from "xxhashjs"; -import {getColoredSvgString} from "../../src/platform/web/theming/actions/svg-colorizer.mjs"; +import {getColoredSvgString} from "../../src/platform/web/theming/shared/svg-colorizer.mjs"; function createHash(content) { const hasher = new h32(0); diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index be48a989..619bab94 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import {derive} from "./actions/color.mjs"; +import {derive} from "./shared/color.mjs"; export class DerivedVariables { private _baseVariables: Record; diff --git a/src/platform/web/theming/IconColorizer.ts b/src/platform/web/theming/IconColorizer.ts index 82a9fd45..e02c0971 100644 --- a/src/platform/web/theming/IconColorizer.ts +++ b/src/platform/web/theming/IconColorizer.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {Platform} from "../Platform.js"; -import {getColoredSvgString} from "./actions/svg-colorizer.mjs"; +import {getColoredSvgString} from "./shared/svg-colorizer.mjs"; type ParsedStructure = { [variableName: string]: { diff --git a/src/platform/web/theming/actions/color.mjs b/src/platform/web/theming/shared/color.mjs similarity index 100% rename from src/platform/web/theming/actions/color.mjs rename to src/platform/web/theming/shared/color.mjs diff --git a/src/platform/web/theming/actions/svg-colorizer.mjs b/src/platform/web/theming/shared/svg-colorizer.mjs similarity index 100% rename from src/platform/web/theming/actions/svg-colorizer.mjs rename to src/platform/web/theming/shared/svg-colorizer.mjs diff --git a/vite.common-config.js b/vite.common-config.js index de464941..2fa09d46 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -9,7 +9,7 @@ const manifest = require("./package.json"); const version = manifest.version; const compiledVariables = new Map(); import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs"; -import {derive} from "./src/platform/web/theming/actions/color.mjs"; +import {derive} from "./src/platform/web/theming/shared/color.mjs"; const commonOptions = { logLevel: "warn", From e1ee2586306ef84c09f91e4770da78921f05c236 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 16:00:58 +0530 Subject: [PATCH 31/43] Change path --- src/platform/web/theming/ThemeLoader.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 81114a54..1c066577 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type {ILogItem} from "../../logging/types"; -import type {ThemeManifest} from "../types/theme"; -import type {Platform} from "./Platform.js"; +import type {ILogItem} from "../../../logging/types"; +import type {ThemeManifest} from "../../types/theme"; +import type {Platform} from "../Platform.js"; import {ThemeBuilder} from "./ThemeBuilder"; type NormalVariant = { From ecb3a66dfcb9c23ca25fde50582c00f7d6052483 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 17:33:06 +0530 Subject: [PATCH 32/43] WIP --- src/platform/web/theming/ThemeLoader.ts | 169 ++++++------------ .../web/theming/parsers/BuiltThemeParser.ts | 106 +++++++++++ .../RuntimeThemeParser.ts} | 37 +--- src/platform/web/theming/parsers/types.ts | 38 ++++ 4 files changed, 203 insertions(+), 147 deletions(-) create mode 100644 src/platform/web/theming/parsers/BuiltThemeParser.ts rename src/platform/web/theming/{ThemeBuilder.ts => parsers/RuntimeThemeParser.ts} (78%) create mode 100644 src/platform/web/theming/parsers/types.ts diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 1c066577..10717975 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -15,37 +15,16 @@ limitations under the License. */ import type {ILogItem} from "../../../logging/types"; -import type {ThemeManifest} from "../../types/theme"; import type {Platform} from "../Platform.js"; -import {ThemeBuilder} from "./ThemeBuilder"; - -type NormalVariant = { - id: string; - cssLocation: string; - variables?: any; -}; - -type Variant = NormalVariant & { - variantName: string; -}; - -type DefaultVariant = { - dark: Variant; - light: Variant; - default: Variant; -} - -export type ThemeInformation = NormalVariant | DefaultVariant; - -export enum ColorSchemePreference { - Dark, - Light -}; +import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser"; +import type {Variant, ThemeInformation} from "./parsers/types"; +import {ColorSchemePreference} from "./parsers/types"; +import { BuiltThemeParser } from "./parsers/BuiltThemeParser"; export class ThemeLoader { private _platform: Platform; private _themeMapping: Record; - private _themeBuilder: ThemeBuilder; + private _injectedVariables?: Record; constructor(platform: Platform) { this._platform = platform; @@ -53,11 +32,11 @@ export class ThemeLoader { async init(manifestLocations: string[], log?: ILogItem): Promise { await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => { - this._themeMapping = {}; const results = await Promise.all( manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); - this._themeBuilder = new ThemeBuilder(this._platform, this.preferredColorScheme); + const runtimeThemeParser = new RuntimeThemeParser(this._platform, this.preferredColorScheme); + const builtThemeParser = new BuiltThemeParser(this.preferredColorScheme); const runtimeThemePromises: Promise[] = []; for (let i = 0; i < results.length; ++i) { const { body } = results[i]; @@ -69,11 +48,11 @@ export class ThemeLoader { } const {body: baseManifest} = results[indexOfBaseManifest]; const baseManifestLocation = manifestLocations[indexOfBaseManifest]; - const promise = this._themeBuilder.populateDerivedTheme(body, baseManifest, baseManifestLocation, log); + const promise = runtimeThemeParser.parse(body, baseManifest, baseManifestLocation, log); runtimeThemePromises.push(promise); } else { - this._populateThemeMap(body, manifestLocations[i], log); + builtThemeParser.parse(body, manifestLocations[i], log); } } catch(e) { @@ -81,98 +60,14 @@ export class ThemeLoader { } } await Promise.all(runtimeThemePromises); - Object.assign(this._themeMapping, this._themeBuilder.themeMapping); - // Add the default-theme as an additional option to the mapping - const defaultThemeId = this.getDefaultTheme(); - if (defaultThemeId) { - const themeDetails = this._findThemeDetailsFromId(defaultThemeId); - if (themeDetails) { - this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! }; - const variables = themeDetails.themeData.variables; - if (variables) { - this._themeMapping["Default"].variables = variables; - } - } - } - log.log({ l: "Default Theme", theme: defaultThemeId}); + this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping }; + Object.assign(this._themeMapping, builtThemeParser.themeMapping, runtimeThemeParser.themeMapping); + this._addDefaultThemeToMapping(log); log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" }); log.log({ l: "Result", themeMapping: this._themeMapping }); }); } - private _populateThemeMap(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { - log.wrap("ThemeLoader.populateThemeMap", () => { - /* - After build has finished, the source section of each theme manifest - contains `built-assets` which is a mapping from the theme-id to - cssLocation of theme - */ - const builtAssets: Record = manifest.source?.["built-assets"]; - const themeName = manifest.name; - if (!themeName) { - throw new Error(`Theme name not found in manifest at ${manifestLocation}`); - } - let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; - for (let [themeId, cssLocation] of Object.entries(builtAssets)) { - try { - /** - * This cssLocation is relative to the location of the manifest file. - * So we first need to resolve it relative to the root of this hydrogen instance. - */ - cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href; - } - catch { - continue; - } - const variant = themeId.match(/.+-(.+)/)?.[1]; - const variantDetails = manifest.values?.variants[variant!]; - if (!variantDetails) { - throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`); - } - const { name: variantName, default: isDefault, dark } = variantDetails; - const themeDisplayName = `${themeName} ${variantName}`; - if (isDefault) { - /** - * This is a default variant! - * We'll add these to the themeMapping (separately) keyed with just the - * theme-name (i.e "Element" instead of "Element Dark"). - * We need to be able to distinguish them from other variants! - * - * This allows us to render radio-buttons with "dark" and - * "light" options. - */ - const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; - defaultVariant.variantName = variantName; - defaultVariant.id = themeId - defaultVariant.cssLocation = cssLocation; - continue; - } - // Non-default variants are keyed in themeMapping with "theme_name variant_name" - // eg: "Element Dark" - this._themeMapping[themeDisplayName] = { - cssLocation, - id: themeId - }; - } - if (defaultDarkVariant.id && defaultLightVariant.id) { - /** - * As mentioned above, if there's both a default dark and a default light variant, - * add them to themeMapping separately. - */ - const defaultVariant = this.preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; - } - else { - /** - * If only one default variant is found (i.e only dark default or light default but not both), - * treat it like any other variant. - */ - const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; - this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; - } - }); - } - setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) { this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => { let cssLocation: string, variables: Record; @@ -191,10 +86,10 @@ export class ThemeLoader { this._platform.replaceStylesheet(cssLocation); if (variables) { log?.log({l: "Derived Theme", variables}); - this._themeBuilder.injectCSSVariables(variables); + this._injectCSSVariables(variables); } else { - this._themeBuilder.removePreviousCSSVariables(); + this._removePreviousCSSVariables(); } this._platform.settingsStorage.setString("theme-name", themeName); if (themeVariant) { @@ -206,6 +101,25 @@ export class ThemeLoader { }); } + private _injectCSSVariables(variables: Record): void { + const root = document.documentElement; + for (const [variable, value] of Object.entries(variables)) { + root.style.setProperty(`--${variable}`, value); + } + this._injectedVariables = variables; + } + + private _removePreviousCSSVariables(): void { + if (!this._injectedVariables) { + return; + } + const root = document.documentElement; + for (const variable of Object.keys(this._injectedVariables)) { + root.style.removeProperty(`--${variable}`); + } + this._injectedVariables = undefined; + } + /** Maps theme display name to theme information */ get themeMapping(): Record { return this._themeMapping; @@ -246,6 +160,23 @@ export class ThemeLoader { } } + private _addDefaultThemeToMapping(log: ILogItem) { + log.wrap("addDefaultThemeToMapping", l => { + const defaultThemeId = this.getDefaultTheme(); + if (defaultThemeId) { + const themeDetails = this._findThemeDetailsFromId(defaultThemeId); + if (themeDetails) { + this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! }; + const variables = themeDetails.themeData.variables; + if (variables) { + this._themeMapping["Default"].variables = variables; + } + } + } + l.log({ l: "Default Theme", theme: defaultThemeId}); + }); + } + get preferredColorScheme(): ColorSchemePreference | undefined { if (window.matchMedia("(prefers-color-scheme: dark)").matches) { return ColorSchemePreference.Dark; diff --git a/src/platform/web/theming/parsers/BuiltThemeParser.ts b/src/platform/web/theming/parsers/BuiltThemeParser.ts new file mode 100644 index 00000000..fbafadb8 --- /dev/null +++ b/src/platform/web/theming/parsers/BuiltThemeParser.ts @@ -0,0 +1,106 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type {ThemeInformation} from "./types"; +import type {ThemeManifest} from "../../../types/theme"; +import type {ILogItem} from "../../../../logging/types"; +import {ColorSchemePreference} from "./types"; + +export class BuiltThemeParser { + private _themeMapping: Record = {}; + private _preferredColorScheme?: ColorSchemePreference; + + constructor(preferredColorScheme?: ColorSchemePreference) { + this._preferredColorScheme = preferredColorScheme; + } + + parse(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) { + log.wrap("BuiltThemeParser.parse", () => { + /* + After build has finished, the source section of each theme manifest + contains `built-assets` which is a mapping from the theme-id to + cssLocation of theme + */ + const builtAssets: Record = manifest.source?.["built-assets"]; + const themeName = manifest.name; + if (!themeName) { + throw new Error(`Theme name not found in manifest at ${manifestLocation}`); + } + let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; + for (let [themeId, cssLocation] of Object.entries(builtAssets)) { + try { + /** + * This cssLocation is relative to the location of the manifest file. + * So we first need to resolve it relative to the root of this hydrogen instance. + */ + cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href; + } + catch { + continue; + } + const variant = themeId.match(/.+-(.+)/)?.[1]; + const variantDetails = manifest.values?.variants[variant!]; + if (!variantDetails) { + throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`); + } + const { name: variantName, default: isDefault, dark } = variantDetails; + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + /** + * This is a default variant! + * We'll add these to the themeMapping (separately) keyed with just the + * theme-name (i.e "Element" instead of "Element Dark"). + * We need to be able to distinguish them from other variants! + * + * This allows us to render radio-buttons with "dark" and + * "light" options. + */ + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + defaultVariant.variantName = variantName; + defaultVariant.id = themeId + defaultVariant.cssLocation = cssLocation; + continue; + } + // Non-default variants are keyed in themeMapping with "theme_name variant_name" + // eg: "Element Dark" + this._themeMapping[themeDisplayName] = { + cssLocation, + id: themeId + }; + } + if (defaultDarkVariant.id && defaultLightVariant.id) { + /** + * As mentioned above, if there's both a default dark and a default light variant, + * add them to themeMapping separately. + */ + const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; + } + else { + /** + * If only one default variant is found (i.e only dark default or light default but not both), + * treat it like any other variant. + */ + const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant; + this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation }; + } + }); + } + + get themeMapping(): Record { + return this._themeMapping; + } +} diff --git a/src/platform/web/theming/ThemeBuilder.ts b/src/platform/web/theming/parsers/RuntimeThemeParser.ts similarity index 78% rename from src/platform/web/theming/ThemeBuilder.ts rename to src/platform/web/theming/parsers/RuntimeThemeParser.ts index d086b6fc..2ab970b6 100644 --- a/src/platform/web/theming/ThemeBuilder.ts +++ b/src/platform/web/theming/parsers/RuntimeThemeParser.ts @@ -13,26 +13,25 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import type {ThemeInformation} from "./ThemeLoader"; -import type {Platform} from "../Platform.js"; -import type {ThemeManifest} from "../../types/theme"; -import {ColorSchemePreference} from "./ThemeLoader"; -import {IconColorizer} from "./IconColorizer"; -import {DerivedVariables} from "./DerivedVariables"; -import {ILogItem} from "../../../logging/types"; +import type {ThemeInformation} from "./types"; +import type {Platform} from "../../Platform.js"; +import type {ThemeManifest} from "../../../types/theme"; +import {ColorSchemePreference} from "./types"; +import {IconColorizer} from "../IconColorizer"; +import {DerivedVariables} from "../DerivedVariables"; +import {ILogItem} from "../../../../logging/types"; -export class ThemeBuilder { +export class RuntimeThemeParser { private _themeMapping: Record = {}; private _preferredColorScheme?: ColorSchemePreference; private _platform: Platform; - private _injectedVariables?: Record; constructor(platform: Platform, preferredColorScheme?: ColorSchemePreference) { this._preferredColorScheme = preferredColorScheme; this._platform = platform; } - async populateDerivedTheme(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { + async parse(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { await log.wrap("ThemeBuilder.populateThemeMap", async () => { const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log); const themeName = manifest.name; @@ -96,22 +95,4 @@ export class ThemeBuilder { return this._themeMapping; } - injectCSSVariables(variables: Record): void { - const root = document.documentElement; - for (const [variable, value] of Object.entries(variables)) { - root.style.setProperty(`--${variable}`, value); - } - this._injectedVariables = variables; - } - - removePreviousCSSVariables(): void { - if (!this._injectedVariables) { - return; - } - const root = document.documentElement; - for (const variable of Object.keys(this._injectedVariables)) { - root.style.removeProperty(`--${variable}`); - } - this._injectedVariables = undefined; - } } diff --git a/src/platform/web/theming/parsers/types.ts b/src/platform/web/theming/parsers/types.ts new file mode 100644 index 00000000..b357cf2c --- /dev/null +++ b/src/platform/web/theming/parsers/types.ts @@ -0,0 +1,38 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export type NormalVariant = { + id: string; + cssLocation: string; + variables?: any; +}; + +export type Variant = NormalVariant & { + variantName: string; +}; + +export type DefaultVariant = { + dark: Variant; + light: Variant; + default: Variant; +} + +export type ThemeInformation = NormalVariant | DefaultVariant; + +export enum ColorSchemePreference { + Dark, + Light +}; From 994667205f851074c71063e5e086c1a48020a334 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 19:38:36 +0530 Subject: [PATCH 33/43] Remove change --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 8f591a6e..f46cc7eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "noEmit": true, "target": "ES2020", "module": "ES2020", - "lib": ["ES2021", "DOM"], "moduleResolution": "node", "esModuleInterop": true }, From de02456641404c1e4cbb1987f1d71169c1f7c58e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 19:46:36 +0530 Subject: [PATCH 34/43] Add explaining comment --- scripts/build-plugins/rollup-plugin-build-themes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index 72053a1f..f44406c6 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -343,6 +343,7 @@ module.exports = function buildThemes(options) { const assetHashedName = this.getFileName(ref); nameToAssetHashedLocation[name] = assetHashedName; } + // Update icon section in output manifest with paths to the icon in build output for (const [variable, location] of Object.entries(icon)) { const [locationWithoutQueryParameters, queryParameters] = location.split("?"); const name = path.basename(locationWithoutQueryParameters); From 7ac2c7c7faff035248c1e8d6fdc5ce771b3d3784 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 21:06:55 +0530 Subject: [PATCH 35/43] Get tests to work --- src/platform/web/theming/shared/color.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/web/theming/shared/color.mjs b/src/platform/web/theming/shared/color.mjs index bd2ea3ea..e777bfac 100644 --- a/src/platform/web/theming/shared/color.mjs +++ b/src/platform/web/theming/shared/color.mjs @@ -13,7 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import {offColor} from 'off-color'; +import pkg from 'off-color'; +const offColor = pkg.offColor; export function derive(value, operation, argument, isDark) { const argumentAsNumber = parseInt(argument); From 8aa96e80310eb431f5cca14affaaf1db38f280c4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 21:19:22 +0530 Subject: [PATCH 36/43] Update log label --- src/platform/web/theming/parsers/RuntimeThemeParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/theming/parsers/RuntimeThemeParser.ts b/src/platform/web/theming/parsers/RuntimeThemeParser.ts index 2ab970b6..9471740a 100644 --- a/src/platform/web/theming/parsers/RuntimeThemeParser.ts +++ b/src/platform/web/theming/parsers/RuntimeThemeParser.ts @@ -32,7 +32,7 @@ export class RuntimeThemeParser { } async parse(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise { - await log.wrap("ThemeBuilder.populateThemeMap", async () => { + await log.wrap("RuntimeThemeParser.parse", async () => { const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log); const themeName = manifest.name; if (!themeName) { From 612b878793ca608a408b70ed3faa34dfe4a911d2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 19 Jul 2022 21:21:35 +0530 Subject: [PATCH 37/43] Update theme name --- theme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme.json b/theme.json index f5fbf3ed..ea7492d4 100644 --- a/theme.json +++ b/theme.json @@ -34,7 +34,7 @@ } }, "red": { - "name": "Red", + "name": "Gruvbox", "variables": { "background-color-primary": "#282828", "background-color-secondary": "#3c3836", From 313e65e00cbc7e2323e87ffbb32081ed21f52ed4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 20 Jul 2022 12:30:41 +0530 Subject: [PATCH 38/43] Write tests --- src/platform/web/theming/DerivedVariables.ts | 32 +++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index 619bab94..e0450e94 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -52,7 +52,6 @@ export class DerivedVariables { private _detectAliases() { const newVariablesToDerive: string[] = []; for (const variable of this._variablesToDerive) { - // If this is an alias, store it for processing later const [alias, value] = variable.split("="); if (value) { this._aliases[alias] = value; @@ -99,3 +98,34 @@ export class DerivedVariables { } } } + +import pkg from "off-color"; +const {offColor} = pkg; + +export function tests() { + return { + "Simple variable derivation": assert => { + const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], false); + const result = deriver.toVariables(); + const resultColor = offColor("#ff00ff").darken(5/100).hex(); + assert.deepEqual(result, {"background-color--darker-5": resultColor}); + }, + + "For dark themes, lighten and darken are inverted": assert => { + const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], true); + const result = deriver.toVariables(); + const resultColor = offColor("#ff00ff").lighten(5/100).hex(); + assert.deepEqual(result, {"background-color--darker-5": resultColor}); + }, + + "Aliases can be derived": assert => { + const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["my-awesome-alias=background-color","my-awesome-alias--darker-5"], false); + const result = deriver.toVariables(); + const resultColor = offColor("#ff00ff").darken(5/100).hex(); + assert.deepEqual(result, { + "my-awesome-alias": "#ff00ff", + "my-awesome-alias--darker-5": resultColor, + }); + }, + } +} From 1456e308a836ed397106a05c5644d72652754ac3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 20 Jul 2022 15:36:02 +0530 Subject: [PATCH 39/43] Add type and fix formatting --- src/platform/web/theming/DerivedVariables.ts | 3 +-- src/platform/web/theming/ThemeLoader.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/platform/web/theming/DerivedVariables.ts b/src/platform/web/theming/DerivedVariables.ts index e0450e94..5a626bfa 100644 --- a/src/platform/web/theming/DerivedVariables.ts +++ b/src/platform/web/theming/DerivedVariables.ts @@ -49,7 +49,7 @@ export class DerivedVariables { return resolvedVariables; } - private _detectAliases() { + private _detectAliases(): void { const newVariablesToDerive: string[] = []; for (const variable of this._variablesToDerive) { const [alias, value] = variable.split("="); @@ -83,7 +83,6 @@ export class DerivedVariables { } } - private _deriveAlias(variable: string, resolvedVariables: Record): string | undefined { const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; const matches = variable.match(RE_VARIABLE_VALUE); diff --git a/src/platform/web/theming/ThemeLoader.ts b/src/platform/web/theming/ThemeLoader.ts index 10717975..be1bafc0 100644 --- a/src/platform/web/theming/ThemeLoader.ts +++ b/src/platform/web/theming/ThemeLoader.ts @@ -19,7 +19,7 @@ import type {Platform} from "../Platform.js"; import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser"; import type {Variant, ThemeInformation} from "./parsers/types"; import {ColorSchemePreference} from "./parsers/types"; -import { BuiltThemeParser } from "./parsers/BuiltThemeParser"; +import {BuiltThemeParser} from "./parsers/BuiltThemeParser"; export class ThemeLoader { private _platform: Platform; From 7feaa479c0f4c79f012c6690b4fe2ee769f94bb6 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 20 Jul 2022 15:55:11 +0530 Subject: [PATCH 40/43] Typescript update to support .mjs files --- package.json | 2 +- tsconfig.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index dd1a1554..9361098c 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "regenerator-runtime": "^0.13.7", "svgo": "^2.8.0", "text-encoding": "^0.7.0", - "typescript": "^4.3.5", + "typescript": "^4.7.0", "vite": "^2.9.8", "xxhashjs": "^0.2.2" }, diff --git a/tsconfig.json b/tsconfig.json index f46cc7eb..72d9d3ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "noImplicitAny": false, "noEmit": true, "target": "ES2020", - "module": "ES2020", + "module": "Node16", "moduleResolution": "node", "esModuleInterop": true }, diff --git a/yarn.lock b/yarn.lock index 1543f8d0..c48d2719 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1665,10 +1665,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@^4.7.0: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.39" From 8d766ac5048e47221ac7db177bfaa3e3b26eba64 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 21 Jul 2022 12:05:10 +0530 Subject: [PATCH 41/43] Remove await within loop --- .../build-plugins/rollup-plugin-build-themes.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index f44406c6..bb1c65c1 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -51,12 +51,20 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) { */ async function generateIconSourceMap(icons, manifestLocation) { const sources = {}; + const fileNames = []; + const promises = []; const fs = require("fs").promises; for (const icon of Object.values(icons)) { const [location] = icon.split("?"); const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); - const iconData = await fs.readFile(resolvedLocation); - const svgString = iconData.toString(); + const iconData = fs.readFile(resolvedLocation); + promises.push(iconData); + const fileName = path.basename(resolvedLocation); + fileNames.push(fileName); + } + const results = await Promise.all(promises); + for (let i = 0; i < results.length; ++i) { + const svgString = results[i].toString(); const result = optimize(svgString, { plugins: [ { @@ -68,8 +76,7 @@ async function generateIconSourceMap(icons, manifestLocation) { ], }); const optimizedSvgString = result.data; - const fileName = path.basename(resolvedLocation); - sources[fileName] = optimizedSvgString; + sources[fileNames[i]] = optimizedSvgString; } return sources; } From 708637e390250db305e11bbc2c7b5bf8cc30479e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 29 Jul 2022 16:45:25 +0530 Subject: [PATCH 42/43] No need for this complex resolve --- scripts/build-plugins/rollup-plugin-build-themes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index bb1c65c1..5b913448 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -56,7 +56,8 @@ async function generateIconSourceMap(icons, manifestLocation) { const fs = require("fs").promises; for (const icon of Object.values(icons)) { const [location] = icon.split("?"); - const resolvedLocation = path.resolve(__dirname, "../../", manifestLocation, location); + // resolve location against manifestLocation + const resolvedLocation = path.resolve(manifestLocation, location); const iconData = fs.readFile(resolvedLocation); promises.push(iconData); const fileName = path.basename(resolvedLocation); From 39817dc36bb8952aa7ce55045c7ead6f506c776f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 29 Jul 2022 17:33:33 +0530 Subject: [PATCH 43/43] Revert back option --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 72d9d3ae..f46cc7eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "noImplicitAny": false, "noEmit": true, "target": "ES2020", - "module": "Node16", + "module": "ES2020", "moduleResolution": "node", "esModuleInterop": true },