Merge pull request #427 from quantified-uncertainty/squiggle-editor-bindings
Add bindings to Squiggle Editor
This commit is contained in:
		
						commit
						bb8ed5ce4f
					
				|  | @ -88,6 +88,7 @@ | |||
|     "@types/react": "17.0.43" | ||||
|   }, | ||||
|   "source": "./src/index.ts", | ||||
|   "browser": "dist/bundle.js", | ||||
|   "main": "dist/src/index.js", | ||||
|   "types": "dist/src/index.d.ts" | ||||
| } | ||||
|  |  | |||
|  | @ -3,10 +3,12 @@ import _ from "lodash"; | |||
| import styled from "styled-components"; | ||||
| import { | ||||
|   run, | ||||
|   runPartial, | ||||
|   errorValueToString, | ||||
|   squiggleExpression, | ||||
|   bindings, | ||||
|   samplingParams, | ||||
| } from "@quri/squiggle-lang"; | ||||
| import type { samplingParams } from "@quri/squiggle-lang"; | ||||
| import { NumberShower } from "./NumberShower"; | ||||
| import { DistributionChart } from "./DistributionChart"; | ||||
| import { ErrorBox } from "./ErrorBox"; | ||||
|  | @ -148,6 +150,8 @@ export interface SquiggleChartProps { | |||
|   /** CSS width of the element */ | ||||
|   width?: number; | ||||
|   height?: number; | ||||
|   /** Bindings of previous variables declared */ | ||||
|   bindings?: bindings; | ||||
| } | ||||
| 
 | ||||
| const ChartWrapper = styled.div` | ||||
|  | @ -162,13 +166,14 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({ | |||
|   outputXYPoints = 1000, | ||||
|   onChange = () => {}, | ||||
|   height = 60, | ||||
|   bindings = {}, | ||||
|   width = NaN, | ||||
| }: SquiggleChartProps) => { | ||||
|   let samplingInputs: samplingParams = { | ||||
|     sampleCount: sampleCount, | ||||
|     xyPointLength: outputXYPoints, | ||||
|   }; | ||||
|   let expressionResult = run(squiggleString, samplingInputs); | ||||
|   let expressionResult = run(squiggleString, bindings, samplingInputs); | ||||
|   let internal: JSX.Element; | ||||
|   if (expressionResult.tag === "Ok") { | ||||
|     let expression = expressionResult.value; | ||||
|  |  | |||
|  | @ -3,7 +3,9 @@ import * as ReactDOM from "react-dom"; | |||
| import { SquiggleChart } from "./SquiggleChart"; | ||||
| import { CodeEditor } from "./CodeEditor"; | ||||
| import styled from "styled-components"; | ||||
| import type { squiggleExpression } from "@quri/squiggle-lang"; | ||||
| import type { squiggleExpression, bindings } from "@quri/squiggle-lang"; | ||||
| import { runPartial, errorValueToString } from "@quri/squiggle-lang"; | ||||
| import { ErrorBox } from "./ErrorBox"; | ||||
| 
 | ||||
| export interface SquiggleEditorProps { | ||||
|   /** The input string for squiggle */ | ||||
|  | @ -26,6 +28,8 @@ export interface SquiggleEditorProps { | |||
|   onChange?(expr: squiggleExpression): void; | ||||
|   /** The width of the element */ | ||||
|   width: number; | ||||
|   /** Previous variable declarations */ | ||||
|   bindings: bindings; | ||||
| } | ||||
| 
 | ||||
| const Input = styled.div` | ||||
|  | @ -46,6 +50,7 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({ | |||
|   diagramCount, | ||||
|   onChange, | ||||
|   environment, | ||||
|   bindings = {}, | ||||
| }: SquiggleEditorProps) => { | ||||
|   let [expression, setExpression] = React.useState(initialSquiggleString); | ||||
|   return ( | ||||
|  | @ -71,6 +76,7 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({ | |||
|         diagramCount={diagramCount} | ||||
|         environment={environment} | ||||
|         onChange={onChange} | ||||
|         bindings={bindings} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
|  | @ -107,3 +113,76 @@ export function renderSquiggleEditorToDom(props: SquiggleEditorProps) { | |||
|   ); | ||||
|   return parent; | ||||
| } | ||||
| 
 | ||||
| export interface SquigglePartialProps { | ||||
|   /** The input string for squiggle */ | ||||
|   initialSquiggleString?: string; | ||||
|   /** If the output requires monte carlo sampling, the amount of samples */ | ||||
|   sampleCount?: number; | ||||
|   /** The amount of points returned to draw the distribution */ | ||||
|   outputXYPoints?: number; | ||||
|   kernelWidth?: number; | ||||
|   pointDistLength?: number; | ||||
|   /** If the result is a function, where the function starts */ | ||||
|   diagramStart?: number; | ||||
|   /** If the result is a function, where the function ends */ | ||||
|   diagramStop?: number; | ||||
|   /** If the result is a function, how many points along the function it samples */ | ||||
|   diagramCount?: number; | ||||
|   /** when the environment changes. Used again for notebook magic*/ | ||||
|   onChange?(expr: bindings): void; | ||||
|   /** The width of the element */ | ||||
|   width: number; | ||||
|   /** Previously declared variables */ | ||||
|   bindings: bindings; | ||||
| } | ||||
| 
 | ||||
| export let SquigglePartial: React.FC<SquigglePartialProps> = ({ | ||||
|   initialSquiggleString = "", | ||||
|   onChange, | ||||
|   bindings, | ||||
| }: SquigglePartialProps) => { | ||||
|   let [expression, setExpression] = React.useState(initialSquiggleString); | ||||
|   let squiggleResult = runPartial(expression, bindings); | ||||
|   if (squiggleResult.tag == "Ok") { | ||||
|     if (onChange) onChange(squiggleResult.value); | ||||
|   } | ||||
|   return ( | ||||
|     <div> | ||||
|       <Input> | ||||
|         <CodeEditor | ||||
|           value={expression} | ||||
|           onChange={setExpression} | ||||
|           oneLine={true} | ||||
|           showGutter={false} | ||||
|           height={20} | ||||
|         /> | ||||
|       </Input> | ||||
|       {squiggleResult.tag == "Error" ? ( | ||||
|         <ErrorBox heading="Error"> | ||||
|           {errorValueToString(squiggleResult.value)} | ||||
|         </ErrorBox> | ||||
|       ) : ( | ||||
|         <></> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export function renderSquigglePartialToDom(props: SquigglePartialProps) { | ||||
|   let parent = document.createElement("div"); | ||||
|   ReactDOM.render( | ||||
|     <SquigglePartial | ||||
|       {...props} | ||||
|       onChange={(bindings) => { | ||||
|         // @ts-ignore
 | ||||
|         parent.value = bindings; | ||||
| 
 | ||||
|         parent.dispatchEvent(new CustomEvent("input")); | ||||
|         if (props.onChange) props.onChange(bindings); | ||||
|       }} | ||||
|     />, | ||||
|     parent | ||||
|   ); | ||||
|   return parent; | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| export { SquiggleChart } from "./components/SquiggleChart"; | ||||
| export { | ||||
|   SquiggleEditor, | ||||
|   SquigglePartial, | ||||
|   renderSquiggleEditorToDom, | ||||
|   renderSquigglePartialToDom, | ||||
| } from "./components/SquiggleEditor"; | ||||
| import SquigglePlayground, { | ||||
|   renderSquigglePlaygroundToDom, | ||||
|  |  | |||
|  | @ -1,23 +1,5 @@ | |||
| import { | ||||
|   run, | ||||
|   Distribution, | ||||
|   resultMap, | ||||
|   squiggleExpression, | ||||
|   errorValueToString, | ||||
| } from "../../src/js/index"; | ||||
| 
 | ||||
| let testRun = (x: string): squiggleExpression => { | ||||
|   let result = run(x, { sampleCount: 100, xyPointLength: 100 }); | ||||
|   expect(result.tag).toEqual("Ok"); | ||||
|   if (result.tag === "Ok") { | ||||
|     return result.value; | ||||
|   } else { | ||||
|     throw Error( | ||||
|       "Expected squiggle expression to evaluate but got error: " + | ||||
|         errorValueToString(result.value) | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
| import { Distribution, resultMap } from "../../src/js/index"; | ||||
| import { testRun, testRunPartial } from "./TestHelpers"; | ||||
| 
 | ||||
| function Ok<b>(x: b) { | ||||
|   return { tag: "Ok", value: x }; | ||||
|  | @ -75,6 +57,17 @@ describe("Record", () => { | |||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe("Partials", () => { | ||||
|   test("Can pass variables between partials and cells", () => { | ||||
|     let bindings = testRunPartial(`x = 5`); | ||||
|     let bindings2 = testRunPartial(`y = x + 2`, bindings); | ||||
|     expect(testRun(`y + 3`, bindings2)).toEqual({ | ||||
|       tag: "number", | ||||
|       value: 10, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe("Distribution", () => { | ||||
|   //It's important that sampleCount is less than 9. If it's more, than that will create randomness
 | ||||
|   //Also, note, the value should be created using makeSampleSetDist() later on.
 | ||||
|  |  | |||
|  | @ -1,14 +1,16 @@ | |||
| import { | ||||
|   run, | ||||
|   // Distribution,
 | ||||
|   runPartial, | ||||
|   bindings, | ||||
|   squiggleExpression, | ||||
|   errorValueToString, | ||||
|   // errorValue,
 | ||||
|   // result,
 | ||||
| } from "../../src/js/index"; | ||||
| 
 | ||||
| export function testRun(x: string): squiggleExpression { | ||||
|   let squiggleResult = run(x, { sampleCount: 1000, xyPointLength: 100 }); | ||||
| export function testRun(x: string, bindings = {}): squiggleExpression { | ||||
|   let squiggleResult = run(x, bindings, { | ||||
|     sampleCount: 1000, | ||||
|     xyPointLength: 100, | ||||
|   }); | ||||
|   // return squiggleResult.value
 | ||||
|   if (squiggleResult.tag === "Ok") { | ||||
|     return squiggleResult.value; | ||||
|  | @ -21,6 +23,22 @@ export function testRun(x: string): squiggleExpression { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| export function testRunPartial(x: string, bindings: bindings = {}): bindings { | ||||
|   let squiggleResult = runPartial(x, bindings, { | ||||
|     sampleCount: 1000, | ||||
|     xyPointLength: 100, | ||||
|   }); | ||||
|   if (squiggleResult.tag === "Ok") { | ||||
|     return squiggleResult.value; | ||||
|   } else { | ||||
|     throw new Error( | ||||
|       `Expected squiggle expression to evaluate but got error: ${errorValueToString( | ||||
|         squiggleResult.value | ||||
|       )}` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function failDefault() { | ||||
|   expect("be reached").toBe("codepath should never"); | ||||
| } | ||||
|  |  | |||
|  | @ -2,7 +2,9 @@ import * as _ from "lodash"; | |||
| import { | ||||
|   genericDist, | ||||
|   samplingParams, | ||||
|   evaluate, | ||||
|   evaluateUsingExternalBindings, | ||||
|   evaluatePartialUsingExternalBindings, | ||||
|   externalBindings, | ||||
|   expressionValue, | ||||
|   errorValue, | ||||
|   distributionError, | ||||
|  | @ -46,7 +48,7 @@ import { | |||
|   Constructors_pointwiseLogarithm, | ||||
|   Constructors_pointwisePower, | ||||
| } from "../rescript/Distributions/DistributionOperation/DistributionOperation.gen"; | ||||
| export type { samplingParams, errorValue }; | ||||
| export type { samplingParams, errorValue, externalBindings as bindings }; | ||||
| 
 | ||||
| export let defaultSamplingInputs: samplingParams = { | ||||
|   sampleCount: 10000, | ||||
|  | @ -96,15 +98,28 @@ export type squiggleExpression = | |||
| 
 | ||||
| export function run( | ||||
|   squiggleString: string, | ||||
|   bindings?: externalBindings, | ||||
|   samplingInputs?: samplingParams | ||||
| ): result<squiggleExpression, errorValue> { | ||||
|   let b = bindings ? bindings : {}; | ||||
|   let si: samplingParams = samplingInputs | ||||
|     ? samplingInputs | ||||
|     : defaultSamplingInputs; | ||||
|   let result: result<expressionValue, errorValue> = evaluate(squiggleString); | ||||
| 
 | ||||
|   let result: result<expressionValue, errorValue> = | ||||
|     evaluateUsingExternalBindings(squiggleString, b); | ||||
|   return resultMap(result, (x) => createTsExport(x, si)); | ||||
| } | ||||
| 
 | ||||
| // Run Partial. A partial is a block of code that doesn't return a value
 | ||||
| export function runPartial( | ||||
|   squiggleString: string, | ||||
|   bindings: externalBindings, | ||||
|   _samplingInputs?: samplingParams | ||||
| ): result<externalBindings, errorValue> { | ||||
|   return evaluatePartialUsingExternalBindings(squiggleString, bindings); | ||||
| } | ||||
| 
 | ||||
| function createTsExport( | ||||
|   x: expressionValue, | ||||
|   sampEnv: samplingParams | ||||
|  | @ -166,7 +181,7 @@ function createTsExport( | |||
|   } | ||||
| } | ||||
| 
 | ||||
| // Helper functions to convert the recsript representations that genType doesn't
 | ||||
| // Helper functions to convert the rescript representations that genType doesn't
 | ||||
| // cover
 | ||||
| function convertRawToTypescript( | ||||
|   result: rescriptExport, | ||||
|  |  | |||
|  | @ -40,6 +40,12 @@ let evaluate = Reducer.evaluate | |||
| @genType | ||||
| let evaluateUsingExternalBindings = Reducer.evaluateUsingExternalBindings | ||||
| 
 | ||||
| @genType | ||||
| let evaluatePartialUsingExternalBindings = Reducer.evaluatePartialUsingExternalBindings | ||||
| 
 | ||||
| @genType | ||||
| type externalBindings = Reducer.externalBindings | ||||
| 
 | ||||
| @genType | ||||
| type expressionValue = ReducerInterface_ExpressionValue.expressionValue | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										51
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								yarn.lock
									
									
									
									
									
								
							|  | @ -2506,9 +2506,9 @@ | |||
|   integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== | ||||
| 
 | ||||
| "@sinclair/typebox@^0.23.3": | ||||
|   version "0.23.4" | ||||
|   resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.23.4.tgz#6ff93fd2585ce44f7481c9ff6af610fbb5de98a4" | ||||
|   integrity sha512-0/WqSvpVbCBAV1yPeko7eAczKbs78dNVAaX14quVlwOb2wxfKuXCx91h4NrEfkYK9zEnyVSW4JVI/trP3iS+Qg== | ||||
|   version "0.23.5" | ||||
|   resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.23.5.tgz#93f7b9f4e3285a7a9ade7557d9a8d36809cbc47d" | ||||
|   integrity sha512-AFBVi/iT4g20DHoujvMH1aEDn8fGJh4xsRGCP6d8RpLPMqsNPvW01Jcn0QysXTsg++/xj25NmJsGyH9xug/wKg== | ||||
| 
 | ||||
| "@sindresorhus/is@^0.14.0": | ||||
|   version "0.14.0" | ||||
|  | @ -4071,9 +4071,9 @@ | |||
|     form-data "^3.0.0" | ||||
| 
 | ||||
| "@types/node@*", "@types/node@^17.0.29", "@types/node@^17.0.5": | ||||
|   version "17.0.29" | ||||
|   resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.29.tgz#7f2e1159231d4a077bb660edab0fde373e375a3d" | ||||
|   integrity sha512-tx5jMmMFwx7wBwq/V7OohKDVb/JwJU5qCVkeLMh1//xycAJ/ESuw9aJ9SEtlCZDYi2pBfe4JkisSoAtbOsBNAA== | ||||
|   version "17.0.30" | ||||
|   resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.30.tgz#2c6e8512acac70815e8176aa30c38025067880ef" | ||||
|   integrity sha512-oNBIZjIqyHYP8VCNAV9uEytXVeXG2oR0w9lgAXro20eugRQfY002qr3CUl6BAe+Yf/z3CRjPdz27Pu6WWtuSRw== | ||||
| 
 | ||||
| "@types/node@^14.0.10": | ||||
|   version "14.18.16" | ||||
|  | @ -4140,17 +4140,10 @@ | |||
|   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" | ||||
|   integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== | ||||
| 
 | ||||
| "@types/react-dom@^18.0.0": | ||||
|   version "18.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.0.tgz#b13f8d098e4b0c45df4f1ed123833143b0c71141" | ||||
|   integrity sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg== | ||||
|   dependencies: | ||||
|     "@types/react" "*" | ||||
| 
 | ||||
| "@types/react-dom@^18.0.2": | ||||
|   version "18.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.2.tgz#2d6b46557aa30257e87e67a6d952146d15979d79" | ||||
|   integrity sha512-UxeS+Wtj5bvLRREz9tIgsK4ntCuLDo0EcAcACgw3E+9wE8ePDr9uQpq53MfcyxyIS55xJ+0B6mDS8c4qkkHLBg== | ||||
| "@types/react-dom@^18.0.0", "@types/react-dom@^18.0.2": | ||||
|   version "18.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.3.tgz#a022ea08c75a476fe5e96b675c3e673363853831" | ||||
|   integrity sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ== | ||||
|   dependencies: | ||||
|     "@types/react" "*" | ||||
| 
 | ||||
|  | @ -4809,9 +4802,9 @@ acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0: | |||
|   integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== | ||||
| 
 | ||||
| address@^1.0.1, address@^1.1.2: | ||||
|   version "1.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" | ||||
|   integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/address/-/address-1.2.0.tgz#d352a62c92fee90f89a693eccd2a8b2139ab02d9" | ||||
|   integrity sha512-tNEZYz5G/zYunxFm7sfhAxkXEuLj3K6BKwv6ZURlsF6yiUQ65z0Q2wZW9L5cPUl9ocofGvXOdFYbFHp0+6MOig== | ||||
| 
 | ||||
| adjust-sourcemap-loader@^4.0.0: | ||||
|   version "4.0.0" | ||||
|  | @ -7906,9 +7899,9 @@ ejs@^3.1.6: | |||
|     jake "^10.8.5" | ||||
| 
 | ||||
| electron-to-chromium@^1.4.118: | ||||
|   version "1.4.124" | ||||
|   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.124.tgz#e9015e234d8632920dcdf5480351da9e845ed220" | ||||
|   integrity sha512-VhaE9VUYU6d2eIb+4xf83CATD+T+3bTzvxvlADkQE+c2hisiw3sZmvEDtsW704+Zky9WZGhBuQXijDVqSriQLA== | ||||
|   version "1.4.127" | ||||
|   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.127.tgz#4ef19d5d920abe2676d938f4170729b44f7f423a" | ||||
|   integrity sha512-nhD6S8nKI0O2MueC6blNOEZio+/PWppE/pevnf3LOlQA/fKPCrDp2Ao4wx4LFwmIkJpVdFdn2763YWLy9ENIZg== | ||||
| 
 | ||||
| element-resize-detector@^1.2.2: | ||||
|   version "1.2.4" | ||||
|  | @ -16599,9 +16592,9 @@ terser@^4.1.2, terser@^4.6.3: | |||
|     source-map-support "~0.5.12" | ||||
| 
 | ||||
| terser@^5.0.0, terser@^5.10.0, terser@^5.3.4, terser@^5.7.2: | ||||
|   version "5.13.0" | ||||
|   resolved "https://registry.yarnpkg.com/terser/-/terser-5.13.0.tgz#d43fd71861df1b4df743980caa257c6fa03acc44" | ||||
|   integrity sha512-sgQ99P+fRBM1jAYzN9RTnD/xEWx/7LZgYTCRgmYriSq1wxxqiQPJgXkkLBBuwySDWJ2PP0PnVQyuf4xLUuH4Ng== | ||||
|   version "5.13.1" | ||||
|   resolved "https://registry.yarnpkg.com/terser/-/terser-5.13.1.tgz#66332cdc5a01b04a224c9fad449fc1a18eaa1799" | ||||
|   integrity sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA== | ||||
|   dependencies: | ||||
|     acorn "^8.5.0" | ||||
|     commander "^2.20.0" | ||||
|  | @ -16968,9 +16961,9 @@ typedarray@^0.0.6: | |||
|   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= | ||||
| 
 | ||||
| typescript@^4.6.3: | ||||
|   version "4.6.3" | ||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" | ||||
|   integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== | ||||
|   version "4.6.4" | ||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" | ||||
|   integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== | ||||
| 
 | ||||
| ua-parser-js@^0.7.30: | ||||
|   version "0.7.31" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user