From b746a0e6abb0cb41d5379f052528d1b0398f2371 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 15:16:11 -0700 Subject: [PATCH] Add tesseract and foreign object rendering (#86) (#87) Signed-off-by: Joshua Li (cherry picked from commit 503ee051dd909a970aaf0864114624d805a1a2cf) Co-authored-by: Joshua Li --- NOTICE.txt | 3 + common/tesseract/.gitignore | 2 + package.json | 5 +- .../context_menu/context_menu_ui.js | 54 ++--- .../visual_report/assets/report_styles.ts | 1 + .../visual_report/generate_report.ts | 138 ++++++++--- .../{patch-html2canvas.js => postinstall.js} | 15 ++ server/plugin.ts | 32 ++- server/routes/index.ts | 2 + server/routes/tesseract.ts | 137 +++++++++++ yarn.lock | 224 +++++++++++++++++- 11 files changed, 543 insertions(+), 70 deletions(-) create mode 100644 common/tesseract/.gitignore rename scripts/{patch-html2canvas.js => postinstall.js} (65%) create mode 100644 server/routes/tesseract.ts diff --git a/NOTICE.txt b/NOTICE.txt index 731cb600..ae67cad0 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,2 +1,5 @@ OpenSearch (https://opensearch.org/) Copyright OpenSearch Contributors + +This product includes software developed by +naptha (https://github.com/naptha/tesseract.js/) diff --git a/common/tesseract/.gitignore b/common/tesseract/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/common/tesseract/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/package.json b/package.json index d6956f65..e0cfbded 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "cypress:run": "cypress run", "cypress:open": "cypress open", "plugin-helpers": "node ../../scripts/plugin_helpers", - "postinstall": "node ./scripts/patch-html2canvas.js" + "postinstall": "node ./scripts/postinstall.js" }, "dependencies": { "babel-polyfill": "^6.26.0", @@ -38,7 +38,8 @@ "react-router-dom": "^5.3.0", "react-toast-notifications": "^2.4.0", "set-interval-async": "1.0.33", - "showdown": "^1.9.1" + "showdown": "^1.9.1", + "tesseract.js": "^4.0.2" }, "devDependencies": { "@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards", diff --git a/public/components/context_menu/context_menu_ui.js b/public/components/context_menu/context_menu_ui.js index 119b7bae..2dfd798f 100644 --- a/public/components/context_menu/context_menu_ui.js +++ b/public/components/context_menu/context_menu_ui.js @@ -8,7 +8,7 @@ import { i18n } from '@osd/i18n'; export const getMenuItem = (name) => { return ` `; }; @@ -46,7 +46,7 @@ export const popoverMenu = (savedObjectAvailable) => {
- + ${i18n.translate( 'opensearch.reports.menu.visual.generateReport', { defaultMessage: 'Generate report' } @@ -54,25 +54,25 @@ export const popoverMenu = (savedObjectAvailable) => {
- + ${message}
<${button} class="${buttonClass}" type="button" data-test-subj="downloadPanel-GeneratePDF" id="generatePDF"> - + - ${i18n.translate( + ${i18n.translate( 'opensearch.reports.menu.visual.downloadPdf', { defaultMessage: 'Download PDF' } )} <${button} class="${buttonClass}" type="button" data-test-subj="downloadPanel-GeneratePNG" id="generatePNG"> - + - ${i18n.translate( + ${i18n.translate( 'opensearch.reports.menu.visual.downloadPng', { defaultMessage: 'Download PNG' } )} @@ -81,7 +81,7 @@ export const popoverMenu = (savedObjectAvailable) => {
- + ${i18n.translate( 'opensearch.reports.menu.visual.scheduleAndShare', { defaultMessage: 'Schedule and share' } @@ -90,9 +90,9 @@ export const popoverMenu = (savedObjectAvailable) => {
<${button} class="${buttonClass}" type="button" data-test-subj="downloadPanel-GeneratePDF" id="createReportDefinition"> - + - ${i18n.translate( + ${i18n.translate( 'opensearch.reports.menu.visual.createReportDefinition', { defaultMessage: 'Create report definition' } )} @@ -101,7 +101,7 @@ export const popoverMenu = (savedObjectAvailable) => {
- + ${i18n.translate('opensearch.reports.menu.visual.view', { defaultMessage: 'View', })} @@ -109,13 +109,13 @@ export const popoverMenu = (savedObjectAvailable) => {
- + ${i18n.translate( 'opensearch.reports.menu.scheduleAndShare', { @@ -202,9 +202,9 @@ export const popoverMenuDiscover = (savedObjectAvailable) => {
<${button} class="${buttonClass}" type="button" data-test-subj="downloadPanel-GeneratePDF" id="createReportDefinition"> - + - ${i18n.translate( + ${i18n.translate( 'opensearch.reports.menu.createReportDefinition', { defaultMessage: 'Create report definition' } )} @@ -213,7 +213,7 @@ export const popoverMenuDiscover = (savedObjectAvailable) => {
- + ${i18n.translate('opensearch.reports.menu.csv.view', { defaultMessage: 'View', })} @@ -221,13 +221,13 @@ export const popoverMenuDiscover = (savedObjectAvailable) => {
-
+
diff --git a/public/components/visual_report/assets/report_styles.ts b/public/components/visual_report/assets/report_styles.ts index a78ee8a8..ab0d2f2a 100644 --- a/public/components/visual_report/assets/report_styles.ts +++ b/public/components/visual_report/assets/report_styles.ts @@ -8,6 +8,7 @@ html, body { margin: 0; padding: 0; + padding-top: 0px; } iframe, embed, object { diff --git a/public/components/visual_report/generate_report.ts b/public/components/visual_report/generate_report.ts index 57254f07..5c93695e 100644 --- a/public/components/visual_report/generate_report.ts +++ b/public/components/visual_report/generate_report.ts @@ -6,6 +6,7 @@ import createDOMPurify from 'dompurify'; import html2canvas from 'html2canvas'; import jsPDF from 'jspdf'; +import { createWorker } from 'tesseract.js'; import { v1 as uuidv1 } from 'uuid'; import { ReportSchemaType } from '../../../server/model'; import { uiSettingsService } from '../utils/settings_service'; @@ -57,7 +58,9 @@ const removeNonReportElements = ( reportSource: VISUAL_REPORT_TYPE ) => { // remove buttons - doc.querySelectorAll("button[class^='euiButton']:not(.visLegend__button)").forEach((e) => e.remove()); + doc + .querySelectorAll("button[class^='euiButton']:not(.visLegend__button)") + .forEach((e) => e.remove()); // remove top navBar doc.querySelectorAll("[class^='euiHeader']").forEach((e) => e.remove()); // remove visualization editor @@ -65,7 +68,6 @@ const removeNonReportElements = ( doc.querySelector('[data-test-subj="splitPanelResizer"]')?.remove(); doc.querySelector('.visEditor__collapsibleSidebar')?.remove(); } - doc.body.style.paddingTop = '0px'; }; const addReportHeader = (doc: Document, header: string) => { @@ -96,8 +98,10 @@ const addReportFooter = (doc: Document, footer: string) => { const addReportStyle = (doc: Document, style: string) => { const styleElement = document.createElement('style'); + styleElement.className = 'reportInjectedStyles'; styleElement.innerHTML = style; doc.getElementsByTagName('head')[0].appendChild(styleElement); + doc.body.style.paddingTop = '0px'; }; const computeHeight = (height: number, header: string, footer: string) => { @@ -115,6 +119,7 @@ const computeHeight = (height: number, header: string, footer: string) => { export const generateReport = async (id: string, forceDelay = 15000) => { const http = uiSettingsService.getHttpClient(); + const useForeignObjectRendering = uiSettingsService.get('reporting:useFOR'); const DOMPurify = createDOMPurify(window); const report = await http.get( @@ -154,6 +159,26 @@ export const generateReport = async (id: string, forceDelay = 15000) => { } await timeout(forceDelay); + // Style changes onclone does not work with foreign object rendering enabled. + // Additionally increase span width to prevent text being truncated + if (useForeignObjectRendering) { + document + .querySelectorAll('span:not([data-html2canvas-ignore])') + .forEach((el) => { + if (!el.closest('.globalFilterItem')) + el.style.width = el.offsetWidth + 30 + 'px'; + }); + document + .querySelectorAll( + 'span.globalFilterItem:not([data-html2canvas-ignore])' + ) + .forEach((el) => (el.style.width = el.offsetWidth + 5 + 'px')); + addReportHeader(document, header); + addReportFooter(document, footer); + addReportStyle(document, reportingStyle); + await timeout(1000); + } + const width = document.documentElement.scrollWidth; const height = computeHeight( document.documentElement.scrollHeight, @@ -170,40 +195,87 @@ export const generateReport = async (id: string, forceDelay = 15000) => { imageTimeout: 30000, useCORS: true, removeContainer: false, + allowTaint: true, + foreignObjectRendering: useForeignObjectRendering, onclone: function (documentClone) { removeNonReportElements(documentClone, reportSource); - addReportHeader(documentClone, header); - addReportFooter(documentClone, footer); - addReportStyle(documentClone, reportingStyle); + if (!useForeignObjectRendering) { + addReportHeader(documentClone, header); + addReportFooter(documentClone, footer); + addReportStyle(documentClone, reportingStyle); + } }, - }).then(function (canvas) { - // TODO remove this and 'removeContainer: false' when https://github.com/niklasvh/html2canvas/pull/2949 is merged - document - .querySelectorAll('.html2canvas-container') - .forEach((e) => { - const iframe = e.contentWindow; - if (e) { - e.src = 'about:blank'; - if (iframe) { - iframe.document.write(''); - iframe.document.clear(); - iframe.close(); + }) + .then(async function (canvas) { + // TODO remove this and 'removeContainer: false' when https://github.com/niklasvh/html2canvas/pull/2949 is merged + document + .querySelectorAll('.html2canvas-container') + .forEach((e) => { + const iframe = e.contentWindow; + if (e) { + e.src = 'about:blank'; + if (iframe) { + iframe.document.write(''); + iframe.document.clear(); + iframe.close(); + } + e.remove(); } - e.remove(); - } - }); + }); - if (format === 'png') { - const link = document.createElement('a'); - link.download = fileName; - link.href = canvas.toDataURL(); - link.click(); - } else { - const orient = canvas.width > canvas.height ? 'landscape' : 'portrait'; - const pdf = new jsPDF(orient, 'px', [canvas.width, canvas.height]); - pdf.addImage(canvas, 'JPEG', 0, 0, canvas.width, canvas.height); - pdf.save(fileName); - } - return true; - }); + if (format === 'png') { + const link = document.createElement('a'); + link.download = fileName; + link.href = canvas.toDataURL(); + link.click(); + } else if (uiSettingsService.get('reporting:useOcr')) { + const worker = await createWorker({ + workerPath: '../api/reporting/tesseract.js/dist/worker.min.js', + langPath: '../api/reporting/tesseract-lang-data', + corePath: '../api/reporting/tesseract.js-core/tesseract-core.wasm.js', + }); + await worker.loadLanguage('eng'); + await worker.initialize('eng'); + const { + data: { text, pdf }, + } = await worker + .recognize(canvas.toDataURL(), { pdfTitle: fileName }, { pdf: true }) + .catch((e) => console.error('recognize', e)); + await worker.terminate(); + + const blob = new Blob([new Uint8Array(pdf)], { + type: 'application/pdf', + }); + const link = document.createElement('a'); + if (link.download !== undefined) { + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', fileName); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } else { + const orient = canvas.width > canvas.height ? 'landscape' : 'portrait'; + const pdf = new jsPDF(orient, 'px', [canvas.width, canvas.height]); + pdf.addImage(canvas, 'JPEG', 0, 0, canvas.width, canvas.height); + pdf.save(fileName); + } + return true; + }) + .finally(() => { + if (useForeignObjectRendering) { + document + .querySelectorAll( + 'span:not(.data-html2canvas-ignore)' + ) + .forEach((el) => (el.style.width = '')); + document.querySelectorAll('.reportWrapper').forEach((e) => e.remove()); + document + .querySelectorAll('.reportInjectedStyles') + .forEach((e) => e.remove()); + document.body.style.paddingTop = ''; + } + }); }; diff --git a/scripts/patch-html2canvas.js b/scripts/postinstall.js similarity index 65% rename from scripts/patch-html2canvas.js rename to scripts/postinstall.js index c01f5bf4..1e53f1c3 100644 --- a/scripts/patch-html2canvas.js +++ b/scripts/postinstall.js @@ -5,6 +5,8 @@ // @ts-check // workaround for Safari support before https://github.com/niklasvh/html2canvas/pull/2911 is merged +const https = require('https'); +const fs = require('fs'); const replace = require('replace-in-file'); const options = { @@ -31,3 +33,16 @@ try { error ); } + +// download tesseract model +const modelFile = fs.createWriteStream(__dirname + '/../common/tesseract/eng.traineddata.gz'); +https.get( + 'https://raw.githubusercontent.com/naptha/tessdata/gh-pages/4.0.0_best/eng.traineddata.gz', + function (response) { + response.pipe(modelFile); + modelFile.on('finish', () => { + modelFile.close(); + console.log('Downloaded eng.traineddata.gz for tesseract.js'); + }); + } +); diff --git a/server/plugin.ts b/server/plugin.ts index d7e43ede..c45571c1 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -3,23 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { schema } from '@osd/config-schema'; import { - PluginInitializerContext, CoreSetup, CoreStart, - Plugin, - Logger, ILegacyClusterClient, + Logger, + Plugin, + PluginInitializerContext, } from '../../../src/core/server'; import opensearchReportsPlugin from './backend/opensearch-reports-plugin'; +import { NotificationsPlugin } from './clusters/notificationsPlugin'; +import { buildConfig, ReportingConfigType } from './config'; +import { ReportingConfig } from './config/config'; +import registerRoutes from './routes'; import { ReportsDashboardsPluginSetup, ReportsDashboardsPluginStart, } from './types'; -import registerRoutes from './routes'; -import { NotificationsPlugin } from './clusters/notificationsPlugin'; -import { buildConfig, ReportingConfigType } from './config'; -import { ReportingConfig } from './config/config'; export interface ReportsPluginRequestContext { logger: Logger; @@ -49,6 +50,23 @@ export class ReportsDashboardsPlugin public async setup(core: CoreSetup) { this.logger.debug('reports-dashboards: Setup'); + core.uiSettings.register({ + 'reporting:useOcr': { + name: 'Reporting use OCR on PDF', + value: false, + description: + 'Whether to run optical character recognition on PDF reports to make text selectable', + schema: schema.boolean(), + }, + 'reporting:useFOR': { + name: 'Reporting use ForeignObject rendering', + value: true, + description: + 'Whether to use ForeignObject rendering when generating reports. If it causes issues, try disabling this option.', + schema: schema.boolean(), + }, + }); + try { const config = await buildConfig( this.initializerContext, diff --git a/server/routes/index.ts b/server/routes/index.ts index 51b93476..0c89e112 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -8,6 +8,7 @@ import registerReportDefinitionRoute from './reportDefinition'; import registerReportSourceRoute from './reportSource'; import registerMetricRoute from './metric'; import registerNotificationRoute from './notifications'; +import registerTesseractRoute from './tesseract'; import { IRouter } from '../../../../src/core/server'; import { ReportingConfig } from 'server/config/config'; @@ -17,4 +18,5 @@ export default function (router: IRouter, config: ReportingConfig) { registerReportSourceRoute(router); registerMetricRoute(router); registerNotificationRoute(router); + registerTesseractRoute(router); } diff --git a/server/routes/tesseract.ts b/server/routes/tesseract.ts new file mode 100644 index 00000000..fa25f348 --- /dev/null +++ b/server/routes/tesseract.ts @@ -0,0 +1,137 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ResponseError } from '@opensearch-project/opensearch/lib/errors'; +import fs from 'fs'; +import path from 'path'; +import { + IOpenSearchDashboardsResponse, + IRouter, +} from '../../../../src/core/server'; +import { API_PREFIX } from '../../common'; +import { errorResponse } from './utils/helpers'; + +/** + * Tesseract.js by default uses CDN to host resources needed to spawn workers + * (https://github.com/naptha/tesseract.js/blob/028a44f/docs/local-installation.md). + * OSD does not allow the CDN scripts unless user defines `csp.rules` in + * `opensearch_dashboards.yml` as `"script-src 'unsafe-eval' 'self' + * https://unpkg.com https://tessdata.projectnaptha.com"`. + * + * These routes are used to mimic the CDN. Currently only english traineddata is + * included and supported. + */ +export default function (router: IRouter) { + router.get( + { + path: `${API_PREFIX}/tesseract.js/dist/worker.min.js`, + validate: false, + }, + async ( + context, + request, + response + ): Promise> => { + //@ts-ignore + const logger: Logger = context.reporting_plugin.logger; + try { + const filePath = path.join( + __dirname, + '..', + '..', + 'node_modules', + 'tesseract.js', + 'dist', + 'worker.min.js' + ); + const fileContent = await fs.promises + .readFile(filePath) + .then((file) => file.toString()); + return response.custom({ + body: fileContent, + headers: { 'Content-Type': 'application/javascript' }, + statusCode: 200, + }); + } catch (error) { + logger.error(`failed during get tesseract.js worker file: ${error}`); + return errorResponse(response, error); + } + } + ); + router.get( + { + path: `${API_PREFIX}/tesseract.js-core/tesseract-core.wasm.js`, + validate: false, + }, + async ( + context, + request, + response + ): Promise> => { + //@ts-ignore + const logger: Logger = context.reporting_plugin.logger; + try { + const filePath = path.join( + __dirname, + '..', + '..', + 'node_modules', + 'tesseract.js-core', + 'tesseract-core.wasm.js' + ); + const fileContent = await fs.promises + .readFile(filePath) + .then((file) => file.toString()); + return response.custom({ + body: fileContent, + headers: { + 'Content-Type': 'application/javascript', + }, + statusCode: 200, + }); + } catch (error) { + logger.error(`failed during get tesseract.js-core wasm file: ${error}`); + return errorResponse(response, error); + } + } + ); + router.get( + { + path: `${API_PREFIX}/tesseract-lang-data/eng.traineddata.gz`, + validate: false, + }, + async ( + context, + request, + response + ): Promise> => { + //@ts-ignore + const logger: Logger = context.reporting_plugin.logger; + try { + const filePath = path.join( + __dirname, + '..', + '..', + 'common', + 'tesseract', + 'eng.traineddata.gz' + ); + const file = await fs.promises.readFile(filePath); + return response.custom({ + body: file, + headers: { + 'Content-Type': 'application/gzip', + }, + statusCode: 200, + }); + } catch (error) { + logger.error( + `failed during get tesseract.js eng.traineddata file: ${error}` + ); + return errorResponse(response, error); + } + } + ); +} diff --git a/yarn.lock b/yarn.lock index 28c43cff..30a94f05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,13 @@ dependencies: "@babel/highlight" "^7.16.7" +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + "@babel/compat-data@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" @@ -89,6 +96,16 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.21.1": + version "7.21.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.1.tgz#951cc626057bc0af2c35cd23e9c64d384dea83dd" + integrity sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA== + dependencies: + "@babel/types" "^7.21.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-compilation-targets@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" @@ -106,6 +123,11 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + "@babel/helper-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" @@ -124,6 +146,14 @@ "@babel/template" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== + dependencies: + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" + "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -145,6 +175,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + "@babel/helper-member-expression-to-functions@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" @@ -252,6 +289,18 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" @@ -262,6 +311,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + "@babel/helper-validator-option@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" @@ -303,6 +357,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.12.3", "@babel/parser@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" @@ -313,6 +376,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== +"@babel/parser@^7.20.7", "@babel/parser@^7.21.2", "@babel/parser@^7.7.0": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" + integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -443,6 +511,15 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/template@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095" @@ -474,6 +551,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.7.0": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.2.tgz#ac7e1f27658750892e815e60ae90f382a46d8e75" + integrity sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.21.1" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.21.2" + "@babel/types" "^7.21.2" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.12.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" @@ -500,6 +593,15 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.7.0": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.2.tgz#92246f6e00f91755893c2876ad653db70c8310d1" + integrity sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" @@ -694,11 +796,35 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + "@jridgewell/resolve-uri@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.11" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" @@ -712,6 +838,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@react-navigation/core@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-3.7.7.tgz#398b23836928f96d23eb60a10f8be77b160f1284" @@ -1385,6 +1519,18 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +babel-eslint@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" + integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.7.0" + "@babel/traverse" "^7.7.0" + "@babel/types" "^7.7.0" + eslint-visitor-keys "^1.0.0" + resolve "^1.12.0" + babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -1565,6 +1711,11 @@ bluebird@^3.5.5, bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bmp-js@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233" + integrity sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw== + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -2837,6 +2988,11 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-visitor-keys@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -3030,6 +3186,11 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +file-type@^12.4.1: + version "12.4.2" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9" + integrity sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -3544,6 +3705,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +idb-keyval@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-3.2.0.tgz#cbbf354deb5684b6cdc84376294fc05932845bd6" + integrity sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ== + identity-obj-proxy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" @@ -3770,6 +3936,11 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-electron@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9" + integrity sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg== + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -3945,6 +4116,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-url@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" + integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -4749,6 +4925,13 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4946,6 +5129,11 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +opencollective-postinstall@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" + integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -5530,7 +5718,7 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.7: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.7: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== @@ -6292,6 +6480,30 @@ terser@^4.1.2, terser@^4.8.1: source-map "~0.6.1" source-map-support "~0.5.12" +tesseract.js-core@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/tesseract.js-core/-/tesseract.js-core-4.0.2.tgz#69b790be1be603d9931951b41b9a664a382defd2" + integrity sha512-ZVyYPN+ZAos31ZErqzcFme6XaOA8xrZxisNyk3nTicHVPSqtQH7UgZ36XoZZY0Exeugr5A+Uxv5ll9aMd1c0jg== + +tesseract.js@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/tesseract.js/-/tesseract.js-4.0.2.tgz#473fe5e8b6da358f38be9822e72e3e9baa285184" + integrity sha512-OKc6N68czBa8QnoBwx+kAUG4OyqskXa1O7wSrOXOEYZ0TQcGa3daRVIsRhtVAmNMTrvZnOsg49kfNrw0BZaFEA== + dependencies: + babel-eslint "^10.1.0" + bmp-js "^0.1.0" + file-type "^12.4.1" + idb-keyval "^3.2.0" + is-electron "^2.2.0" + is-url "^1.2.4" + node-fetch "^2.6.0" + opencollective-postinstall "^2.0.2" + regenerator-runtime "^0.13.3" + resolve-url "^0.2.1" + tesseract.js-core "^4.0.2" + wasm-feature-detect "^1.2.11" + zlibjs "^0.3.1" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -6739,6 +6951,11 @@ walker@^1.0.7: dependencies: makeerror "1.0.x" +wasm-feature-detect@^1.2.11: + version "1.5.1" + resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.5.1.tgz#0db57a7d7f8c26b743dde85386215ae2b135e78a" + integrity sha512-GHr23qmuehNXHY4902/hJ6EV5sUANIJC3R/yMfQ7hWDg3nfhlcJfnIL96R2ohpIwa62araN6aN4bLzzzq5GXkg== + watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" @@ -7043,3 +7260,8 @@ yauzl@^2.10.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +zlibjs@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554" + integrity sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==