It appears you have a well-structured Git repository with various files, including SVG icons and HTML documents. Here's a brief overview:

This commit is contained in:
2025-06-11 09:05:15 +02:00
parent 36c2466e53
commit 6d6aa954dd
15556 changed files with 1076330 additions and 1 deletions

57
backend/node_modules/svgo/lib/builtin.js generated vendored Normal file
View File

@@ -0,0 +1,57 @@
'use strict';
exports.builtin = [
require('../plugins/preset-default.js'),
require('../plugins/addAttributesToSVGElement.js'),
require('../plugins/addClassesToSVGElement.js'),
require('../plugins/cleanupAttrs.js'),
require('../plugins/cleanupEnableBackground.js'),
require('../plugins/cleanupIds.js'),
require('../plugins/cleanupListOfValues.js'),
require('../plugins/cleanupNumericValues.js'),
require('../plugins/collapseGroups.js'),
require('../plugins/convertColors.js'),
require('../plugins/convertEllipseToCircle.js'),
require('../plugins/convertOneStopGradients.js'),
require('../plugins/convertPathData.js'),
require('../plugins/convertShapeToPath.js'),
require('../plugins/convertStyleToAttrs.js'),
require('../plugins/convertTransform.js'),
require('../plugins/mergeStyles.js'),
require('../plugins/inlineStyles.js'),
require('../plugins/mergePaths.js'),
require('../plugins/minifyStyles.js'),
require('../plugins/moveElemsAttrsToGroup.js'),
require('../plugins/moveGroupAttrsToElems.js'),
require('../plugins/prefixIds.js'),
require('../plugins/removeAttributesBySelector.js'),
require('../plugins/removeAttrs.js'),
require('../plugins/removeComments.js'),
require('../plugins/removeDesc.js'),
require('../plugins/removeDimensions.js'),
require('../plugins/removeDoctype.js'),
require('../plugins/removeEditorsNSData.js'),
require('../plugins/removeElementsByAttr.js'),
require('../plugins/removeEmptyAttrs.js'),
require('../plugins/removeEmptyContainers.js'),
require('../plugins/removeEmptyText.js'),
require('../plugins/removeHiddenElems.js'),
require('../plugins/removeMetadata.js'),
require('../plugins/removeNonInheritableGroupAttrs.js'),
require('../plugins/removeOffCanvasPaths.js'),
require('../plugins/removeRasterImages.js'),
require('../plugins/removeScriptElement.js'),
require('../plugins/removeStyleElement.js'),
require('../plugins/removeTitle.js'),
require('../plugins/removeUnknownsAndDefaults.js'),
require('../plugins/removeUnusedNS.js'),
require('../plugins/removeUselessDefs.js'),
require('../plugins/removeUselessStrokeAndFill.js'),
require('../plugins/removeViewBox.js'),
require('../plugins/removeXlink.js'),
require('../plugins/removeXMLNS.js'),
require('../plugins/removeXMLProcInst.js'),
require('../plugins/reusePaths.js'),
require('../plugins/sortAttrs.js'),
require('../plugins/sortDefsChildren.js'),
];

262
backend/node_modules/svgo/lib/parser.js generated vendored Normal file
View File

@@ -0,0 +1,262 @@
'use strict';
/**
* @typedef {import('./types').XastNode} XastNode
* @typedef {import('./types').XastInstruction} XastInstruction
* @typedef {import('./types').XastDoctype} XastDoctype
* @typedef {import('./types').XastComment} XastComment
* @typedef {import('./types').XastRoot} XastRoot
* @typedef {import('./types').XastElement} XastElement
* @typedef {import('./types').XastCdata} XastCdata
* @typedef {import('./types').XastText} XastText
* @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').XastChild} XastChild
*/
// @ts-ignore sax will be replaced with something else later
const SAX = require('@trysound/sax');
const { textElems } = require('../plugins/_collections');
class SvgoParserError extends Error {
/**
* @param message {string}
* @param line {number}
* @param column {number}
* @param source {string}
* @param file {void | string}
*/
constructor(message, line, column, source, file) {
super(message);
this.name = 'SvgoParserError';
this.message = `${file || '<input>'}:${line}:${column}: ${message}`;
this.reason = message;
this.line = line;
this.column = column;
this.source = source;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, SvgoParserError);
}
}
toString() {
const lines = this.source.split(/\r?\n/);
const startLine = Math.max(this.line - 3, 0);
const endLine = Math.min(this.line + 2, lines.length);
const lineNumberWidth = String(endLine).length;
const startColumn = Math.max(this.column - 54, 0);
const endColumn = Math.max(this.column + 20, 80);
const code = lines
.slice(startLine, endLine)
.map((line, index) => {
const lineSlice = line.slice(startColumn, endColumn);
let ellipsisPrefix = '';
let ellipsisSuffix = '';
if (startColumn !== 0) {
ellipsisPrefix = startColumn > line.length - 1 ? ' ' : '…';
}
if (endColumn < line.length - 1) {
ellipsisSuffix = '…';
}
const number = startLine + 1 + index;
const gutter = ` ${number.toString().padStart(lineNumberWidth)} | `;
if (number === this.line) {
const gutterSpacing = gutter.replace(/[^|]/g, ' ');
const lineSpacing = (
ellipsisPrefix + line.slice(startColumn, this.column - 1)
).replace(/[^\t]/g, ' ');
const spacing = gutterSpacing + lineSpacing;
return `>${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}\n ${spacing}^`;
}
return ` ${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}`;
})
.join('\n');
return `${this.name}: ${this.message}\n\n${code}\n`;
}
}
const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^']+)'|"([^"]+)")\s*>/g;
const config = {
strict: true,
trim: false,
normalize: false,
lowercase: true,
xmlns: true,
position: true,
};
/**
* Convert SVG (XML) string to SVG-as-JS object.
*
* @type {(data: string, from?: string) => XastRoot}
*/
const parseSvg = (data, from) => {
const sax = SAX.parser(config.strict, config);
/**
* @type {XastRoot}
*/
const root = { type: 'root', children: [] };
/**
* @type {XastParent}
*/
let current = root;
/**
* @type {XastParent[]}
*/
const stack = [root];
/**
* @type {(node: XastChild) => void}
*/
const pushToContent = (node) => {
// TODO remove legacy parentNode in v4
Object.defineProperty(node, 'parentNode', {
writable: true,
value: current,
});
current.children.push(node);
};
/**
* @type {(doctype: string) => void}
*/
sax.ondoctype = (doctype) => {
/**
* @type {XastDoctype}
*/
const node = {
type: 'doctype',
// TODO parse doctype for name, public and system to match xast
name: 'svg',
data: {
doctype,
},
};
pushToContent(node);
const subsetStart = doctype.indexOf('[');
if (subsetStart >= 0) {
entityDeclaration.lastIndex = subsetStart;
let entityMatch = entityDeclaration.exec(data);
while (entityMatch != null) {
sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3];
entityMatch = entityDeclaration.exec(data);
}
}
};
/**
* @type {(data: { name: string, body: string }) => void}
*/
sax.onprocessinginstruction = (data) => {
/**
* @type {XastInstruction}
*/
const node = {
type: 'instruction',
name: data.name,
value: data.body,
};
pushToContent(node);
};
/**
* @type {(comment: string) => void}
*/
sax.oncomment = (comment) => {
/**
* @type {XastComment}
*/
const node = {
type: 'comment',
value: comment.trim(),
};
pushToContent(node);
};
/**
* @type {(cdata: string) => void}
*/
sax.oncdata = (cdata) => {
/**
* @type {XastCdata}
*/
const node = {
type: 'cdata',
value: cdata,
};
pushToContent(node);
};
/**
* @type {(data: { name: string, attributes: Record<string, { value: string }>}) => void}
*/
sax.onopentag = (data) => {
/**
* @type {XastElement}
*/
let element = {
type: 'element',
name: data.name,
attributes: {},
children: [],
};
for (const [name, attr] of Object.entries(data.attributes)) {
element.attributes[name] = attr.value;
}
pushToContent(element);
current = element;
stack.push(element);
};
/**
* @type {(text: string) => void}
*/
sax.ontext = (text) => {
if (current.type === 'element') {
// prevent trimming of meaningful whitespace inside textual tags
if (textElems.has(current.name)) {
/**
* @type {XastText}
*/
const node = {
type: 'text',
value: text,
};
pushToContent(node);
} else if (/\S/.test(text)) {
/**
* @type {XastText}
*/
const node = {
type: 'text',
value: text.trim(),
};
pushToContent(node);
}
}
};
sax.onclosetag = () => {
stack.pop();
current = stack[stack.length - 1];
};
/**
* @type {(e: any) => void}
*/
sax.onerror = (e) => {
const error = new SvgoParserError(
e.reason,
e.line + 1,
e.column,
data,
from,
);
if (e.message.indexOf('Unexpected end') === -1) {
throw error;
}
};
sax.write(data).close();
return root;
};
exports.parseSvg = parseSvg;

380
backend/node_modules/svgo/lib/path.js generated vendored Normal file
View File

@@ -0,0 +1,380 @@
'use strict';
const { removeLeadingZero, toFixed } = require('./svgo/tools');
/**
* @typedef {import('./types').PathDataItem} PathDataItem
* @typedef {import('./types').PathDataCommand} PathDataCommand
*/
// Based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF
const argsCountPerCommand = {
M: 2,
m: 2,
Z: 0,
z: 0,
L: 2,
l: 2,
H: 1,
h: 1,
V: 1,
v: 1,
C: 6,
c: 6,
S: 4,
s: 4,
Q: 4,
q: 4,
T: 2,
t: 2,
A: 7,
a: 7,
};
/**
* @type {(c: string) => c is PathDataCommand}
*/
const isCommand = (c) => {
return c in argsCountPerCommand;
};
/**
* @type {(c: string) => boolean}
*/
const isWsp = (c) => {
const codePoint = c.codePointAt(0);
return (
codePoint === 0x20 ||
codePoint === 0x9 ||
codePoint === 0xd ||
codePoint === 0xa
);
};
/**
* @type {(c: string) => boolean}
*/
const isDigit = (c) => {
const codePoint = c.codePointAt(0);
if (codePoint == null) {
return false;
}
return 48 <= codePoint && codePoint <= 57;
};
/**
* @typedef {'none' | 'sign' | 'whole' | 'decimal_point' | 'decimal' | 'e' | 'exponent_sign' | 'exponent'} ReadNumberState
*/
/**
* @type {(string: string, cursor: number) => [number, ?number]}
*/
const readNumber = (string, cursor) => {
let i = cursor;
let value = '';
let state = /** @type {ReadNumberState} */ ('none');
for (; i < string.length; i += 1) {
const c = string[i];
if (c === '+' || c === '-') {
if (state === 'none') {
state = 'sign';
value += c;
continue;
}
if (state === 'e') {
state = 'exponent_sign';
value += c;
continue;
}
}
if (isDigit(c)) {
if (state === 'none' || state === 'sign' || state === 'whole') {
state = 'whole';
value += c;
continue;
}
if (state === 'decimal_point' || state === 'decimal') {
state = 'decimal';
value += c;
continue;
}
if (state === 'e' || state === 'exponent_sign' || state === 'exponent') {
state = 'exponent';
value += c;
continue;
}
}
if (c === '.') {
if (state === 'none' || state === 'sign' || state === 'whole') {
state = 'decimal_point';
value += c;
continue;
}
}
if (c === 'E' || c == 'e') {
if (
state === 'whole' ||
state === 'decimal_point' ||
state === 'decimal'
) {
state = 'e';
value += c;
continue;
}
}
break;
}
const number = Number.parseFloat(value);
if (Number.isNaN(number)) {
return [cursor, null];
} else {
// step back to delegate iteration to parent loop
return [i - 1, number];
}
};
/**
* @type {(string: string) => PathDataItem[]}
*/
const parsePathData = (string) => {
/**
* @type {PathDataItem[]}
*/
const pathData = [];
/**
* @type {?PathDataCommand}
*/
let command = null;
let args = /** @type {number[]} */ ([]);
let argsCount = 0;
let canHaveComma = false;
let hadComma = false;
for (let i = 0; i < string.length; i += 1) {
const c = string.charAt(i);
if (isWsp(c)) {
continue;
}
// allow comma only between arguments
if (canHaveComma && c === ',') {
if (hadComma) {
break;
}
hadComma = true;
continue;
}
if (isCommand(c)) {
if (hadComma) {
return pathData;
}
if (command == null) {
// moveto should be leading command
if (c !== 'M' && c !== 'm') {
return pathData;
}
} else {
// stop if previous command arguments are not flushed
if (args.length !== 0) {
return pathData;
}
}
command = c;
args = [];
argsCount = argsCountPerCommand[command];
canHaveComma = false;
// flush command without arguments
if (argsCount === 0) {
pathData.push({ command, args });
}
continue;
}
// avoid parsing arguments if no command detected
if (command == null) {
return pathData;
}
// read next argument
let newCursor = i;
let number = null;
if (command === 'A' || command === 'a') {
const position = args.length;
if (position === 0 || position === 1) {
// allow only positive number without sign as first two arguments
if (c !== '+' && c !== '-') {
[newCursor, number] = readNumber(string, i);
}
}
if (position === 2 || position === 5 || position === 6) {
[newCursor, number] = readNumber(string, i);
}
if (position === 3 || position === 4) {
// read flags
if (c === '0') {
number = 0;
}
if (c === '1') {
number = 1;
}
}
} else {
[newCursor, number] = readNumber(string, i);
}
if (number == null) {
return pathData;
}
args.push(number);
canHaveComma = true;
hadComma = false;
i = newCursor;
// flush arguments when necessary count is reached
if (args.length === argsCount) {
pathData.push({ command, args });
// subsequent moveto coordinates are treated as implicit lineto commands
if (command === 'M') {
command = 'L';
}
if (command === 'm') {
command = 'l';
}
args = [];
}
}
return pathData;
};
exports.parsePathData = parsePathData;
/**
* @type {(number: number, precision?: number) => {
* roundedStr: string,
* rounded: number
* }}
*/
const roundAndStringify = (number, precision) => {
if (precision != null) {
number = toFixed(number, precision);
}
return {
roundedStr: removeLeadingZero(number),
rounded: number,
};
};
/**
* Elliptical arc large-arc and sweep flags are rendered with spaces
* because many non-browser environments are not able to parse such paths
*
* @type {(
* command: string,
* args: number[],
* precision?: number,
* disableSpaceAfterFlags?: boolean
* ) => string}
*/
const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => {
let result = '';
let previous;
for (let i = 0; i < args.length; i++) {
const { roundedStr, rounded } = roundAndStringify(args[i], precision);
if (
disableSpaceAfterFlags &&
(command === 'A' || command === 'a') &&
// consider combined arcs
(i % 7 === 4 || i % 7 === 5)
) {
result += roundedStr;
} else if (i === 0 || rounded < 0) {
// avoid space before first and negative numbers
result += roundedStr;
} else if (
!Number.isInteger(previous) &&
rounded != 0 &&
rounded < 1 &&
rounded > -1
) {
// remove space before decimal with zero whole
// only when previous number is also decimal
result += roundedStr;
} else {
result += ` ${roundedStr}`;
}
previous = rounded;
}
return result;
};
/**
* @typedef {{
* pathData: PathDataItem[];
* precision?: number;
* disableSpaceAfterFlags?: boolean;
* }} StringifyPathDataOptions
*/
/**
* @param {StringifyPathDataOptions} options
* @returns {string}
*/
const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
if (pathData.length === 1) {
const { command, args } = pathData[0];
return (
command + stringifyArgs(command, args, precision, disableSpaceAfterFlags)
);
}
let result = '';
let prev = { ...pathData[0] };
// match leading moveto with following lineto
if (pathData[1].command === 'L') {
prev.command = 'M';
} else if (pathData[1].command === 'l') {
prev.command = 'm';
}
for (let i = 1; i < pathData.length; i++) {
const { command, args } = pathData[i];
if (
(prev.command === command &&
prev.command !== 'M' &&
prev.command !== 'm') ||
// combine matching moveto and lineto sequences
(prev.command === 'M' && command === 'L') ||
(prev.command === 'm' && command === 'l')
) {
prev.args = [...prev.args, ...args];
if (i === pathData.length - 1) {
result +=
prev.command +
stringifyArgs(
prev.command,
prev.args,
precision,
disableSpaceAfterFlags,
);
}
} else {
result +=
prev.command +
stringifyArgs(
prev.command,
prev.args,
precision,
disableSpaceAfterFlags,
);
if (i === pathData.length - 1) {
result +=
command +
stringifyArgs(command, args, precision, disableSpaceAfterFlags);
} else {
prev = { command, args };
}
}
}
return result;
};
exports.stringifyPathData = stringifyPathData;

295
backend/node_modules/svgo/lib/stringifier.js generated vendored Normal file
View File

@@ -0,0 +1,295 @@
'use strict';
/**
* @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').XastRoot} XastRoot
* @typedef {import('./types').XastElement} XastElement
* @typedef {import('./types').XastInstruction} XastInstruction
* @typedef {import('./types').XastDoctype} XastDoctype
* @typedef {import('./types').XastText} XastText
* @typedef {import('./types').XastCdata} XastCdata
* @typedef {import('./types').XastComment} XastComment
* @typedef {import('./types').StringifyOptions} StringifyOptions
*/
const { textElems } = require('../plugins/_collections');
/**
* @typedef {{
* indent: string,
* textContext: ?XastElement,
* indentLevel: number,
* }} State
*/
/**
* @typedef {Required<StringifyOptions>} Options
*/
/**
* @type {(char: string) => string}
*/
const encodeEntity = (char) => {
return entities[char];
};
/** @type {Options} */
const defaults = {
doctypeStart: '<!DOCTYPE',
doctypeEnd: '>',
procInstStart: '<?',
procInstEnd: '?>',
tagOpenStart: '<',
tagOpenEnd: '>',
tagCloseStart: '</',
tagCloseEnd: '>',
tagShortStart: '<',
tagShortEnd: '/>',
attrStart: '="',
attrEnd: '"',
commentStart: '<!--',
commentEnd: '-->',
cdataStart: '<![CDATA[',
cdataEnd: ']]>',
textStart: '',
textEnd: '',
indent: 4,
regEntities: /[&'"<>]/g,
regValEntities: /[&"<>]/g,
encodeEntity,
pretty: false,
useShortTags: true,
eol: 'lf',
finalNewline: false,
};
/** @type {Record<string, string>} */
const entities = {
'&': '&amp;',
"'": '&apos;',
'"': '&quot;',
'>': '&gt;',
'<': '&lt;',
};
/**
* convert XAST to SVG string
*
* @type {(data: XastRoot, config: StringifyOptions) => string}
*/
const stringifySvg = (data, userOptions = {}) => {
/**
* @type {Options}
*/
const config = { ...defaults, ...userOptions };
const indent = config.indent;
let newIndent = ' ';
if (typeof indent === 'number' && Number.isNaN(indent) === false) {
newIndent = indent < 0 ? '\t' : ' '.repeat(indent);
} else if (typeof indent === 'string') {
newIndent = indent;
}
/**
* @type {State}
*/
const state = {
indent: newIndent,
textContext: null,
indentLevel: 0,
};
const eol = config.eol === 'crlf' ? '\r\n' : '\n';
if (config.pretty) {
config.doctypeEnd += eol;
config.procInstEnd += eol;
config.commentEnd += eol;
config.cdataEnd += eol;
config.tagShortEnd += eol;
config.tagOpenEnd += eol;
config.tagCloseEnd += eol;
config.textEnd += eol;
}
let svg = stringifyNode(data, config, state);
if (config.finalNewline && svg.length > 0 && !svg.endsWith('\n')) {
svg += eol;
}
return svg;
};
exports.stringifySvg = stringifySvg;
/**
* @type {(node: XastParent, config: Options, state: State) => string}
*/
const stringifyNode = (data, config, state) => {
let svg = '';
state.indentLevel += 1;
for (const item of data.children) {
if (item.type === 'element') {
svg += stringifyElement(item, config, state);
}
if (item.type === 'text') {
svg += stringifyText(item, config, state);
}
if (item.type === 'doctype') {
svg += stringifyDoctype(item, config);
}
if (item.type === 'instruction') {
svg += stringifyInstruction(item, config);
}
if (item.type === 'comment') {
svg += stringifyComment(item, config);
}
if (item.type === 'cdata') {
svg += stringifyCdata(item, config, state);
}
}
state.indentLevel -= 1;
return svg;
};
/**
* create indent string in accordance with the current node level.
*
* @type {(config: Options, state: State) => string}
*/
const createIndent = (config, state) => {
let indent = '';
if (config.pretty && state.textContext == null) {
indent = state.indent.repeat(state.indentLevel - 1);
}
return indent;
};
/**
* @type {(node: XastDoctype, config: Options) => string}
*/
const stringifyDoctype = (node, config) => {
return config.doctypeStart + node.data.doctype + config.doctypeEnd;
};
/**
* @type {(node: XastInstruction, config: Options) => string}
*/
const stringifyInstruction = (node, config) => {
return (
config.procInstStart + node.name + ' ' + node.value + config.procInstEnd
);
};
/**
* @type {(node: XastComment, config: Options) => string}
*/
const stringifyComment = (node, config) => {
return config.commentStart + node.value + config.commentEnd;
};
/**
* @type {(node: XastCdata, config: Options, state: State) => string}
*/
const stringifyCdata = (node, config, state) => {
return (
createIndent(config, state) +
config.cdataStart +
node.value +
config.cdataEnd
);
};
/**
* @type {(node: XastElement, config: Options, state: State) => string}
*/
const stringifyElement = (node, config, state) => {
// empty element and short tag
if (node.children.length === 0) {
if (config.useShortTags) {
return (
createIndent(config, state) +
config.tagShortStart +
node.name +
stringifyAttributes(node, config) +
config.tagShortEnd
);
} else {
return (
createIndent(config, state) +
config.tagShortStart +
node.name +
stringifyAttributes(node, config) +
config.tagOpenEnd +
config.tagCloseStart +
node.name +
config.tagCloseEnd
);
}
// non-empty element
} else {
let tagOpenStart = config.tagOpenStart;
let tagOpenEnd = config.tagOpenEnd;
let tagCloseStart = config.tagCloseStart;
let tagCloseEnd = config.tagCloseEnd;
let openIndent = createIndent(config, state);
let closeIndent = createIndent(config, state);
if (state.textContext) {
tagOpenStart = defaults.tagOpenStart;
tagOpenEnd = defaults.tagOpenEnd;
tagCloseStart = defaults.tagCloseStart;
tagCloseEnd = defaults.tagCloseEnd;
openIndent = '';
} else if (textElems.has(node.name)) {
tagOpenEnd = defaults.tagOpenEnd;
tagCloseStart = defaults.tagCloseStart;
closeIndent = '';
state.textContext = node;
}
const children = stringifyNode(node, config, state);
if (state.textContext === node) {
state.textContext = null;
}
return (
openIndent +
tagOpenStart +
node.name +
stringifyAttributes(node, config) +
tagOpenEnd +
children +
closeIndent +
tagCloseStart +
node.name +
tagCloseEnd
);
}
};
/**
* @type {(node: XastElement, config: Options) => string}
*/
const stringifyAttributes = (node, config) => {
let attrs = '';
for (const [name, value] of Object.entries(node.attributes)) {
// TODO remove attributes without values support in v3
if (value !== undefined) {
const encodedValue = value
.toString()
.replace(config.regValEntities, config.encodeEntity);
attrs += ' ' + name + config.attrStart + encodedValue + config.attrEnd;
} else {
attrs += ' ' + name;
}
}
return attrs;
};
/**
* @type {(node: XastText, config: Options, state: State) => string}
*/
const stringifyText = (node, config, state) => {
return (
createIndent(config, state) +
config.textStart +
node.value.replace(config.regEntities, config.encodeEntity) +
(state.textContext ? '' : config.textEnd)
);
};

336
backend/node_modules/svgo/lib/style.js generated vendored Normal file
View File

@@ -0,0 +1,336 @@
'use strict';
/**
* @typedef {import('css-tree').Rule} CsstreeRule
* @typedef {import('./types').Specificity} Specificity
* @typedef {import('./types').Stylesheet} Stylesheet
* @typedef {import('./types').StylesheetRule} StylesheetRule
* @typedef {import('./types').StylesheetDeclaration} StylesheetDeclaration
* @typedef {import('./types').ComputedStyles} ComputedStyles
* @typedef {import('./types').XastRoot} XastRoot
* @typedef {import('./types').XastElement} XastElement
* @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').XastChild} XastChild
*/
const csstree = require('css-tree');
const csswhat = require('css-what');
const {
syntax: { specificity },
} = require('csso');
const { visit, matches } = require('./xast.js');
const {
attrsGroups,
inheritableAttrs,
presentationNonInheritableGroupAttrs,
} = require('../plugins/_collections.js');
// @ts-ignore not defined in @types/csstree
const csstreeWalkSkip = csstree.walk.skip;
/**
* @type {(ruleNode: CsstreeRule, dynamic: boolean) => StylesheetRule[]}
*/
const parseRule = (ruleNode, dynamic) => {
/**
* @type {StylesheetDeclaration[]}
*/
const declarations = [];
// collect declarations
ruleNode.block.children.forEach((cssNode) => {
if (cssNode.type === 'Declaration') {
declarations.push({
name: cssNode.property,
value: csstree.generate(cssNode.value),
important: cssNode.important === true,
});
}
});
/** @type {StylesheetRule[]} */
const rules = [];
csstree.walk(ruleNode.prelude, (node) => {
if (node.type === 'Selector') {
const newNode = csstree.clone(node);
let hasPseudoClasses = false;
csstree.walk(newNode, (pseudoClassNode, item, list) => {
if (pseudoClassNode.type === 'PseudoClassSelector') {
hasPseudoClasses = true;
list.remove(item);
}
});
rules.push({
specificity: specificity(node),
dynamic: hasPseudoClasses || dynamic,
// compute specificity from original node to consider pseudo classes
selector: csstree.generate(newNode),
declarations,
});
}
});
return rules;
};
/**
* @type {(css: string, dynamic: boolean) => StylesheetRule[]}
*/
const parseStylesheet = (css, dynamic) => {
/** @type {StylesheetRule[]} */
const rules = [];
const ast = csstree.parse(css, {
parseValue: false,
parseAtrulePrelude: false,
});
csstree.walk(ast, (cssNode) => {
if (cssNode.type === 'Rule') {
rules.push(...parseRule(cssNode, dynamic || false));
return csstreeWalkSkip;
}
if (cssNode.type === 'Atrule') {
if (
cssNode.name === 'keyframes' ||
cssNode.name === '-webkit-keyframes'
) {
return csstreeWalkSkip;
}
csstree.walk(cssNode, (ruleNode) => {
if (ruleNode.type === 'Rule') {
rules.push(...parseRule(ruleNode, dynamic || true));
return csstreeWalkSkip;
}
});
return csstreeWalkSkip;
}
});
return rules;
};
/**
* @type {(css: string) => StylesheetDeclaration[]}
*/
const parseStyleDeclarations = (css) => {
/** @type {StylesheetDeclaration[]} */
const declarations = [];
const ast = csstree.parse(css, {
context: 'declarationList',
parseValue: false,
});
csstree.walk(ast, (cssNode) => {
if (cssNode.type === 'Declaration') {
declarations.push({
name: cssNode.property,
value: csstree.generate(cssNode.value),
important: cssNode.important === true,
});
}
});
return declarations;
};
/**
* @param {Stylesheet} stylesheet
* @param {XastElement} node
* @returns {ComputedStyles}
*/
const computeOwnStyle = (stylesheet, node) => {
/** @type {ComputedStyles} */
const computedStyle = {};
const importantStyles = new Map();
// collect attributes
for (const [name, value] of Object.entries(node.attributes)) {
if (attrsGroups.presentation.has(name)) {
computedStyle[name] = { type: 'static', inherited: false, value };
importantStyles.set(name, false);
}
}
// collect matching rules
for (const { selector, declarations, dynamic } of stylesheet.rules) {
if (matches(node, selector)) {
for (const { name, value, important } of declarations) {
const computed = computedStyle[name];
if (computed && computed.type === 'dynamic') {
continue;
}
if (dynamic) {
computedStyle[name] = { type: 'dynamic', inherited: false };
continue;
}
if (
computed == null ||
important === true ||
importantStyles.get(name) === false
) {
computedStyle[name] = { type: 'static', inherited: false, value };
importantStyles.set(name, important);
}
}
}
}
// collect inline styles
const styleDeclarations =
node.attributes.style == null
? []
: parseStyleDeclarations(node.attributes.style);
for (const { name, value, important } of styleDeclarations) {
const computed = computedStyle[name];
if (computed && computed.type === 'dynamic') {
continue;
}
if (
computed == null ||
important === true ||
importantStyles.get(name) === false
) {
computedStyle[name] = { type: 'static', inherited: false, value };
importantStyles.set(name, important);
}
}
return computedStyle;
};
/**
* Compares selector specificities.
* Derived from https://github.com/keeganstreet/specificity/blob/8757133ddd2ed0163f120900047ff0f92760b536/specificity.js#L207
*
* @param {Specificity} a
* @param {Specificity} b
* @returns {number}
*/
const compareSpecificity = (a, b) => {
for (let i = 0; i < 4; i += 1) {
if (a[i] < b[i]) {
return -1;
} else if (a[i] > b[i]) {
return 1;
}
}
return 0;
};
exports.compareSpecificity = compareSpecificity;
/**
* @type {(root: XastRoot) => Stylesheet}
*/
const collectStylesheet = (root) => {
/** @type {StylesheetRule[]} */
const rules = [];
/** @type {Map<XastElement, XastParent>} */
const parents = new Map();
visit(root, {
element: {
enter: (node, parentNode) => {
parents.set(node, parentNode);
if (node.name !== 'style') {
return;
}
if (
node.attributes.type == null ||
node.attributes.type === '' ||
node.attributes.type === 'text/css'
) {
const dynamic =
node.attributes.media != null && node.attributes.media !== 'all';
for (const child of node.children) {
if (child.type === 'text' || child.type === 'cdata') {
rules.push(...parseStylesheet(child.value, dynamic));
}
}
}
},
},
});
// sort by selectors specificity
rules.sort((a, b) => compareSpecificity(a.specificity, b.specificity));
return { rules, parents };
};
exports.collectStylesheet = collectStylesheet;
/**
* @param {Stylesheet} stylesheet
* @param {XastElement} node
* @returns {ComputedStyles}
*/
const computeStyle = (stylesheet, node) => {
const { parents } = stylesheet;
const computedStyles = computeOwnStyle(stylesheet, node);
let parent = parents.get(node);
while (parent != null && parent.type !== 'root') {
const inheritedStyles = computeOwnStyle(stylesheet, parent);
for (const [name, computed] of Object.entries(inheritedStyles)) {
if (
computedStyles[name] == null &&
inheritableAttrs.has(name) &&
!presentationNonInheritableGroupAttrs.has(name)
) {
computedStyles[name] = { ...computed, inherited: true };
}
}
parent = parents.get(parent);
}
return computedStyles;
};
exports.computeStyle = computeStyle;
/**
* Determines if the CSS selector includes or traverses the given attribute.
*
* Classes and IDs are generated as attribute selectors, so you can check for
* if a `.class` or `#id` is included by passing `name=class` or `name=id`
* respectively.
*
* @param {csstree.ListItem<csstree.CssNode>|string} selector
* @param {string} name
* @param {?string} value
* @param {boolean} traversed
* @returns {boolean}
*/
const includesAttrSelector = (
selector,
name,
value = null,
traversed = false,
) => {
const selectors =
typeof selector === 'string'
? csswhat.parse(selector)
: csswhat.parse(csstree.generate(selector.data));
for (const subselector of selectors) {
const hasAttrSelector = subselector.some((segment, index) => {
if (traversed) {
if (index === subselector.length - 1) {
return false;
}
const isNextTraversal = csswhat.isTraversal(subselector[index + 1]);
if (!isNextTraversal) {
return false;
}
}
if (segment.type !== 'attribute' || segment.name !== name) {
return false;
}
return value == null ? true : segment.value === value;
});
if (hasAttrSelector) {
return true;
}
}
return false;
};
exports.includesAttrSelector = includesAttrSelector;

85
backend/node_modules/svgo/lib/svgo-node.js generated vendored Normal file
View File

@@ -0,0 +1,85 @@
'use strict';
const os = require('os');
const fs = require('fs');
const { pathToFileURL } = require('url');
const path = require('path');
const { optimize: optimizeAgnostic } = require('./svgo.js');
const importConfig = async (configFile) => {
let config;
// at the moment dynamic import may randomly fail with segfault
// to workaround this for some users .cjs extension is loaded
// exclusively with require
if (configFile.endsWith('.cjs')) {
config = require(configFile);
} else {
// dynamic import expects file url instead of path and may fail
// when windows path is provided
const { default: imported } = await import(pathToFileURL(configFile));
config = imported;
}
if (config == null || typeof config !== 'object' || Array.isArray(config)) {
throw Error(`Invalid config file "${configFile}"`);
}
return config;
};
const isFile = async (file) => {
try {
const stats = await fs.promises.stat(file);
return stats.isFile();
} catch {
return false;
}
};
const loadConfig = async (configFile, cwd = process.cwd()) => {
if (configFile != null) {
if (path.isAbsolute(configFile)) {
return await importConfig(configFile);
} else {
return await importConfig(path.join(cwd, configFile));
}
}
let dir = cwd;
// eslint-disable-next-line no-constant-condition
while (true) {
const js = path.join(dir, 'svgo.config.js');
if (await isFile(js)) {
return await importConfig(js);
}
const mjs = path.join(dir, 'svgo.config.mjs');
if (await isFile(mjs)) {
return await importConfig(mjs);
}
const cjs = path.join(dir, 'svgo.config.cjs');
if (await isFile(cjs)) {
return await importConfig(cjs);
}
const parent = path.dirname(dir);
if (dir === parent) {
return null;
}
dir = parent;
}
};
exports.loadConfig = loadConfig;
const optimize = (input, config) => {
if (config == null) {
config = {};
}
if (typeof config !== 'object') {
throw Error('Config should be an object');
}
return optimizeAgnostic(input, {
...config,
js2svg: {
// platform specific default for end of line
eol: os.EOL === '\r\n' ? 'crlf' : 'lf',
...config.js2svg,
},
});
};
exports.optimize = optimize;

69
backend/node_modules/svgo/lib/svgo.d.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
import type { StringifyOptions, DataUri, Plugin as PluginFn } from './types';
import type {
BuiltinsWithOptionalParams,
BuiltinsWithRequiredParams,
} from '../plugins/plugins-types';
type CustomPlugin = {
name: string;
fn: PluginFn<void>;
};
type PluginConfig =
| keyof BuiltinsWithOptionalParams
| {
[Name in keyof BuiltinsWithOptionalParams]: {
name: Name;
params?: BuiltinsWithOptionalParams[Name];
};
}[keyof BuiltinsWithOptionalParams]
| {
[Name in keyof BuiltinsWithRequiredParams]: {
name: Name;
params: BuiltinsWithRequiredParams[Name];
};
}[keyof BuiltinsWithRequiredParams]
| CustomPlugin;
export type Config = {
/** Can be used by plugins, for example prefixids */
path?: string;
/** Pass over SVGs multiple times to ensure all optimizations are applied. */
multipass?: boolean;
/** Precision of floating point numbers. Will be passed to each plugin that supports this param. */
floatPrecision?: number;
/**
* Plugins configuration
* ['preset-default'] is default
* Can also specify any builtin plugin
* ['sortAttrs', { name: 'prefixIds', params: { prefix: 'my-prefix' } }]
* Or custom
* [{ name: 'myPlugin', fn: () => ({}) }]
*/
plugins?: PluginConfig[];
/** Options for rendering optimized SVG from AST. */
js2svg?: StringifyOptions;
/** Output as Data URI string. */
datauri?: DataUri;
};
type Output = {
data: string;
};
/** The core of SVGO */
export declare function optimize(input: string, config?: Config): Output;
/**
* If you write a tool on top of svgo you might need a way to load svgo config.
*
* You can also specify relative or absolute path and customize current working directory.
*/
export declare function loadConfig(
configFile: string,
cwd?: string,
): Promise<Config>;
export declare function loadConfig(
configFile?: null,
cwd?: string,
): Promise<Config | null>;

102
backend/node_modules/svgo/lib/svgo.js generated vendored Normal file
View File

@@ -0,0 +1,102 @@
'use strict';
const { parseSvg } = require('./parser.js');
const { stringifySvg } = require('./stringifier.js');
const { builtin } = require('./builtin.js');
const { invokePlugins } = require('./svgo/plugins.js');
const { encodeSVGDatauri } = require('./svgo/tools.js');
const pluginsMap = {};
for (const plugin of builtin) {
pluginsMap[plugin.name] = plugin;
}
const resolvePluginConfig = (plugin) => {
if (typeof plugin === 'string') {
// resolve builtin plugin specified as string
const builtinPlugin = pluginsMap[plugin];
if (builtinPlugin == null) {
throw Error(`Unknown builtin plugin "${plugin}" specified.`);
}
return {
name: plugin,
params: {},
fn: builtinPlugin.fn,
};
}
if (typeof plugin === 'object' && plugin != null) {
if (plugin.name == null) {
throw Error(`Plugin name should be specified`);
}
// use custom plugin implementation
let fn = plugin.fn;
if (fn == null) {
// resolve builtin plugin implementation
const builtinPlugin = pluginsMap[plugin.name];
if (builtinPlugin == null) {
throw Error(`Unknown builtin plugin "${plugin.name}" specified.`);
}
fn = builtinPlugin.fn;
}
return {
name: plugin.name,
params: plugin.params,
fn,
};
}
return null;
};
const optimize = (input, config) => {
if (config == null) {
config = {};
}
if (typeof config !== 'object') {
throw Error('Config should be an object');
}
const maxPassCount = config.multipass ? 10 : 1;
let prevResultSize = Number.POSITIVE_INFINITY;
let output = '';
const info = {};
if (config.path != null) {
info.path = config.path;
}
for (let i = 0; i < maxPassCount; i += 1) {
info.multipassCount = i;
const ast = parseSvg(input, config.path);
const plugins = config.plugins || ['preset-default'];
if (!Array.isArray(plugins)) {
throw Error(
'malformed config, `plugins` property must be an array.\nSee more info here: https://github.com/svg/svgo#configuration',
);
}
const resolvedPlugins = plugins
.filter((plugin) => plugin != null)
.map(resolvePluginConfig);
if (resolvedPlugins.length < plugins.length) {
console.warn(
'Warning: plugins list includes null or undefined elements, these will be ignored.',
);
}
const globalOverrides = {};
if (config.floatPrecision != null) {
globalOverrides.floatPrecision = config.floatPrecision;
}
invokePlugins(ast, info, resolvedPlugins, null, globalOverrides);
output = stringifySvg(ast, config.js2svg);
if (output.length < prevResultSize) {
input = output;
prevResultSize = output.length;
} else {
break;
}
}
if (config.datauri) {
output = encodeSVGDatauri(output, config.datauri);
}
return {
data: output,
};
};
exports.optimize = optimize;

528
backend/node_modules/svgo/lib/svgo/coa.js generated vendored Normal file
View File

@@ -0,0 +1,528 @@
'use strict';
const fs = require('fs');
const path = require('path');
const colors = require('picocolors');
const { loadConfig, optimize } = require('../svgo-node.js');
const { builtin } = require('../builtin.js');
const PKG = require('../../package.json');
const { encodeSVGDatauri, decodeSVGDatauri } = require('./tools.js');
const regSVGFile = /\.svg$/i;
/**
* Synchronously check if path is a directory. Tolerant to errors like ENOENT.
*
* @param {string} path
*/
function checkIsDir(path) {
try {
return fs.lstatSync(path).isDirectory();
} catch (e) {
return false;
}
}
module.exports = function makeProgram(program) {
program
.name(PKG.name)
.description(PKG.description, {
INPUT: 'Alias to --input',
})
.version(PKG.version, '-v, --version')
.arguments('[INPUT...]')
.option('-i, --input <INPUT...>', 'Input files, "-" for STDIN')
.option('-s, --string <STRING>', 'Input SVG data string')
.option(
'-f, --folder <FOLDER>',
'Input folder, optimize and rewrite all *.svg files',
)
.option(
'-o, --output <OUTPUT...>',
'Output file or folder (by default the same as the input), "-" for STDOUT',
)
.option(
'-p, --precision <INTEGER>',
'Set number of digits in the fractional part, overrides plugins params',
)
.option('--config <CONFIG>', 'Custom config file, only .js is supported')
.option(
'--datauri <FORMAT>',
'Output as Data URI string (base64), URI encoded (enc) or unencoded (unenc)',
)
.option(
'--multipass',
'Pass over SVGs multiple times to ensure all optimizations are applied',
)
.option('--pretty', 'Make SVG pretty printed')
.option('--indent <INTEGER>', 'Indent number when pretty printing SVGs')
.option(
'--eol <EOL>',
'Line break to use when outputting SVG: lf, crlf. If unspecified, uses platform default.',
)
.option('--final-newline', 'Ensure SVG ends with a line break')
.option(
'-r, --recursive',
"Use with '--folder'. Optimizes *.svg files in folders recursively.",
)
.option(
'--exclude <PATTERN...>',
"Use with '--folder'. Exclude files matching regular expression pattern.",
)
.option(
'-q, --quiet',
'Only output error messages, not regular status messages',
)
.option('--show-plugins', 'Show available plugins and exit')
// used by picocolors internally
.option('--no-color', 'Output plain text without color')
.action(action);
};
async function action(args, opts, command) {
var input = opts.input || args;
var output = opts.output;
var config = {};
if (opts.precision != null) {
const number = Number.parseInt(opts.precision, 10);
if (Number.isNaN(number)) {
console.error(
"error: option '-p, --precision' argument must be an integer number",
);
process.exit(1);
} else {
opts.precision = number;
}
}
if (opts.datauri != null) {
if (
opts.datauri !== 'base64' &&
opts.datauri !== 'enc' &&
opts.datauri !== 'unenc'
) {
console.error(
"error: option '--datauri' must have one of the following values: 'base64', 'enc' or 'unenc'",
);
process.exit(1);
}
}
if (opts.indent != null) {
const number = Number.parseInt(opts.indent, 10);
if (Number.isNaN(number)) {
console.error(
"error: option '--indent' argument must be an integer number",
);
process.exit(1);
} else {
opts.indent = number;
}
}
if (opts.eol != null && opts.eol !== 'lf' && opts.eol !== 'crlf') {
console.error(
"error: option '--eol' must have one of the following values: 'lf' or 'crlf'",
);
process.exit(1);
}
// --show-plugins
if (opts.showPlugins) {
showAvailablePlugins();
return;
}
// w/o anything
if (
(input.length === 0 || input[0] === '-') &&
!opts.string &&
!opts.stdin &&
!opts.folder &&
process.stdin.isTTY === true
) {
return command.help();
}
if (
typeof process == 'object' &&
process.versions &&
process.versions.node &&
PKG &&
PKG.engines.node
) {
var nodeVersion = String(PKG.engines.node).match(/\d*(\.\d+)*/)[0];
if (parseFloat(process.versions.node) < parseFloat(nodeVersion)) {
throw Error(
`${PKG.name} requires Node.js version ${nodeVersion} or higher.`,
);
}
}
// --config
const loadedConfig = await loadConfig(opts.config);
if (loadedConfig != null) {
config = loadedConfig;
}
// --quiet
if (opts.quiet) {
config.quiet = opts.quiet;
}
// --recursive
if (opts.recursive) {
config.recursive = opts.recursive;
}
// --exclude
config.exclude = opts.exclude
? opts.exclude.map((pattern) => RegExp(pattern))
: [];
// --precision
if (opts.precision != null) {
var precision = Math.min(Math.max(0, opts.precision), 20);
config.floatPrecision = precision;
}
// --multipass
if (opts.multipass) {
config.multipass = true;
}
// --pretty
if (opts.pretty) {
config.js2svg = config.js2svg || {};
config.js2svg.pretty = true;
if (opts.indent != null) {
config.js2svg.indent = opts.indent;
}
}
// --eol
if (opts.eol) {
config.js2svg = config.js2svg || {};
config.js2svg.eol = opts.eol;
}
// --final-newline
if (opts.finalNewline) {
config.js2svg = config.js2svg || {};
config.js2svg.finalNewline = true;
}
// --output
if (output) {
if (input.length && input[0] != '-') {
if (output.length == 1 && checkIsDir(output[0])) {
var dir = output[0];
for (var i = 0; i < input.length; i++) {
output[i] = checkIsDir(input[i])
? input[i]
: path.resolve(dir, path.basename(input[i]));
}
} else if (output.length < input.length) {
output = output.concat(input.slice(output.length));
}
}
} else if (input.length) {
output = input;
} else if (opts.string) {
output = '-';
}
if (opts.datauri) {
config.datauri = opts.datauri;
}
// --folder
if (opts.folder) {
var ouputFolder = (output && output[0]) || opts.folder;
await optimizeFolder(config, opts.folder, ouputFolder);
}
// --input
if (input.length !== 0) {
// STDIN
if (input[0] === '-') {
return new Promise((resolve, reject) => {
var data = '',
file = output[0];
process.stdin
.on('data', (chunk) => (data += chunk))
.once('end', () =>
processSVGData(config, null, data, file).then(resolve, reject),
);
});
// file
} else {
await Promise.all(
input.map((file, n) => optimizeFile(config, file, output[n])),
);
}
// --string
} else if (opts.string) {
var data = decodeSVGDatauri(opts.string);
return processSVGData(config, null, data, output[0]);
}
}
/**
* Optimize SVG files in a directory.
*
* @param {Object} config options
* @param {string} dir input directory
* @param {string} output output directory
* @return {Promise}
*/
function optimizeFolder(config, dir, output) {
if (!config.quiet) {
console.log(`Processing directory '${dir}':\n`);
}
return fs.promises
.readdir(dir)
.then((files) => processDirectory(config, dir, files, output));
}
/**
* Process given files, take only SVG.
*
* @param {Object} config options
* @param {string} dir input directory
* @param {Array} files list of file names in the directory
* @param {string} output output directory
* @return {Promise}
*/
function processDirectory(config, dir, files, output) {
// take only *.svg files, recursively if necessary
var svgFilesDescriptions = getFilesDescriptions(config, dir, files, output);
return svgFilesDescriptions.length
? Promise.all(
svgFilesDescriptions.map((fileDescription) =>
optimizeFile(
config,
fileDescription.inputPath,
fileDescription.outputPath,
),
),
)
: Promise.reject(
new Error(`No SVG files have been found in '${dir}' directory.`),
);
}
/**
* Get SVG files descriptions.
*
* @param {Object} config options
* @param {string} dir input directory
* @param {Array} files list of file names in the directory
* @param {string} output output directory
* @return {Array}
*/
function getFilesDescriptions(config, dir, files, output) {
const filesInThisFolder = files
.filter(
(name) =>
regSVGFile.test(name) &&
!config.exclude.some((regExclude) => regExclude.test(name)),
)
.map((name) => ({
inputPath: path.resolve(dir, name),
outputPath: path.resolve(output, name),
}));
return config.recursive
? [].concat(
filesInThisFolder,
files
.filter((name) => checkIsDir(path.resolve(dir, name)))
.map((subFolderName) => {
const subFolderPath = path.resolve(dir, subFolderName);
const subFolderFiles = fs.readdirSync(subFolderPath);
const subFolderOutput = path.resolve(output, subFolderName);
return getFilesDescriptions(
config,
subFolderPath,
subFolderFiles,
subFolderOutput,
);
})
.reduce((a, b) => [].concat(a, b), []),
)
: filesInThisFolder;
}
/**
* Read SVG file and pass to processing.
*
* @param {Object} config options
* @param {string} file
* @param {string} output
* @return {Promise}
*/
function optimizeFile(config, file, output) {
return fs.promises.readFile(file, 'utf8').then(
(data) => processSVGData(config, { path: file }, data, output, file),
(error) => checkOptimizeFileError(config, file, output, error),
);
}
/**
* Optimize SVG data.
*
* @param {Object} config options
* @param {string} data SVG content to optimize
* @param {string} output where to write optimized file
* @param {string} [input] input file name (being used if output is a directory)
* @return {Promise}
*/
function processSVGData(config, info, data, output, input) {
var startTime = Date.now(),
prevFileSize = Buffer.byteLength(data, 'utf8');
let result;
try {
result = optimize(data, { ...config, ...info });
} catch (error) {
if (error.name === 'SvgoParserError') {
console.error(colors.red(error.toString()));
process.exit(1);
} else {
throw error;
}
}
if (config.datauri) {
result.data = encodeSVGDatauri(result.data, config.datauri);
}
var resultFileSize = Buffer.byteLength(result.data, 'utf8'),
processingTime = Date.now() - startTime;
return writeOutput(input, output, result.data).then(
function () {
if (!config.quiet && output != '-') {
if (input) {
console.log(`\n${path.basename(input)}:`);
}
printTimeInfo(processingTime);
printProfitInfo(prevFileSize, resultFileSize);
}
},
(error) =>
Promise.reject(
new Error(
error.code === 'ENOTDIR'
? `Error: output '${output}' is not a directory.`
: error,
),
),
);
}
/**
* Write result of an optimization.
*
* @param {string} input
* @param {string} output output file name. '-' for stdout
* @param {string} data data to write
* @return {Promise}
*/
function writeOutput(input, output, data) {
if (output == '-') {
process.stdout.write(data);
return Promise.resolve();
}
fs.mkdirSync(path.dirname(output), { recursive: true });
return fs.promises
.writeFile(output, data, 'utf8')
.catch((error) => checkWriteFileError(input, output, data, error));
}
/**
* Write time taken to optimize.
*
* @param {number} time time in milliseconds.
*/
function printTimeInfo(time) {
console.log(`Done in ${time} ms!`);
}
/**
* Write optimizing stats in a human-readable format.
*
* @param {number} inBytes size before optimization.
* @param {number} outBytes size after optimization.
*/
function printProfitInfo(inBytes, outBytes) {
const profitPercent = 100 - (outBytes * 100) / inBytes;
/** @type {[string, Function]} */
const ui = profitPercent < 0 ? ['+', colors.red] : ['-', colors.green];
console.log(
Math.round((inBytes / 1024) * 1000) / 1000 + ' KiB',
ui[0],
ui[1](Math.abs(Math.round(profitPercent * 10) / 10) + '%'),
'=',
Math.round((outBytes / 1024) * 1000) / 1000 + ' KiB',
);
}
/**
* Check for errors, if it's a dir optimize the dir.
*
* @param {Object} config
* @param {string} input
* @param {string} output
* @param {Error} error
* @return {Promise}
*/
function checkOptimizeFileError(config, input, output, error) {
if (error.code == 'EISDIR') {
return optimizeFolder(config, input, output);
} else if (error.code == 'ENOENT') {
return Promise.reject(
new Error(`Error: no such file or directory '${error.path}'.`),
);
}
return Promise.reject(error);
}
/**
* Check for saving file error. If the output is a dir, then write file there.
*
* @param {string} input
* @param {string} output
* @param {string} data
* @param {Error} error
* @return {Promise}
*/
function checkWriteFileError(input, output, data, error) {
if (error.code == 'EISDIR' && input) {
return fs.promises.writeFile(
path.resolve(output, path.basename(input)),
data,
'utf8',
);
} else {
return Promise.reject(error);
}
}
/** Show list of available plugins with short description. */
function showAvailablePlugins() {
const list = builtin
.sort((a, b) => a.name.localeCompare(b.name))
.map((plugin) => ` [ ${colors.green(plugin.name)} ] ${plugin.description}`)
.join('\n');
console.log('Currently available plugins:\n' + list);
}
module.exports.checkIsDir = checkIsDir;

View File

@@ -0,0 +1,2 @@
declare let obj: any;
export = obj;

View File

@@ -0,0 +1,120 @@
'use strict';
const isTag = (node) => {
return node.type === 'element';
};
const existsOne = (test, elems) => {
return elems.some((elem) => {
if (isTag(elem)) {
return test(elem) || existsOne(test, getChildren(elem));
} else {
return false;
}
});
};
const getAttributeValue = (elem, name) => {
return elem.attributes[name];
};
const getChildren = (node) => {
return node.children || [];
};
const getName = (elemAst) => {
return elemAst.name;
};
const getParent = (node) => {
return node.parentNode || null;
};
const getSiblings = (elem) => {
var parent = getParent(elem);
return parent ? getChildren(parent) : [];
};
const getText = (node) => {
if (node.children[0].type === 'text' && node.children[0].type === 'cdata') {
return node.children[0].value;
}
return '';
};
const hasAttrib = (elem, name) => {
return elem.attributes[name] !== undefined;
};
const removeSubsets = (nodes) => {
let idx = nodes.length;
let node;
let ancestor;
let replace;
// Check if each node (or one of its ancestors) is already contained in the
// array.
while (--idx > -1) {
node = ancestor = nodes[idx];
// Temporarily remove the node under consideration
nodes[idx] = null;
replace = true;
while (ancestor) {
if (nodes.includes(ancestor)) {
replace = false;
nodes.splice(idx, 1);
break;
}
ancestor = getParent(ancestor);
}
// If the node has been found to be unique, re-insert it.
if (replace) {
nodes[idx] = node;
}
}
return nodes;
};
const findAll = (test, elems) => {
const result = [];
for (const elem of elems) {
if (isTag(elem)) {
if (test(elem)) {
result.push(elem);
}
result.push(...findAll(test, getChildren(elem)));
}
}
return result;
};
const findOne = (test, elems) => {
for (const elem of elems) {
if (isTag(elem)) {
if (test(elem)) {
return elem;
}
const result = findOne(test, getChildren(elem));
if (result) {
return result;
}
}
}
return null;
};
const svgoCssSelectAdapter = {
isTag,
existsOne,
getAttributeValue,
getChildren,
getName,
getParent,
getSiblings,
getText,
hasAttrib,
removeSubsets,
findAll,
findOne,
};
module.exports = svgoCssSelectAdapter;

61
backend/node_modules/svgo/lib/svgo/plugins.js generated vendored Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
const { visit } = require('../xast.js');
/**
* Plugins engine.
*
* @module plugins
*
* @param {Object} ast input ast
* @param {Object} info extra information
* @param {Array} plugins plugins object from config
* @return {Object} output ast
*/
const invokePlugins = (ast, info, plugins, overrides, globalOverrides) => {
for (const plugin of plugins) {
const override = overrides?.[plugin.name];
if (override === false) {
continue;
}
const params = { ...plugin.params, ...globalOverrides, ...override };
const visitor = plugin.fn(ast, params, info);
if (visitor != null) {
visit(ast, visitor);
}
}
};
exports.invokePlugins = invokePlugins;
const createPreset = ({ name, plugins }) => {
return {
name,
fn: (ast, params, info) => {
const { floatPrecision, overrides } = params;
const globalOverrides = {};
if (floatPrecision != null) {
globalOverrides.floatPrecision = floatPrecision;
}
if (overrides) {
const pluginNames = plugins.map(({ name }) => name);
for (const pluginName of Object.keys(overrides)) {
if (!pluginNames.includes(pluginName)) {
console.warn(
`You are trying to configure ${pluginName} which is not part of ${name}.\n` +
`Try to put it before or after, for example\n\n` +
`plugins: [\n` +
` {\n` +
` name: '${name}',\n` +
` },\n` +
` '${pluginName}'\n` +
`]\n`,
);
}
}
}
invokePlugins(ast, info, plugins, overrides, globalOverrides);
},
};
};
exports.createPreset = createPreset;

245
backend/node_modules/svgo/lib/svgo/tools.js generated vendored Normal file
View File

@@ -0,0 +1,245 @@
'use strict';
/**
* @typedef {import('../../lib/types').XastElement} XastElement
* @typedef {import('../types').PathDataCommand} PathDataCommand
* @typedef {import('../types').DataUri} DataUri
*/
const { attrsGroups, referencesProps } = require('../../plugins/_collections');
const regReferencesUrl = /\burl\((["'])?#(.+?)\1\)/g;
const regReferencesHref = /^#(.+?)$/;
const regReferencesBegin = /(\w+)\.[a-zA-Z]/;
/**
* Encode plain SVG data string into Data URI string.
*
* @type {(str: string, type?: DataUri) => string}
*/
exports.encodeSVGDatauri = (str, type) => {
var prefix = 'data:image/svg+xml';
if (!type || type === 'base64') {
// base64
prefix += ';base64,';
str = prefix + Buffer.from(str).toString('base64');
} else if (type === 'enc') {
// URI encoded
str = prefix + ',' + encodeURIComponent(str);
} else if (type === 'unenc') {
// unencoded
str = prefix + ',' + str;
}
return str;
};
/**
* Decode SVG Data URI string into plain SVG string.
*
* @type {(str: string) => string}
*/
exports.decodeSVGDatauri = (str) => {
var regexp = /data:image\/svg\+xml(;charset=[^;,]*)?(;base64)?,(.*)/;
var match = regexp.exec(str);
// plain string
if (!match) return str;
var data = match[3];
if (match[2]) {
// base64
str = Buffer.from(data, 'base64').toString('utf8');
} else if (data.charAt(0) === '%') {
// URI encoded
str = decodeURIComponent(data);
} else if (data.charAt(0) === '<') {
// unencoded
str = data;
}
return str;
};
/**
* @typedef {{
* noSpaceAfterFlags?: boolean,
* leadingZero?: boolean,
* negativeExtraSpace?: boolean
* }} CleanupOutDataParams
*/
/**
* Convert a row of numbers to an optimized string view.
*
* @example
* [0, -1, .5, .5] → "0-1 .5.5"
*
* @type {(data: number[], params: CleanupOutDataParams, command?: PathDataCommand) => string}
*/
exports.cleanupOutData = (data, params, command) => {
let str = '';
let delimiter;
/**
* @type {number}
*/
let prev;
data.forEach((item, i) => {
// space delimiter by default
delimiter = ' ';
// no extra space in front of first number
if (i == 0) delimiter = '';
// no extra space after 'arcto' command flags(large-arc and sweep flags)
// a20 60 45 0 1 30 20 → a20 60 45 0130 20
if (params.noSpaceAfterFlags && (command == 'A' || command == 'a')) {
var pos = i % 7;
if (pos == 4 || pos == 5) delimiter = '';
}
// remove floating-point numbers leading zeros
// 0.5 → .5
// -0.5 → -.5
const itemStr = params.leadingZero
? removeLeadingZero(item)
: item.toString();
// no extra space in front of negative number or
// in front of a floating number if a previous number is floating too
if (
params.negativeExtraSpace &&
delimiter != '' &&
(item < 0 || (itemStr.charAt(0) === '.' && prev % 1 !== 0))
) {
delimiter = '';
}
// save prev item value
prev = item;
str += delimiter + itemStr;
});
return str;
};
/**
* Remove floating-point numbers leading zero.
*
* @param {number} value
* @returns {string}
* @example
* 0.5 → .5
* -0.5 → -.5
*/
const removeLeadingZero = (value) => {
const strValue = value.toString();
if (0 < value && value < 1 && strValue.startsWith('0')) {
return strValue.slice(1);
}
if (-1 < value && value < 0 && strValue[1] === '0') {
return strValue[0] + strValue.slice(2);
}
return strValue;
};
exports.removeLeadingZero = removeLeadingZero;
/**
* If the current node contains any scripts. This does not check parents or
* children of the node, only the properties and attributes of the node itself.
*
* @param {XastElement} node Current node to check against.
* @returns {boolean} If the current node contains scripts.
*/
const hasScripts = (node) => {
if (node.name === 'script' && node.children.length !== 0) {
return true;
}
if (node.name === 'a') {
const hasJsLinks = Object.entries(node.attributes).some(
([attrKey, attrValue]) =>
(attrKey === 'href' || attrKey.endsWith(':href')) &&
attrValue != null &&
attrValue.trimStart().startsWith('javascript:'),
);
if (hasJsLinks) {
return true;
}
}
const eventAttrs = [
...attrsGroups.animationEvent,
...attrsGroups.documentEvent,
...attrsGroups.documentElementEvent,
...attrsGroups.globalEvent,
...attrsGroups.graphicalEvent,
];
return eventAttrs.some((attr) => node.attributes[attr] != null);
};
exports.hasScripts = hasScripts;
/**
* For example, a string that contains one or more of following would match and
* return true:
*
* * `url(#gradient001)`
* * `url('#gradient001')`
*
* @param {string} body
* @returns {boolean} If the given string includes a URL reference.
*/
const includesUrlReference = (body) => {
return new RegExp(regReferencesUrl).test(body);
};
exports.includesUrlReference = includesUrlReference;
/**
* @param {string} attribute
* @param {string} value
* @returns {string[]}
*/
const findReferences = (attribute, value) => {
const results = [];
if (referencesProps.has(attribute)) {
const matches = value.matchAll(regReferencesUrl);
for (const match of matches) {
results.push(match[2]);
}
}
if (attribute === 'href' || attribute.endsWith(':href')) {
const match = regReferencesHref.exec(value);
if (match != null) {
results.push(match[1]);
}
}
if (attribute === 'begin') {
const match = regReferencesBegin.exec(value);
if (match != null) {
results.push(match[1]);
}
}
return results.map((body) => decodeURI(body));
};
exports.findReferences = findReferences;
/**
* Does the same as {@link Number.toFixed} but without casting
* the return value to a string.
*
* @param {number} num
* @param {number} precision
* @returns {number}
*/
const toFixed = (num, precision) => {
const pow = 10 ** precision;
return Math.round(num * pow) / pow;
};
exports.toFixed = toFixed;

174
backend/node_modules/svgo/lib/types.d.ts generated vendored Normal file
View File

@@ -0,0 +1,174 @@
export type XastDoctype = {
type: 'doctype';
name: string;
data: {
doctype: string;
};
};
export type XastInstruction = {
type: 'instruction';
name: string;
value: string;
};
export type XastComment = {
type: 'comment';
value: string;
};
export type XastCdata = {
type: 'cdata';
value: string;
};
export type XastText = {
type: 'text';
value: string;
};
export type XastElement = {
type: 'element';
name: string;
attributes: Record<string, string>;
children: XastChild[];
};
export type XastChild =
| XastDoctype
| XastInstruction
| XastComment
| XastCdata
| XastText
| XastElement;
export type XastRoot = {
type: 'root';
children: XastChild[];
};
export type XastParent = XastRoot | XastElement;
export type XastNode = XastRoot | XastChild;
export type StringifyOptions = {
doctypeStart?: string;
doctypeEnd?: string;
procInstStart?: string;
procInstEnd?: string;
tagOpenStart?: string;
tagOpenEnd?: string;
tagCloseStart?: string;
tagCloseEnd?: string;
tagShortStart?: string;
tagShortEnd?: string;
attrStart?: string;
attrEnd?: string;
commentStart?: string;
commentEnd?: string;
cdataStart?: string;
cdataEnd?: string;
textStart?: string;
textEnd?: string;
indent?: number | string;
regEntities?: RegExp;
regValEntities?: RegExp;
encodeEntity?: (char: string) => string;
pretty?: boolean;
useShortTags?: boolean;
eol?: 'lf' | 'crlf';
finalNewline?: boolean;
};
type VisitorNode<Node> = {
enter?: (node: Node, parentNode: XastParent) => void | symbol;
exit?: (node: Node, parentNode: XastParent) => void;
};
type VisitorRoot = {
enter?: (node: XastRoot, parentNode: null) => void;
exit?: (node: XastRoot, parentNode: null) => void;
};
export type Visitor = {
doctype?: VisitorNode<XastDoctype>;
instruction?: VisitorNode<XastInstruction>;
comment?: VisitorNode<XastComment>;
cdata?: VisitorNode<XastCdata>;
text?: VisitorNode<XastText>;
element?: VisitorNode<XastElement>;
root?: VisitorRoot;
};
export type PluginInfo = {
path?: string;
multipassCount: number;
};
export type Plugin<Params> = (
root: XastRoot,
params: Params,
info: PluginInfo,
) => null | Visitor;
export type Specificity = [number, number, number];
export type StylesheetDeclaration = {
name: string;
value: string;
important: boolean;
};
export type StylesheetRule = {
dynamic: boolean;
selector: string;
specificity: Specificity;
declarations: StylesheetDeclaration[];
};
export type Stylesheet = {
rules: StylesheetRule[];
parents: Map<XastElement, XastParent>;
};
type StaticStyle = {
type: 'static';
inherited: boolean;
value: string;
};
type DynamicStyle = {
type: 'dynamic';
inherited: boolean;
};
export type ComputedStyles = Record<string, StaticStyle | DynamicStyle>;
export type PathDataCommand =
| 'M'
| 'm'
| 'Z'
| 'z'
| 'L'
| 'l'
| 'H'
| 'h'
| 'V'
| 'v'
| 'C'
| 'c'
| 'S'
| 's'
| 'Q'
| 'q'
| 'T'
| 't'
| 'A'
| 'a';
export type PathDataItem = {
command: PathDataCommand;
args: number[];
};
export type DataUri = 'base64' | 'enc' | 'unenc';

87
backend/node_modules/svgo/lib/xast.js generated vendored Normal file
View File

@@ -0,0 +1,87 @@
'use strict';
/**
* @typedef {import('./types').XastNode} XastNode
* @typedef {import('./types').XastChild} XastChild
* @typedef {import('./types').XastParent} XastParent
* @typedef {import('./types').Visitor} Visitor
*/
const { selectAll, selectOne, is } = require('css-select');
const xastAdaptor = require('./svgo/css-select-adapter.js');
const cssSelectOptions = {
xmlMode: true,
adapter: xastAdaptor,
};
/**
* @type {(node: XastNode, selector: string) => XastChild[]}
*/
const querySelectorAll = (node, selector) => {
return selectAll(selector, node, cssSelectOptions);
};
exports.querySelectorAll = querySelectorAll;
/**
* @type {(node: XastNode, selector: string) => ?XastChild}
*/
const querySelector = (node, selector) => {
return selectOne(selector, node, cssSelectOptions);
};
exports.querySelector = querySelector;
/**
* @type {(node: XastChild, selector: string) => boolean}
*/
const matches = (node, selector) => {
return is(node, selector, cssSelectOptions);
};
exports.matches = matches;
const visitSkip = Symbol();
exports.visitSkip = visitSkip;
/**
* @type {(node: XastNode, visitor: Visitor, parentNode?: any) => void}
*/
const visit = (node, visitor, parentNode) => {
const callbacks = visitor[node.type];
if (callbacks && callbacks.enter) {
// @ts-ignore hard to infer
const symbol = callbacks.enter(node, parentNode);
if (symbol === visitSkip) {
return;
}
}
// visit root children
if (node.type === 'root') {
// copy children array to not loose cursor when children is spliced
for (const child of node.children) {
visit(child, visitor, node);
}
}
// visit element children if still attached to parent
if (node.type === 'element') {
if (parentNode.children.includes(node)) {
for (const child of node.children) {
visit(child, visitor, node);
}
}
}
if (callbacks && callbacks.exit) {
// @ts-ignore hard to infer
callbacks.exit(node, parentNode);
}
};
exports.visit = visit;
/**
* @param {XastChild} node
* @param {XastParent} parentNode
*/
const detachNodeFromParent = (node, parentNode) => {
// avoid splice to not break for loops
parentNode.children = parentNode.children.filter((child) => child !== node);
};
exports.detachNodeFromParent = detachNodeFromParent;