From 6dfdeb0c71d4a0607d60dbdf1c2692c72a8c2f2e Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 1 Apr 2022 14:40:00 +0800 Subject: [PATCH] Add `prefer-modern-math-apis` rule (#1780) --- configs/recommended.js | 1 + docs/rules/prefer-modern-math-apis.md | 64 +++++ readme.md | 1 + rules/prefer-modern-math-apis.js | 138 ++++++++++ test/package.mjs | 5 +- test/prefer-modern-math-apis.mjs | 67 +++++ test/snapshots/prefer-modern-math-apis.mjs.md | 259 ++++++++++++++++++ .../prefer-modern-math-apis.mjs.snap | Bin 0 -> 825 bytes 8 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 docs/rules/prefer-modern-math-apis.md create mode 100644 rules/prefer-modern-math-apis.js create mode 100644 test/prefer-modern-math-apis.mjs create mode 100644 test/snapshots/prefer-modern-math-apis.mjs.md create mode 100644 test/snapshots/prefer-modern-math-apis.mjs.snap diff --git a/configs/recommended.js b/configs/recommended.js index cb21d0b80a..d076d0418e 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -84,6 +84,7 @@ module.exports = { 'unicorn/prefer-keyboard-event-key': 'error', 'unicorn/prefer-math-trunc': 'error', 'unicorn/prefer-modern-dom-apis': 'error', + 'unicorn/prefer-modern-math-apis': 'error', 'unicorn/prefer-module': 'error', 'unicorn/prefer-negative-index': 'error', 'unicorn/prefer-node-protocol': 'error', diff --git a/docs/rules/prefer-modern-math-apis.md b/docs/rules/prefer-modern-math-apis.md new file mode 100644 index 0000000000..10f00645b0 --- /dev/null +++ b/docs/rules/prefer-modern-math-apis.md @@ -0,0 +1,64 @@ +# Prefer modern `Math` APIs over legacy patterns + + + +āœ… *This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.* + +šŸ”§ *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).* + + +Math additions in ES2015: + +- [Math.sign()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign) +- [Math.trunc()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc) +- [Math.cbrt()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt) +- [Math.expm1()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1) +- [Math.log1p()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p) +- [Math.log10()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10) +- [Math.log2()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2) +- [Math.sinh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh) +- [Math.cosh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh) +- [Math.tanh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh) +- [Math.asinh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh) +- [Math.acosh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh) +- [Math.atanh()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh) +- [Math.hypot()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot) +- [Math.clz32()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32) +- [Math.imul()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul) +- [Math.fround()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround) + +Currently, we only check a few known cases, but we are open to add more patterns. + +If you find a suitable case for this rule, please [open an issue](https://github.com/sindresorhus/eslint-plugin-unicorn/issues/new?title=%20%60prefer-modern-math-apis%60%20%20change%20request&labels=evaluating). + +## Prefer `Math.log10(x)` over + +```js +Math.log(x) * Math.LOG10E +``` + +```js +Math.LOG10E * Math.log(x) +``` + +```js +Math.log(x) / Math.LN10 +``` + +## Prefer `Math.log2(x)` over + +```js +Math.log(x) * Math.LOG2E +``` + +```js +Math.LOG2E * Math.log(x) +``` + +```js +Math.log(x) / Math.LN2 +``` + +## Separate rule for `Math.trunc()` + +See [`unicorn/prefer-math-trunc`](./prefer-math-trunc.md) rule. diff --git a/readme.md b/readme.md index 9b56f55b98..e52943b639 100644 --- a/readme.md +++ b/readme.md @@ -124,6 +124,7 @@ Each rule has emojis denoting: | [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | āœ… | šŸ”§ | | | [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. | āœ… | šŸ”§ | šŸ’” | | [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. | āœ… | šŸ”§ | | +| [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. | āœ… | šŸ”§ | | | [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | āœ… | šŸ”§ | šŸ’” | | [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`. | āœ… | šŸ”§ | | | [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. | āœ… | šŸ”§ | | diff --git a/rules/prefer-modern-math-apis.js b/rules/prefer-modern-math-apis.js new file mode 100644 index 0000000000..cb727c3fc0 --- /dev/null +++ b/rules/prefer-modern-math-apis.js @@ -0,0 +1,138 @@ +'use strict'; +const {getParenthesizedText} = require('./utils/parentheses.js'); + +const MESSAGE_ID = 'prefer-modern-math-apis'; +const messages = { + [MESSAGE_ID]: 'Prefer `{{replacement}}` over `{{description}}`.', +}; + +const isMathProperty = (node, property) => + node.type === 'MemberExpression' + && !node.optional + && !node.computed + && node.object.type === 'Identifier' + && node.object.name === 'Math' + && node.property.type === 'Identifier' + && node.property.name === property; + +const isMathMethodCall = (node, method) => + node.type === 'CallExpression' + && !node.optional + && isMathProperty(node.callee, method) + && node.arguments.length === 1 + && node.arguments[0].type !== 'SpreadElement'; + +// `Math.log(x) * Math.LOG10E` -> `Math.log10(x)` +// `Math.LOG10E * Math.log(x)` -> `Math.log10(x)` +// `Math.log(x) * Math.LOG2E` -> `Math.log2(x)` +// `Math.LOG2E * Math.log(x)` -> `Math.log2(x)` +function createLogCallTimesConstantCheck({constantName, replacementMethod}) { + const replacement = `Math.${replacementMethod}(ā€¦)`; + + return function (node, context) { + if (!(node.type === 'BinaryExpression' && node.operator === '*')) { + return; + } + + let mathLogCall; + let description; + if (isMathMethodCall(node.left, 'log') && isMathProperty(node.right, constantName)) { + mathLogCall = node.left; + description = `Math.log(ā€¦) * Math.${constantName}`; + } else if (isMathMethodCall(node.right, 'log') && isMathProperty(node.left, constantName)) { + mathLogCall = node.right; + description = `Math.${constantName} * Math.log(ā€¦)`; + } + + if (!mathLogCall) { + return; + } + + const [valueNode] = mathLogCall.arguments; + + return { + node, + messageId: MESSAGE_ID, + data: { + replacement, + description, + }, + fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.getSourceCode())})`), + }; + }; +} + +// `Math.log(x) / Math.LN10` -> `Math.log10(x)` +// `Math.log(x) / Math.LN2` -> `Math.log2(x)` +function createLogCallDivideConstantCheck({constantName, replacementMethod}) { + const message = { + messageId: MESSAGE_ID, + data: { + replacement: `Math.${replacementMethod}(ā€¦)`, + description: `Math.log(ā€¦) / Math.${constantName}`, + }, + }; + + return function (node, context) { + if ( + !( + node.type === 'BinaryExpression' + && node.operator === '/' + && isMathMethodCall(node.left, 'log') + && isMathProperty(node.right, constantName) + ) + ) { + return; + } + + const [valueNode] = node.left.arguments; + + return { + ...message, + node, + fix: fixer => fixer.replaceText(node, `Math.${replacementMethod}(${getParenthesizedText(valueNode, context.getSourceCode())})`), + }; + }; +} + +const checkFunctions = [ + createLogCallTimesConstantCheck({constantName: 'LOG10E', replacementMethod: 'log10'}), + createLogCallTimesConstantCheck({constantName: 'LOG2E', replacementMethod: 'log2'}), + createLogCallDivideConstantCheck({constantName: 'LN10', replacementMethod: 'log10'}), + createLogCallDivideConstantCheck({constantName: 'LN2', replacementMethod: 'log2'}), +]; + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => { + const nodes = []; + + return { + BinaryExpression(node) { + nodes.push(node); + }, + * 'Program:exit'() { + for (const node of nodes) { + for (const getProblem of checkFunctions) { + const problem = getProblem(node, context); + + if (problem) { + yield problem; + } + } + } + }, + }; +}; + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer modern `Math` APIs over legacy patterns.', + }, + fixable: 'code', + messages, + }, +}; diff --git a/test/package.mjs b/test/package.mjs index 1097712f43..3551064fec 100644 --- a/test/package.mjs +++ b/test/package.mjs @@ -61,7 +61,10 @@ function getNamedOptions(jsonSchema) { } const RULES_WITHOUT_PASS_FAIL_SECTIONS = new Set([ - 'filename-case', // Doesn't show code samples since it's just focused on filenames. + // Doesn't show code samples since it's just focused on filenames. + 'filename-case', + // Intended to not use `pass`/`fail` section in this rule. + 'prefer-modern-math-apis', ]); test('Every rule is defined in index file in alphabetical order', t => { diff --git a/test/prefer-modern-math-apis.mjs b/test/prefer-modern-math-apis.mjs new file mode 100644 index 0000000000..fdaa9e472a --- /dev/null +++ b/test/prefer-modern-math-apis.mjs @@ -0,0 +1,67 @@ +import outdent from 'outdent'; +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +// `Math.log10()` and `Math.log2()` +const duplicateLog10Test = code => [ + code, + // `Math.log2()` test + code.replace(/Math\.LOG10E/g, 'Math.LOG2E').replace(/Math\.LN10/g, 'Math.LN2'), +]; +test.snapshot({ + valid: [ + 'Math.log(x) * Math.log(x)', + + 'Math.LOG10E * Math.LOG10E', + 'Math.log(x) * Math[LOG10E]', + 'Math.log(x) * LOG10E', + 'Math[log](x) * Math.LOG10E', + 'foo.Math.log(x) * Math.LOG10E', + 'Math.log(x) * foo.Math.LOG10E', + 'Math.log(x) * Math.NOT_LOG10E', + 'Math.log(x) * Math?.LOG10E', + 'Math?.log(x) * Math.LOG10E', + 'log(x) * Math.LOG10E', + 'new Math.log(x) * Math.LOG10E', + 'Math.not_log(x) + Math.LOG10E', + 'Math.log(x)[Math.LOG10E]', + 'Math.log() * Math.LOG10E', + 'Math.log(x, extraArgument) * Math.LOG10E', + 'Math.log(...x) * Math.LOG10E', + + 'Math.LN10 / Math.LN10', + 'Math.log(x) /Math[LN10]', + 'Math.log(x) / LN10', + 'Math[log](x) / Math.LN10', + 'foo.Math.log(x) / Math.LN10', + 'Math.log(x) / foo.Math.LN10', + 'Math.log(x) / Math.log(x)', + 'Math.log(x) / Math.NOT_LN10', + 'Math.log(x) / Math?.LN10', + 'Math?.log(x) / Math.LN10', + 'log(x) / Math.LN10', + 'new Math.log(x) / Math.LN10', + 'Math.not_log(x) + Math.LN10', + 'Math.log(x)[Math.LN10]', + 'Math.log() / Math.LN10', + 'Math.log(x, extraArgument) / Math.LN10', + 'Math.log(...x) / Math.LN10', + ].flatMap(code => duplicateLog10Test(code)), + invalid: [ + 'Math.log(x) * Math.LOG10E', + 'Math.LOG10E * Math.log(x)', + 'Math.log(x) / Math.LN10', + 'Math.log((( 0,x ))) * Math.LOG10E', + 'Math.LOG10E * Math.log((( 0,x )))', + 'Math.log((( 0,x ))) / Math.LN10', + outdent` + function foo(x) { + return ( + Math.log(x) + / Math.LN10 + ); + } + `, + ].flatMap(code => duplicateLog10Test(code)), +}); diff --git a/test/snapshots/prefer-modern-math-apis.mjs.md b/test/snapshots/prefer-modern-math-apis.mjs.md new file mode 100644 index 0000000000..cbdf2dc5d7 --- /dev/null +++ b/test/snapshots/prefer-modern-math-apis.mjs.md @@ -0,0 +1,259 @@ +# Snapshot report for `test/prefer-modern-math-apis.mjs` + +The actual snapshot is saved in `prefer-modern-math-apis.mjs.snap`. + +Generated by [AVA](https://avajs.dev). + +## Invalid #1 + 1 | Math.log(x) * Math.LOG10E + +> Output + + `āŠ + 1 | Math.log10(x)āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log(x) * Math.LOG10EāŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log10(ā€¦)\` over \`Math.log(ā€¦) * Math.LOG10E\`.āŠ + ` + +## Invalid #2 + 1 | Math.log(x) * Math.LOG2E + +> Output + + `āŠ + 1 | Math.log2(x)āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log(x) * Math.LOG2EāŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log2(ā€¦)\` over \`Math.log(ā€¦) * Math.LOG2E\`.āŠ + ` + +## Invalid #3 + 1 | Math.LOG10E * Math.log(x) + +> Output + + `āŠ + 1 | Math.log10(x)āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.LOG10E * Math.log(x)āŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log10(ā€¦)\` over \`Math.LOG10E * Math.log(ā€¦)\`.āŠ + ` + +## Invalid #4 + 1 | Math.LOG2E * Math.log(x) + +> Output + + `āŠ + 1 | Math.log2(x)āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.LOG2E * Math.log(x)āŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log2(ā€¦)\` over \`Math.LOG2E * Math.log(ā€¦)\`.āŠ + ` + +## Invalid #5 + 1 | Math.log(x) / Math.LN10 + +> Output + + `āŠ + 1 | Math.log10(x)āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log(x) / Math.LN10āŠ + | ^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log10(ā€¦)\` over \`Math.log(ā€¦) / Math.LN10\`.āŠ + ` + +## Invalid #6 + 1 | Math.log(x) / Math.LN2 + +> Output + + `āŠ + 1 | Math.log2(x)āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log(x) / Math.LN2āŠ + | ^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log2(ā€¦)\` over \`Math.log(ā€¦) / Math.LN2\`.āŠ + ` + +## Invalid #7 + 1 | Math.log((( 0,x ))) * Math.LOG10E + +> Output + + `āŠ + 1 | Math.log10((( 0,x )))āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log((( 0,x ))) * Math.LOG10EāŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log10(ā€¦)\` over \`Math.log(ā€¦) * Math.LOG10E\`.āŠ + ` + +## Invalid #8 + 1 | Math.log((( 0,x ))) * Math.LOG2E + +> Output + + `āŠ + 1 | Math.log2((( 0,x )))āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log((( 0,x ))) * Math.LOG2EāŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log2(ā€¦)\` over \`Math.log(ā€¦) * Math.LOG2E\`.āŠ + ` + +## Invalid #9 + 1 | Math.LOG10E * Math.log((( 0,x ))) + +> Output + + `āŠ + 1 | Math.log10((( 0,x )))āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.LOG10E * Math.log((( 0,x )))āŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log10(ā€¦)\` over \`Math.LOG10E * Math.log(ā€¦)\`.āŠ + ` + +## Invalid #10 + 1 | Math.LOG2E * Math.log((( 0,x ))) + +> Output + + `āŠ + 1 | Math.log2((( 0,x )))āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.LOG2E * Math.log((( 0,x )))āŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log2(ā€¦)\` over \`Math.LOG2E * Math.log(ā€¦)\`.āŠ + ` + +## Invalid #11 + 1 | Math.log((( 0,x ))) / Math.LN10 + +> Output + + `āŠ + 1 | Math.log10((( 0,x )))āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log((( 0,x ))) / Math.LN10āŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log10(ā€¦)\` over \`Math.log(ā€¦) / Math.LN10\`.āŠ + ` + +## Invalid #12 + 1 | Math.log((( 0,x ))) / Math.LN2 + +> Output + + `āŠ + 1 | Math.log2((( 0,x )))āŠ + ` + +> Error 1/1 + + `āŠ + > 1 | Math.log((( 0,x ))) / Math.LN2āŠ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`Math.log2(ā€¦)\` over \`Math.log(ā€¦) / Math.LN2\`.āŠ + ` + +## Invalid #13 + 1 | function foo(x) { + 2 | return ( + 3 | Math.log(x) + 4 | / Math.LN10 + 5 | ); + 6 | } + +> Output + + `āŠ + 1 | function foo(x) {āŠ + 2 | return (āŠ + 3 | Math.log10(x)āŠ + 4 | );āŠ + 5 | }āŠ + ` + +> Error 1/1 + + `āŠ + 1 | function foo(x) {āŠ + 2 | return (āŠ + > 3 | Math.log(x)āŠ + | ^^^^^^^^^^^āŠ + > 4 | / Math.LN10āŠ + | ^^^^^^^^^^^^^^^ Prefer \`Math.log10(ā€¦)\` over \`Math.log(ā€¦) / Math.LN10\`.āŠ + 5 | );āŠ + 6 | }āŠ + ` + +## Invalid #14 + 1 | function foo(x) { + 2 | return ( + 3 | Math.log(x) + 4 | / Math.LN2 + 5 | ); + 6 | } + +> Output + + `āŠ + 1 | function foo(x) {āŠ + 2 | return (āŠ + 3 | Math.log2(x)āŠ + 4 | );āŠ + 5 | }āŠ + ` + +> Error 1/1 + + `āŠ + 1 | function foo(x) {āŠ + 2 | return (āŠ + > 3 | Math.log(x)āŠ + | ^^^^^^^^^^^āŠ + > 4 | / Math.LN2āŠ + | ^^^^^^^^^^^^^^ Prefer \`Math.log2(ā€¦)\` over \`Math.log(ā€¦) / Math.LN2\`.āŠ + 5 | );āŠ + 6 | }āŠ + ` diff --git a/test/snapshots/prefer-modern-math-apis.mjs.snap b/test/snapshots/prefer-modern-math-apis.mjs.snap new file mode 100644 index 0000000000000000000000000000000000000000..8a94f5ab7ae3bedff72cbd8bf93371d80c36be5c GIT binary patch literal 825 zcmV-91IGM8RzVlU$B-E_tk%%LEop zg5v)`tjN7^Z^4ni+p?P(&0X4KEt$ZgnoJOU2#D3WJEo}qnq9*0zTxoIiwk}-fkhRV zA@~ar>t{!*T=>H|cN){rNB#okKr=w1@+=U%2Z$}!L@LI~9(l8LdilH0{aa=;fkmG| zaV0AQLjkLrr(^LHkF%Uz@lxr0SxjKj*-)&^#=y|f`A5n$Q{VUN&!dqtb3)fKfkj=R z_$&}_TKR5kv5Lsrlu0G0S4!M{zyuck0L3-z3=HR1Mrez0g_*qP6O!B8GV=%%Sad!V zYjZF#T#!v-SjTuHUcp9p@}r2E225blFerWt#CLg~J(ypzQD~A{gZ7rBeG{0#q5_-{ zJPnAS@I9IoxwLUh#5&I>&5763n82b3pg59?f#Kz>&w`geua4ijxh~Z2`@0iNV9{zQ z{tLv+4D8_8WMpL!WDMj|P%u=eQAjJzOD@UG&r?Xt&)2BXRHz1u8v(^Ri&9HUi}DmS zfP7;RpVK$7BttJJKi$v(s17J<0ut4<2C__ntXeK)bYsZh|=)I<$}J_ewZ;c9*S-3<*~6|~?wk@aEq1tuWMUziqwmFS`R zLk7(sMmW9Eh2jlVhr?_E+G*rUq%SZ$VT31ckj+Q;LIBw**ocS(e3Tvu*a&NwkrYA* zvuK!jamG3@rBOBUVu^U5733%07JS|y5DgS1T$E@*al8nI<10`d%~*w2ZWy8D<_bc^ z1u^c#Fcn)FL840`jw7W=pjuHNikYy{omz*#FeVtP)GT~SbuLE85|QnwnB7PyHadvP zZ@B7h%KU&@kCPhU6<7kCXa`fxq2yM}82Opx#swWqjS_r8jlV1=zYGQdB{wSJ-wXf% DME89r literal 0 HcmV?d00001