From 8068c5eff5554850fa1cc820b2013ee9cd1a3461 Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:30:02 -0400 Subject: [PATCH 01/93] Duplicate seti icon theme from VS Code --- icons/theme-seti/.vscodeignore | 4 + icons/theme-seti/CONTRIBUTING.md | 33 + icons/theme-seti/README.md | 32 + icons/theme-seti/ThirdPartyNotices.txt | 32 + icons/theme-seti/build/update-icon-theme.js | 473 ++++ icons/theme-seti/cgmanifest.json | 16 + icons/theme-seti/icons/preview.html | 104 + .../icons/seti-circular-128x128.png | Bin 0 -> 4814 bytes icons/theme-seti/icons/seti.woff | Bin 0 -> 37300 bytes .../theme-seti/icons/vs-seti-icon-theme.json | 2406 +++++++++++++++++ icons/theme-seti/package.json | 30 + icons/theme-seti/package.nls.json | 5 + 12 files changed, 3135 insertions(+) create mode 100644 icons/theme-seti/.vscodeignore create mode 100644 icons/theme-seti/CONTRIBUTING.md create mode 100644 icons/theme-seti/README.md create mode 100644 icons/theme-seti/ThirdPartyNotices.txt create mode 100644 icons/theme-seti/build/update-icon-theme.js create mode 100644 icons/theme-seti/cgmanifest.json create mode 100644 icons/theme-seti/icons/preview.html create mode 100644 icons/theme-seti/icons/seti-circular-128x128.png create mode 100644 icons/theme-seti/icons/seti.woff create mode 100644 icons/theme-seti/icons/vs-seti-icon-theme.json create mode 100644 icons/theme-seti/package.json create mode 100644 icons/theme-seti/package.nls.json diff --git a/icons/theme-seti/.vscodeignore b/icons/theme-seti/.vscodeignore new file mode 100644 index 0000000..25699ef --- /dev/null +++ b/icons/theme-seti/.vscodeignore @@ -0,0 +1,4 @@ +build/** +cgmanifest.json +icons/preview.html +CONTRIBUTING.md diff --git a/icons/theme-seti/CONTRIBUTING.md b/icons/theme-seti/CONTRIBUTING.md new file mode 100644 index 0000000..df0501d --- /dev/null +++ b/icons/theme-seti/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# theme-seti + +This is an icon theme that uses the icons from [`seti-ui`](https://github.com/jesseweed/seti-ui). + +## Previewing icons + +There is a [`./icons/preview.html`](./icons/preview.html) file that can be opened to see all of the icons included in the theme. +To view this, it needs to be hosted by a web server. The easiest way is to open the file with the `Open with Live Server` command from the [Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer). + + +## Updating icons + +- Make a PR against https://github.com/jesseweed/seti-ui with your icon changes. +- Once accepted there, ping us or make a PR yourself that updates the theme and font here + +To adopt the latest changes from https://github.com/jesseweed/seti-ui: + +- have the main branches of `https://github.com/jesseweed/seti-ui` and `https://github.com/microsoft/vscode` cloned in the same parent folder +- in the `seti-ui` folder, run `npm install` and `npm run prepublishOnly`. This will generate updated icons and fonts. +- in the `vscode/extensions/theme-seti` folder run `npm run update`. This will launch the [icon theme update script](build/update-icon-theme.js) that updates the theme as well as the font based on content in `seti-ui`. +- to test the icon theme, look at the icon preview as described above. +- when done, create a PR with the changes in https://github.com/microsoft/vscode. +Add a screenshot of the preview page to accompany it. + + +### Languages not shipped with `vscode` + +Languages that are not shipped with `vscode` must be added to the `nonBuiltInLanguages` object inside of `update-icon-theme.js`. + +These should match [the file mapping in `seti-ui`](https://github.com/jesseweed/seti-ui/blob/master/styles/components/icons/mapping.less). + +Please try and keep this list in alphabetical order! Thank you. + diff --git a/icons/theme-seti/README.md b/icons/theme-seti/README.md new file mode 100644 index 0000000..dcd9ba9 --- /dev/null +++ b/icons/theme-seti/README.md @@ -0,0 +1,32 @@ +# theme-seti + +This is an icon theme that uses the icons from [`seti-ui`](https://github.com/jesseweed/seti-ui). + +## Updating icons + +There is script that can be used to update icons, [./build/update-icon-theme.js](build/update-icon-theme.js). + +To run this script, run `npm run update` from the `theme-seti` directory. + +This can be run in one of two ways: looking at a local copy of `seti-ui` for icons, or getting them straight from GitHub. + +If you want to run it from a local copy of `seti-ui`, first clone [`seti-ui`](https://github.com/jesseweed/seti-ui) to the folder next to your `vscode` repo (from the `theme-seti` directory, `../../`). +Then, inside the `set-ui` directory, run `npm install` followed by `npm run prepublishOnly`. This will generate updated icons. + +If you want to download the icons straight from GitHub, change the `FROM_DISK` variable to `false` inside of `update-icon-theme.js`. + +### Languages not shipped with `vscode` + +Languages that are not shipped with `vscode` must be added to the `nonBuiltInLanguages` object inside of `update-icon-theme.js`. + +These should match [the file mapping in `seti-ui`](https://github.com/jesseweed/seti-ui/blob/master/styles/components/icons/mapping.less). + +Please try and keep this list in alphabetical order! Thank you. + +## Previewing icons + +There is a [`./icons/preview.html`](./icons/preview.html) file that can be opened to see all of the icons included in the theme. +Note that to view this, it needs to be hosted by a web server. + +When updating icons, it is always a good idea to make sure that they work properly by looking at this page. +When submitting a PR that updates these icons, a screenshot of the preview page should accompany it. diff --git a/icons/theme-seti/ThirdPartyNotices.txt b/icons/theme-seti/ThirdPartyNotices.txt new file mode 100644 index 0000000..29cbcd4 --- /dev/null +++ b/icons/theme-seti/ThirdPartyNotices.txt @@ -0,0 +1,32 @@ + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +For Microsoft vscode-theme-seti + +This file is based on or incorporates material from the projects listed below ("Third Party OSS"). The original copyright +notice and the license under which Microsoft received such Third Party OSS, are set forth below. Such licenses and notice +are provided for informational purposes only. Microsoft licenses the Third Party OSS to you under the licensing terms for +the Microsoft product or service. Microsoft reserves all other rights not expressly granted under this agreement, whether +by implication, estoppel or otherwise.† + +1. Seti UI - A subtle dark colored UI theme for Atom. (https://github.com/jesseweed/seti-ui) + +Copyright (c) 2014 Jesse Weed + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/icons/theme-seti/build/update-icon-theme.js b/icons/theme-seti/build/update-icon-theme.js new file mode 100644 index 0000000..2b16374 --- /dev/null +++ b/icons/theme-seti/build/update-icon-theme.js @@ -0,0 +1,473 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const https = require('https'); +const url = require('url'); +const minimatch = require('minimatch'); + +// list of languagesId not shipped with VSCode. The information is used to associate an icon with a language association +// Please try and keep this list in alphabetical order! Thank you. +const nonBuiltInLanguages = { // { fileNames, extensions } + "argdown": { extensions: ['ad', 'adown', 'argdown', 'argdn'] }, + "bicep": { extensions: ['bicep'] }, + "elixir": { extensions: ['ex'] }, + "elm": { extensions: ['elm'] }, + "erb": { extensions: ['erb', 'rhtml', 'html.erb'] }, + "github-issues": { extensions: ['github-issues'] }, + "gradle": { extensions: ['gradle'] }, + "godot": { extensions: ['gd', 'godot', 'tres', 'tscn'] }, + "haml": { extensions: ['haml'] }, + "haskell": { extensions: ['hs'] }, + "haxe": { extensions: ['hx'] }, + "jinja": { extensions: ['jinja'] }, + "kotlin": { extensions: ['kt'] }, + "mustache": { extensions: ['mustache', 'mst', 'mu', 'stache'] }, + "nunjucks": { extensions: ['nunjucks', 'nunjs', 'nunj', 'nj', 'njk', 'tmpl', 'tpl'] }, + "ocaml": { extensions: ['ml', 'mli', 'mll', 'mly', 'eliom', 'eliomi'] }, + "puppet": { extensions: ['puppet'] }, + "r": { extensions: ['r', 'rhistory', 'rprofile', 'rt'] }, + "rescript": { extensions: ['res', 'resi'] }, + "sass": { extensions: ['sass'] }, + "stylus": { extensions: ['styl'] }, + "terraform": { extensions: ['tf', 'tfvars', 'hcl'] }, + "todo": { fileNames: ['todo'] }, + "vala": { extensions: ['vala'] }, + "vue": { extensions: ['vue'] } +}; + +// list of languagesId that inherit the icon from another language +const inheritIconFromLanguage = { + "jsonc": 'json', + "jsonl": 'json', + "postcss": 'css', + "django-html": 'html', + "blade": 'php' +}; + +const ignoreExtAssociation = { + "properties": true +}; + +const FROM_DISK = true; // set to true to take content from a repo checked out next to the vscode repo + +let font, fontMappingsFile, fileAssociationFile, colorsFile; +if (!FROM_DISK) { + font = 'https://raw.githubusercontent.com/jesseweed/seti-ui/master/styles/_fonts/seti/seti.woff'; + fontMappingsFile = 'https://raw.githubusercontent.com/jesseweed/seti-ui/master/styles/_fonts/seti.less'; + fileAssociationFile = 'https://raw.githubusercontent.com/jesseweed/seti-ui/master/styles/components/icons/mapping.less'; + colorsFile = 'https://raw.githubusercontent.com/jesseweed/seti-ui/master/styles/ui-variables.less'; +} else { + font = '../../../seti-ui/styles/_fonts/seti/seti.woff'; + fontMappingsFile = '../../../seti-ui/styles/_fonts/seti.less'; + fileAssociationFile = '../../../seti-ui/styles/components/icons/mapping.less'; + colorsFile = '../../../seti-ui/styles/ui-variables.less'; +} + +function getCommitSha(repoId) { + const commitInfo = 'https://api.github.com/repos/' + repoId + '/commits/master'; + return download(commitInfo).then(function (content) { + try { + const lastCommit = JSON.parse(content); + return Promise.resolve({ + commitSha: lastCommit.sha, + commitDate: lastCommit.commit.author.date + }); + } catch (e) { + console.error('Failed parsing ' + content); + return Promise.resolve(null); + } + }, function () { + console.error('Failed loading ' + commitInfo); + return Promise.resolve(null); + }); +} + +function download(source) { + if (source.startsWith('.')) { + return readFile(source); + } + return new Promise((c, e) => { + const _url = url.parse(source); + const options = { host: _url.host, port: _url.port, path: _url.path, headers: { 'User-Agent': 'NodeJS' } }; + let content = ''; + https.get(options, function (response) { + response.on('data', function (data) { + content += data.toString(); + }).on('end', function () { + c(content); + }); + }).on('error', function (err) { + e(err.message); + }); + }); +} + +function readFile(fileName) { + return new Promise((c, e) => { + fs.readFile(fileName, function (err, data) { + if (err) { + e(err); + } else { + c(data.toString()); + } + }); + }); +} + +function downloadBinary(source, dest) { + if (source.startsWith('.')) { + return copyFile(source, dest); + } + + return new Promise((c, e) => { + https.get(source, function (response) { + switch (response.statusCode) { + case 200: { + const file = fs.createWriteStream(dest); + response.on('data', function (chunk) { + file.write(chunk); + }).on('end', function () { + file.end(); + c(null); + }).on('error', function (err) { + fs.unlink(dest); + e(err.message); + }); + break; + } + case 301: + case 302: + case 303: + case 307: + console.log('redirect to ' + response.headers.location); + downloadBinary(response.headers.location, dest).then(c, e); + break; + default: + e(new Error('Server responded with status code ' + response.statusCode)); + } + }); + }); +} + +function copyFile(fileName, dest) { + return new Promise((c, e) => { + let cbCalled = false; + function handleError(err) { + if (!cbCalled) { + e(err); + cbCalled = true; + } + } + const rd = fs.createReadStream(fileName); + rd.on("error", handleError); + const wr = fs.createWriteStream(dest); + wr.on("error", handleError); + wr.on("close", function () { + if (!cbCalled) { + c(); + cbCalled = true; + } + }); + rd.pipe(wr); + }); +} + +function darkenColor(color) { + let res = '#'; + for (let i = 1; i < 7; i += 2) { + const newVal = Math.round(parseInt('0x' + color.substr(i, 2), 16) * 0.9); + const hex = newVal.toString(16); + if (hex.length === 1) { + res += '0'; + } + res += hex; + } + return res; +} + +function mergeMapping(to, from, property) { + if (from[property]) { + if (to[property]) { + to[property].push(...from[property]); + } else { + to[property] = from[property]; + } + } +} + +function getLanguageMappings() { + const langMappings = {}; + const allExtensions = fs.readdirSync('..'); + for (let i = 0; i < allExtensions.length; i++) { + const dirPath = path.join('..', allExtensions[i], 'package.json'); + if (fs.existsSync(dirPath)) { + const content = fs.readFileSync(dirPath).toString(); + const jsonContent = JSON.parse(content); + const languages = jsonContent.contributes && jsonContent.contributes.languages; + if (Array.isArray(languages)) { + for (let k = 0; k < languages.length; k++) { + const languageId = languages[k].id; + if (languageId) { + const extensions = languages[k].extensions; + const mapping = {}; + if (Array.isArray(extensions)) { + mapping.extensions = extensions.map(function (e) { return e.substr(1).toLowerCase(); }); + } + const filenames = languages[k].filenames; + if (Array.isArray(filenames)) { + mapping.fileNames = filenames.map(function (f) { return f.toLowerCase(); }); + } + const filenamePatterns = languages[k].filenamePatterns; + if (Array.isArray(filenamePatterns)) { + mapping.filenamePatterns = filenamePatterns.map(function (f) { return f.toLowerCase(); }); + } + const existing = langMappings[languageId]; + + if (existing) { + // multiple contributions to the same language + // give preference to the contribution wth the configuration + if (languages[k].configuration) { + mergeMapping(mapping, existing, 'extensions'); + mergeMapping(mapping, existing, 'fileNames'); + mergeMapping(mapping, existing, 'filenamePatterns'); + langMappings[languageId] = mapping; + } else { + mergeMapping(existing, mapping, 'extensions'); + mergeMapping(existing, mapping, 'fileNames'); + mergeMapping(existing, mapping, 'filenamePatterns'); + } + } else { + langMappings[languageId] = mapping; + } + } + } + } + } + } + for (const languageId in nonBuiltInLanguages) { + langMappings[languageId] = nonBuiltInLanguages[languageId]; + } + return langMappings; +} + +exports.copyFont = function () { + return downloadBinary(font, './icons/seti.woff'); +}; + +exports.update = function () { + + console.log('Reading from ' + fontMappingsFile); + const def2Content = {}; + const ext2Def = {}; + const fileName2Def = {}; + const def2ColorId = {}; + const colorId2Value = {}; + const lang2Def = {}; + + function writeFileIconContent(info) { + const iconDefinitions = {}; + const allDefs = Object.keys(def2Content).sort(); + + for (let i = 0; i < allDefs.length; i++) { + const def = allDefs[i]; + const entry = { fontCharacter: def2Content[def] }; + const colorId = def2ColorId[def]; + if (colorId) { + const colorValue = colorId2Value[colorId]; + if (colorValue) { + entry.fontColor = colorValue; + + const entryInverse = { fontCharacter: entry.fontCharacter, fontColor: darkenColor(colorValue) }; + iconDefinitions[def + '_light'] = entryInverse; + } + } + iconDefinitions[def] = entry; + } + + function getInvertSet(input) { + const result = {}; + for (const assoc in input) { + const invertDef = input[assoc] + '_light'; + if (iconDefinitions[invertDef]) { + result[assoc] = invertDef; + } + } + return result; + } + + const res = { + information_for_contributors: [ + 'This file has been generated from data in https://github.com/jesseweed/seti-ui', + '- icon definitions: https://github.com/jesseweed/seti-ui/blob/master/styles/_fonts/seti.less', + '- icon colors: https://github.com/jesseweed/seti-ui/blob/master/styles/ui-variables.less', + '- file associations: https://github.com/jesseweed/seti-ui/blob/master/styles/components/icons/mapping.less', + 'If you want to provide a fix or improvement, please create a pull request against the jesseweed/seti-ui repository.', + 'Once accepted there, we are happy to receive an update request.', + ], + fonts: [{ + id: "seti", + src: [{ "path": "./seti.woff", "format": "woff" }], + weight: "normal", + style: "normal", + size: "150%" + }], + iconDefinitions: iconDefinitions, + // folder: "_folder", + file: "_default", + fileExtensions: ext2Def, + fileNames: fileName2Def, + languageIds: lang2Def, + light: { + file: "_default_light", + fileExtensions: getInvertSet(ext2Def), + languageIds: getInvertSet(lang2Def), + fileNames: getInvertSet(fileName2Def) + }, + version: 'https://github.com/jesseweed/seti-ui/commit/' + info.commitSha, + }; + + const path = './icons/vs-seti-icon-theme.json'; + fs.writeFileSync(path, JSON.stringify(res, null, '\t')); + console.log('written ' + path); + } + + + let match; + + return download(fontMappingsFile).then(function (content) { + const regex = /@([\w-]+):\s*'(\\E[0-9A-F]+)';/g; + const contents = {}; + while ((match = regex.exec(content)) !== null) { + contents[match[1]] = match[2]; + } + + return download(fileAssociationFile).then(function (content) { + const regex2 = /\.icon-(?:set|partial)\(['"]([\w-\.+]+)['"],\s*['"]([\w-]+)['"],\s*(@[\w-]+)\)/g; + while ((match = regex2.exec(content)) !== null) { + const pattern = match[1]; + let def = '_' + match[2]; + const colorId = match[3]; + let storedColorId = def2ColorId[def]; + let i = 1; + while (storedColorId && colorId !== storedColorId) { // different colors for the same def? + def = `_${match[2]}_${i}`; + storedColorId = def2ColorId[def]; + i++; + } + if (!def2ColorId[def]) { + def2ColorId[def] = colorId; + def2Content[def] = contents[match[2]]; + } + + if (def === '_default') { + continue; // no need to assign default color. + } + if (pattern[0] === '.') { + ext2Def[pattern.substr(1).toLowerCase()] = def; + } else { + fileName2Def[pattern.toLowerCase()] = def; + } + } + // replace extensions for languageId + const langMappings = getLanguageMappings(); + for (let lang in langMappings) { + const mappings = langMappings[lang]; + const exts = mappings.extensions || []; + const fileNames = mappings.fileNames || []; + const filenamePatterns = mappings.filenamePatterns || []; + let preferredDef = null; + // use the first file extension association for the preferred definition + for (let i1 = 0; i1 < exts.length && !preferredDef; i1++) { + preferredDef = ext2Def[exts[i1]]; + } + // use the first file name association for the preferred definition, if not availbale + for (let i1 = 0; i1 < fileNames.length && !preferredDef; i1++) { + preferredDef = fileName2Def[fileNames[i1]]; + } + for (let i1 = 0; i1 < filenamePatterns.length && !preferredDef; i1++) { + let pattern = filenamePatterns[i1]; + for (const name in fileName2Def) { + if (minimatch(name, pattern)) { + preferredDef = fileName2Def[name]; + break; + } + } + } + if (preferredDef) { + lang2Def[lang] = preferredDef; + if (!nonBuiltInLanguages[lang] && !inheritIconFromLanguage[lang]) { + for (let i2 = 0; i2 < exts.length; i2++) { + // remove the extension association, unless it is different from the preferred + if (ext2Def[exts[i2]] === preferredDef || ignoreExtAssociation[exts[i2]]) { + delete ext2Def[exts[i2]]; + } + } + for (let i2 = 0; i2 < fileNames.length; i2++) { + // remove the fileName association, unless it is different from the preferred + if (fileName2Def[fileNames[i2]] === preferredDef) { + delete fileName2Def[fileNames[i2]]; + } + } + for (let i2 = 0; i2 < filenamePatterns.length; i2++) { + let pattern = filenamePatterns[i2]; + // remove the filenamePatterns association, unless it is different from the preferred + for (const name in fileName2Def) { + if (minimatch(name, pattern) && fileName2Def[name] === preferredDef) { + delete fileName2Def[name]; + } + } + } + } + } + } + for (const lang in inheritIconFromLanguage) { + const superLang = inheritIconFromLanguage[lang]; + const def = lang2Def[superLang]; + if (def) { + lang2Def[lang] = def; + } else { + console.log('skipping icon def for ' + lang + ': no icon for ' + superLang + ' defined'); + } + + } + + + return download(colorsFile).then(function (content) { + const regex3 = /(@[\w-]+):\s*(#[0-9a-z]+)/g; + while ((match = regex3.exec(content)) !== null) { + colorId2Value[match[1]] = match[2]; + } + return getCommitSha('jesseweed/seti-ui').then(function (info) { + try { + writeFileIconContent(info); + + const cgmanifestPath = './cgmanifest.json'; + const cgmanifest = fs.readFileSync(cgmanifestPath).toString(); + const cgmanifestContent = JSON.parse(cgmanifest); + cgmanifestContent['registrations'][0]['component']['git']['commitHash'] = info.commitSha; + fs.writeFileSync(cgmanifestPath, JSON.stringify(cgmanifestContent, null, '\t')); + console.log('updated ' + cgmanifestPath); + + console.log('Updated to jesseweed/seti-ui@' + info.commitSha.substr(0, 7) + ' (' + info.commitDate.substr(0, 10) + ')'); + + } catch (e) { + console.error(e); + } + }); + }); + }); + }, console.error); +}; + +if (path.basename(process.argv[1]) === 'update-icon-theme.js') { + exports.copyFont().then(() => exports.update()); +} + + + diff --git a/icons/theme-seti/cgmanifest.json b/icons/theme-seti/cgmanifest.json new file mode 100644 index 0000000..8d50dd2 --- /dev/null +++ b/icons/theme-seti/cgmanifest.json @@ -0,0 +1,16 @@ +{ + "registrations": [ + { + "component": { + "type": "git", + "git": { + "name": "seti-ui", + "repositoryUrl": "https://github.com/jesseweed/seti-ui", + "commitHash": "1cac4f30f93cc898103c62dde41823a09b0d7b74" + } + }, + "version": "0.1.0" + } + ], + "version": 1 +} \ No newline at end of file diff --git a/icons/theme-seti/icons/preview.html b/icons/theme-seti/icons/preview.html new file mode 100644 index 0000000..84b5166 --- /dev/null +++ b/icons/theme-seti/icons/preview.html @@ -0,0 +1,104 @@ + + + +
+ +VSrP-7<+RW!CWboh@80RSMJ008*BPdSHEa~n@H008n800_)_^Tn9$KZgJuV}D9sM9kT+A%
zKFuQ_E=2G7mn>iVw#t&$EY~t^9(7hArY%;25^azeA?QjKrLASa5&EdJb;ll2Ue4_x
z65S{jFz=h=C_g@wV2#7ManRmHM!ZW{D#))M&+Zt-<<45Gch~9WUzdOR%I{9TaOG21
z)^0oEd>1sjU-}|>=LfapcYXc