flow like the river

This commit is contained in:
root 2025-11-07 00:06:12 +01:00
commit 013fe673f3
42435 changed files with 5764238 additions and 0 deletions

173
BACK_BACK/node_modules/uncss/src/jsdom.js generated vendored Executable file
View file

@ -0,0 +1,173 @@
'use strict';
const fs = require('fs'),
isHTML = require('is-html'),
isURL = require('is-absolute-url'),
{ JSDOM, ResourceLoader, VirtualConsole } = require('jsdom'),
path = require('path'),
{ Console } = require('console'),
_ = require('lodash');
/**
* Jsdom expects promises returned by ResourceLoader.fetch to have an 'abort' method.
* @param {Promise} promise The promise to augment.
*/
function makeResourcePromise(promise) {
promise.abort = () => { /* noop */ };
return promise;
}
class CustomResourcesLoader extends ResourceLoader {
constructor(htmlroot, strictSSL, userAgent) {
super({
strictSSL,
userAgent
});
// The htmlroot option allows root-relative URLs (starting with a slash)
// to be used for all resources. Without it, root-relative URLs are
// looked up relative to file://, so will not be found.
this.htmlroot = htmlroot || '';
}
fetch(originalUrl, options) {
const element = options && options.element;
if (!element) {
// HTTP request?
return super.fetch(originalUrl, options);
}
if (!element || element.nodeName !== 'SCRIPT') {
// Only scripts need to be fetched. Stylesheets are read later by uncss.
return makeResourcePromise(Promise.resolve(Buffer.from('')));
}
// See whether raw attribute value is root-relative.
const src = element.getAttribute('src');
if (src && path.isAbsolute(src)) {
const url = path.join(this.htmlroot, src);
return makeResourcePromise(new Promise((resolve, reject) => {
try {
const buffer = fs.readFileSync(url);
resolve(buffer);
} catch (e) {
reject(e);
}
}));
}
return super.fetch(originalUrl, options);
}
}
function defaultOptions() {
return {
features: {
FetchExternalResources: ['script'],
ProcessExternalResources: ['script']
},
runScripts: 'dangerously',
userAgent: 'uncss',
virtualConsole: new VirtualConsole().sendTo(new Console(process.stderr))
};
}
/**
* Load a page.
* @param {String} src
* @param {Object} options
* @return {Promise<JSDOM>}
*/
function fromSource(src, options) {
const config = _.cloneDeep(options.jsdom);
config.resources = new CustomResourcesLoader(options.htmlroot, options.strictSSL, options.userAgent);
return new Promise((resolve, reject) => {
let pagePromise;
if (isURL(src)) {
pagePromise = JSDOM.fromURL(src, config);
} else if (isHTML(src)) {
pagePromise = Promise.resolve(new JSDOM(src, config));
} else {
pagePromise = JSDOM.fromFile(src, config);
}
return pagePromise.then((page) => {
if (options.inject) {
if (typeof options.inject === 'function') {
options.inject(page.window);
} else {
require(path.join(__dirname, options.inject))(page.window);
}
}
setTimeout(() => resolve(page), options.timeout);
}).catch((e) => {
reject(e);
});
});
}
/**
* Extract stylesheets' hrefs from dom
* @param {Object} window A jsdom window
* @param {Object} options Options, as passed to UnCSS
* @return {Array}
*/
function getStylesheets(window, options) {
if (Array.isArray(options.media) === false) {
options.media = [options.media];
}
const media = _.union(['', 'all', 'screen'], options.media);
const elements = window.document.querySelectorAll('link[rel="stylesheet"]');
return Array.prototype.map
.call(elements, (link) => ({
href: link.getAttribute('href'),
media: link.getAttribute('media') || ''
}))
.filter((sheet) => media.indexOf(sheet.media) !== -1)
.map((sheet) => sheet.href);
}
/**
* Filter unused selectors.
* @param {Object} window A jsdom window
* @param {Array} sels List of selectors to be filtered
* @return {Array}
*/
function findAll(window, sels) {
const document = window.document;
// Unwrap noscript elements.
const elements = document.getElementsByTagName('noscript');
Array.prototype.forEach.call(elements, (ns) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = ns.textContent;
// Insert each child of the <noscript> as its sibling
Array.prototype.forEach.call(wrapper.children, (child) => {
ns.parentNode.insertBefore(child, ns);
});
});
// Do the filtering.
return sels.filter((selector) => {
try {
return document.querySelector(selector);
} catch (e) {
return true;
}
});
}
module.exports = {
defaultOptions,
fromSource,
findAll,
getStylesheets
};

255
BACK_BACK/node_modules/uncss/src/lib.js generated vendored Executable file
View file

@ -0,0 +1,255 @@
'use strict';
const jsdom = require('./jsdom.js'),
postcss = require('postcss'),
postcssSelectorParser = require('postcss-selector-parser'),
_ = require('lodash');
/* Some styles are applied only with user interaction, and therefore its
* selectors cannot be used with querySelectorAll.
* http://www.w3.org/TR/2001/CR-css3-selectors-20011113/
*/
const dePseudify = (function () {
const ignoredPseudos = [
/* link */
':link', ':visited',
/* user action */
':hover', ':active', ':focus', ':focus-within',
/* UI element states */
':enabled', ':disabled', ':checked', ':indeterminate',
/* form validation */
':required', ':invalid', ':valid',
/* pseudo elements */
'::first-line', '::first-letter', '::selection', '::before', '::after',
/* pseudo classes */
':target',
/* CSS2 pseudo elements */
':before', ':after',
/* Vendor-specific pseudo-elements:
* https://developer.mozilla.org/ja/docs/Glossary/Vendor_Prefix
*/
'::?-(?:moz|ms|webkit|o)-[a-z0-9-]+'
],
// Actual regex is of the format: /^(:hover|:focus|...)$/i
pseudosRegex = new RegExp('^(' + ignoredPseudos.join('|') + ')$', 'i');
function transform (selectors) {
selectors.walkPseudos((selector) => {
if (pseudosRegex.test(selector.value)) {
selector.remove();
}
});
}
const processor = postcssSelectorParser(transform);
return function (selector) {
return processor.processSync(selector);
};
}());
/**
* Private function used in filterUnusedRules.
* @param {Array} selectors CSS selectors created by the CSS parser
* @param {Array} ignore List of selectors to be ignored
* @param {Array} usedSelectors List of Selectors found in the jsdom pages
* @return {Array} The selectors matched in the DOMs
*/
function filterUnusedSelectors(selectors, ignore, usedSelectors) {
/* There are some selectors not supported for matching, like
* :before, :after
* They should be removed only if the parent is not found.
* Example: '.clearfix:before' should be removed only if there
* is no '.clearfix'
*/
return selectors.filter((selector) => {
selector = dePseudify(selector);
/* TODO: process @-rules */
if (selector[0] === '@') {
return true;
}
for (let i = 0, len = ignore.length; i < len; ++i) {
if (_.isRegExp(ignore[i]) && ignore[i].test(selector)) {
return true;
}
if (/:\w+/.test(ignore[i])) {
const ignored = dePseudify(ignore[i]);
if (ignored === selector) {
return true;
}
}
if (ignore[i] === selector) {
return true;
}
}
return usedSelectors.indexOf(selector) !== -1;
});
}
/**
* Filter @keyframes that are not used
* @param {Object} css The postcss.Root node
* @param {Array} animations
* @param {Array} unusedRules
* @return {Array}
*/
function filterKeyframes(css, unusedRules) {
const usedAnimations = [];
css.walkDecls((decl) => {
if (_.endsWith(decl.prop, 'animation-name')) {
/* Multiple animations, separated by comma */
usedAnimations.push(...postcss.list.comma(decl.value));
} else if (_.endsWith(decl.prop, 'animation')) {
/* Support multiple animations */
postcss.list.comma(decl.value).forEach((anim) => {
/* If declared as animation, name can be anywhere in the string; so we include all the properties */
usedAnimations.push(...postcss.list.space(anim));
});
}
});
const usedAnimationsSet = new Set(usedAnimations);
css.walkAtRules(/keyframes$/, (atRule) => {
if (!usedAnimationsSet.has(atRule.params)) {
unusedRules.push(atRule);
atRule.remove();
}
});
}
/**
* Filter rules with no selectors remaining
* @param {Object} css The postcss.Root node
* @return {Array}
*/
function filterEmptyAtRules(css) {
/* Filter media queries with no remaining rules */
css.walkAtRules((atRule) => {
if (atRule.name === 'media' && atRule.nodes.length === 0) {
atRule.remove();
}
});
}
/**
* Find which selectors are used in {pages}
* @param {Array} page List of jsdom pages
* @param {Object} css The postcss.Root node
* @return {Promise}
*/
function getUsedSelectors(page, css) {
let usedSelectors = [];
css.walkRules((rule) => {
usedSelectors = _.concat(usedSelectors, rule.selectors.map(dePseudify));
});
return jsdom.findAll(page.window, usedSelectors);
}
/**
* Get all the selectors mentioned in {css}
* @param {Object} css The postcss.Root node
* @return {Array}
*/
function getAllSelectors(css) {
let selectors = [];
css.walkRules((rule) => {
selectors = _.concat(selectors, rule.selector);
});
return selectors;
}
/**
* Remove css rules not used in the dom
* @param {Array} pages List of jsdom pages
* @param {Object} css The postcss.Root node
* @param {Array} ignore List of selectors to be ignored
* @param {Array} usedSelectors List of selectors that are found in {pages}
* @return {Object} A css_parse-compatible stylesheet
*/
function filterUnusedRules(css, ignore, usedSelectors) {
let ignoreNextRule = false,
ignoreNextRulesStart = false,
unusedRules = [],
unusedRuleSelectors,
usedRuleSelectors;
/* Rule format:
* { selectors: [ '...', '...' ],
* declarations: [ { property: '...', value: '...' } ]
* },.
* Two steps: filter the unused selectors for each rule,
* filter the rules with no selectors
*/
ignoreNextRule = false;
css.walk((rule) => {
if (rule.type === 'comment') {
if (/^!?\s?uncss:ignore start\s?$/.test(rule.text)) { // ignore next rules while using comment `/* uncss:ignore start */`
ignoreNextRulesStart = true;
} else if (/^!?\s?uncss:ignore end\s?$/.test(rule.text)) { // until `/* uncss:ignore end */` was found
ignoreNextRulesStart = false;
} else if (/^!?\s?uncss:ignore\s?$/.test(rule.text)) { // ignore next rule while using comment `/* uncss:ignore */`
ignoreNextRule = true;
}
} else if (rule.type === 'rule') {
if (rule.parent.type === 'atrule' && _.endsWith(rule.parent.name, 'keyframes')) {
// Don't remove animation keyframes that have selector names of '30%' or 'to'
return;
}
if (ignoreNextRulesStart) {
ignore = ignore.concat(rule.selectors);
} else if (ignoreNextRule) {
ignoreNextRule = false;
ignore = ignore.concat(rule.selectors);
}
usedRuleSelectors = filterUnusedSelectors(
rule.selectors,
ignore,
usedSelectors
);
unusedRuleSelectors = rule.selectors.filter((selector) => usedRuleSelectors.indexOf(selector) < 0);
if (unusedRuleSelectors && unusedRuleSelectors.length) {
unusedRules.push({
type: 'rule',
selectors: unusedRuleSelectors,
position: rule.source
});
}
if (usedRuleSelectors.length === 0) {
rule.remove();
} else {
rule.selectors = usedRuleSelectors;
}
}
});
/* Filter the @media rules with no rules */
filterEmptyAtRules(css);
/* Filter unused @keyframes */
filterKeyframes(css, unusedRules);
return css;
}
/**
* Main exposed function
* @param {Array} pages List of jsdom pages
* @param {Object} css The postcss.Root node
* @param {Array} ignore List of selectors to be ignored
* @return {Promise}
*/
module.exports = function uncss(pages, css, ignore) {
return Promise.all(pages.map((page) => getUsedSelectors(page, css)))
.then((usedSelectors) => {
usedSelectors = _.flatten(usedSelectors);
const filteredCss = filterUnusedRules(css, ignore, usedSelectors);
const allSelectors = getAllSelectors(css);
return [filteredCss, {
/* Get the selectors for the report */
all: allSelectors,
unused: _.difference(allSelectors, usedSelectors),
used: usedSelectors
}];
});
};
module.exports.dePseudify = dePseudify;

260
BACK_BACK/node_modules/uncss/src/uncss.js generated vendored Executable file
View file

@ -0,0 +1,260 @@
'use strict';
const glob = require('glob'),
isHTML = require('is-html'),
isURL = require('is-absolute-url'),
jsdom = require('./jsdom.js'),
postcss = require('postcss'),
uncss = require('./lib.js'),
utility = require('./utility.js'),
_ = require('lodash');
/**
* Get the contents of HTML pages through jsdom.
* @param {Array} files List of HTML files
* @param {Object} options UnCSS options
* @return {Array|Promise}
*/
function getHTML(files, options) {
if (_.isString(files)) {
files = [files];
}
files = _.flatten(files.map((file) => {
if (!isURL(file) && !isHTML(file)) {
return glob.sync(file);
}
return file;
}));
if (!files.length) {
return Promise.reject(new Error('UnCSS: no HTML files found'));
}
// Save files for later reference.
options.files = files;
return Promise.all(files.map((file) => jsdom.fromSource(file, options)));
}
/**
* Get the contents of CSS files.
* @param {Array} files List of HTML files
* @param {Object} options UnCSS options
* @param {Array} pages Pages opened by jsdom
* @return {Promise}
*/
function getStylesheets(files, options, pages) {
if (options.stylesheets && options.stylesheets.length) {
/* Simulate the behavior below */
return Promise.resolve([files, options, pages, [options.stylesheets]]);
}
/* Extract the stylesheets from the HTML */
return Promise.all(pages.map((page) => jsdom.getStylesheets(page.window, options)))
.then((stylesheets) => [files, options, pages, stylesheets]);
}
/**
* Get the contents of CSS files.
* @param {Array} files List of HTML files
* @param {Object} options UnCSS options
* @param {Array} pages Pages opened by jsdom
* @param {Array} stylesheets List of CSS files
* @return {Array}
*/
function getCSS([files, options, pages, stylesheets]) {
/* Ignore specified stylesheets */
if (options.ignoreSheets.length) {
stylesheets = stylesheets
.map((arr) => {
return arr.filter((sheet) => {
return _.every(options.ignoreSheets, (ignore) => {
if (_.isRegExp(ignore)) {
return !ignore.test(sheet);
}
return sheet !== ignore;
});
});
});
}
if (_.flatten(stylesheets).length) {
/* Only run this if we found links to stylesheets (there may be none...)
* files = ['some_file.html', 'some_other_file.html']
* stylesheets = [['relative_css_path.css', ...],
* ['maybe_a_duplicate.css', ...]]
* We need to - make the stylesheets' paths relative to the HTML files,
* - flatten the array,
* - remove duplicates
*/
stylesheets =
_.chain(stylesheets)
.map((sheets, i) => utility.parsePaths(files[i], sheets, options))
.flatten()
.uniq()
.value();
} else {
/* Reset the array if we didn't find any link tags */
stylesheets = [];
}
return Promise.all([options, pages, utility.readStylesheets(stylesheets, options.banner)]);
}
/**
* Do the actual work
* @param {Array} files List of HTML files
* @param {Object} options UnCSS options
* @param {Array} pages Pages opened by jsdom
* @param {Array} stylesheets List of CSS files
* @return {Promise}
*/
function processWithTextApi([options, pages, stylesheets]) {
/* If we specified a raw string of CSS, add it to the stylesheets array */
if (options.raw) {
if (_.isString(options.raw)) {
stylesheets.push(options.raw);
} else {
throw new Error('UnCSS: options.raw - expected a string');
}
}
/* At this point, there isn't any point in running the rest of the task if:
* - We didn't specify any stylesheet links in the options object
* - We couldn't find any stylesheet links in the HTML itself
* - We weren't passed a string of raw CSS in addition to, or to replace
* either of the above
*/
if (!_.flatten(stylesheets).length) {
throw new Error('UnCSS: no stylesheets found');
}
/* OK, so we have some CSS to work with!
* Three steps:
* - Parse the CSS
* - Remove the unused rules
* - Return the optimized CSS as a string
*/
const cssStr = stylesheets.join(' \n');
let pcss,
report;
try {
pcss = postcss.parse(cssStr);
} catch (err) {
/* Try and construct a helpful error message */
throw utility.parseErrorMessage(err, cssStr);
}
return uncss(pages, pcss, options.ignore).then(([css, rep]) => {
let newCssStr = '';
postcss.stringify(css, (result) => {
newCssStr += result;
});
if (options.report) {
report = {
original: cssStr,
selectors: rep
};
}
return [newCssStr, report];
});
}
/**
* Main exposed function.
* Here we check the options and callback, then run the files through jsdom.
* @param {Array} files Array of filenames
* @param {Object} [options] options
* @param {Function} callback(Error, String, Object)
*/
function init(files, options, callback) {
if (_.isFunction(options)) {
/* There were no options, this argument is actually the callback */
callback = options;
options = {};
} else if (!_.isFunction(callback)) {
throw new TypeError('UnCSS: expected a callback');
}
/* Try and read options from the specified uncssrc file */
if (options.uncssrc) {
try {
/* Manually-specified options take precedence over uncssrc options */
options = _.merge(utility.parseUncssrc(options.uncssrc), options);
} catch (err) {
if (err instanceof SyntaxError) {
callback(new SyntaxError('UnCSS: uncssrc file is invalid JSON.'));
return;
}
callback(err);
return;
}
}
/* Assign default values to options, unless specified */
options = _.merge({
banner: true,
csspath: '',
html: files,
htmlRoot: null,
ignore: [],
ignoreSheets: [],
inject: null,
jsdom: jsdom.defaultOptions(),
media: [],
raw: null,
report: false,
stylesheets: null,
timeout: 0,
uncssrc: null,
userAgent: 'uncss'
}, options);
process(options).then(([css, report]) => callback(null, css, report), callback);
}
function processAsPostCss(options, pages) {
return uncss(pages, options.rawPostCss, options.ignore);
}
function process(opts) {
return getHTML(opts.html, opts).then((pages) => {
function cleanup (result) {
pages.forEach((page) => page.window.close());
return result;
}
if (opts.usePostCssInternal) {
return processAsPostCss(opts, pages)
.then(cleanup);
}
return getStylesheets(opts.files, opts, pages)
.then(getCSS)
.then(processWithTextApi)
.then(cleanup);
});
}
const postcssPlugin = postcss.plugin('uncss', (opts) => {
let options = _.merge({
usePostCssInternal: true,
// Ignore stylesheets in the HTML files; only use those from the stream
ignoreSheets: [/\s*/],
html: [],
ignore: [],
jsdom: jsdom.defaultOptions()
}, opts);
return function (css, result) { // eslint-disable-line no-unused-vars
options = _.merge(options, {
// This is used to pass the css object in to processAsPostCSS
rawPostCss: css
});
return process(options);
};
});
module.exports = init;
module.exports.postcssPlugin = postcssPlugin;

205
BACK_BACK/node_modules/uncss/src/utility.js generated vendored Executable file
View file

@ -0,0 +1,205 @@
'use strict';
const isHTML = require('is-html'),
isURL = require('is-absolute-url'),
request = require('request'),
fs = require('fs'),
os = require('os'),
path = require('path'),
url = require('url');
function isWindows() {
return os.platform() === 'win32';
}
/**
* Check if the supplied string might be a RegExp and, if so, return the corresponding RegExp.
* @param {String} str The regex to transform.
* @return {RegExp|String} The final RegExp
*/
function strToRegExp(str) {
if (str[0] === '/') {
return new RegExp(str.replace(/^\/|\/$/g, ''));
}
return str;
}
/**
* Parse a given uncssrc file.
* @param {String} filename The location of the uncssrc file
* @return {Object} The options object
*/
function parseUncssrc(filename) {
let options = JSON.parse(fs.readFileSync(filename, 'utf-8'));
/* RegExps can't be stored as JSON, therefore we need to parse them manually.
* A string is a RegExp if it starts with '/', since that wouldn't be a valid CSS selector.
*/
options.ignore = options.ignore ? options.ignore.map(strToRegExp) : undefined;
options.ignoreSheets = options.ignoreSheets ? options.ignoreSheets.map(strToRegExp) : [];
return options;
}
/**
* Parse paths relatives to a source.
* @param {String} source Where the paths originate from
* @param {Array} stylesheets List of paths
* @param {Object} options Options, as passed to UnCSS
* @return {Array} List of paths
*/
function parsePaths(source, stylesheets, options) {
return stylesheets.map((sheet) => {
let sourceProtocol;
const isLocalFile = sheet.substr(0, 5) === 'file:';
if (sheet.substr(0, 4) === 'http') {
/* No need to parse, it's already a valid path */
return sheet;
}
/* Check if we are fetching over http(s) */
if (isURL(source) && !isLocalFile) {
sourceProtocol = url.parse(source).protocol;
if (sheet.substr(0, 2) === '//') {
/* Use the same protocol we used for fetching this page.
* Default to http.
*/
return sourceProtocol ? sourceProtocol + sheet : 'http:' + sheet;
}
return url.resolve(source, sheet);
}
/* We are fetching local files
* Should probably report an error if we find an absolute path and
* have no htmlroot specified.
*/
/* Fix the case when there is a query string or hash */
sheet = sheet.split('?')[0].split('#')[0];
/* Path already parsed by jsdom or user supplied local file */
if (isLocalFile) {
sheet = url.parse(sheet).path.replace('%20', ' ');
/* If on windows, remove first '/' */
sheet = isWindows() ? sheet.substring(1) : sheet;
if (options.htmlroot) {
return path.join(options.htmlroot, sheet);
}
sheet = path.relative(path.join(path.dirname(source)), sheet);
}
if (sheet[0] === '/' && options.htmlroot) {
return path.join(options.htmlroot, sheet);
} else if (isHTML(source)) {
return path.join(options.csspath, sheet);
}
return path.join(path.dirname(source), options.csspath, sheet);
});
}
/**
* Given an UTF8 string, return a version of this string without the first byte if it corresponds
* to the Byte Order Mark
* @param {String} utf8String the string to strip
* @return {String}
*/
function stripBom(utf8String) {
if (utf8String.charCodeAt(0) === 0xFEFF) {
return utf8String.substr(1);
}
return utf8String;
}
/**
* Given an array of filenames, return an array of the files' contents,
* only if the filename matches a regex
* @param {Array} files An array of the filenames to read
* @return {Promise}
*/
function readStylesheets(files, outputBanner) {
return Promise.all(files.map((filename) => {
if (isURL(filename)) {
return new Promise((resolve, reject) => {
request({
url: filename,
headers: { 'User-Agent': 'UnCSS' }
}, (err, response, body) => {
if (err) {
return reject(err);
}
return resolve(body);
});
});
} else if (fs.existsSync(filename)) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf-8', (err, contents) => {
if (err) {
return reject(err);
}
return resolve(stripBom(contents));
});
});
}
throw new Error(`UnCSS: could not open ${path.join(process.cwd(), filename)}`);
})).then((res) => {
// res is an array of the content of each file in files (in the same order)
if (outputBanner) {
for (let i = 0, len = files.length; i < len; i++) {
// We append a small banner to keep track of which file we are currently processing
// super helpful for debugging
const banner = `/*** uncss> filename: ${files[i].replace(/\\/g, '/')} ***/\n`;
res[i] = banner + res[i];
}
}
return res;
});
}
function parseErrorMessage(error, cssStr) {
/* TODO: FIXME */
/* Base line for conveying the line number in the error message */
let zeroLine = 0;
if (error.line) {
const lines = cssStr.split('\n');
if (lines.length) {
/* We get the filename of the css file that contains the error */
let i = error.line - 1;
while (i >= 0 && !error.filename) {
if (lines[i].substr(0, 21) === '/*** uncss> filename:') {
error.filename = lines[i].substring(22, lines[i].length - 4);
zeroLine = i;
}
i--;
}
for (let j = error.line - 6; j < error.line + 5; j++) {
if (j - zeroLine < 0 || j >= lines.length) {
continue;
}
let line = lines[j];
/* It could be minified CSS */
if (line.length > 120 && error.column) {
line = line.substring(error.column - 40, error.column);
}
error.message += '\n\t' + (j + 1 - zeroLine) + ': ';
error.message += j === error.line - 1 ? ' -> ' : ' ';
error.message += line;
}
}
}
if (zeroLine > 0) {
error.message = error.message.replace(/[0-9]+:/, error.line - zeroLine + ':');
}
error.message = `uncss/node_modules/css: unable to parse ${error.filename}:\n${error.message}\n`;
return error;
}
module.exports = {
isWindows,
parseUncssrc,
parseErrorMessage,
parsePaths,
readStylesheets
};